How to select route based on header in Zuul - netflix-zuul

I'm using a Netflix Zuul proxy in front of my services A and B.
How can I make the Zuul proxy to choose between routes to A and B based on a HTTP header in the incoming request?

You should create a prefilter based on your logic. Something like this :
#Component
public class RedirectionFilter extends ZuulFilter {
#Override
public String filterType() {
return "pre";
}
#Override
public int filterOrder() {
return 2;
}
#Override
public boolean shouldFilter() {
return true;
}
#Override
public Object run() {
RequestContext ctx = RequestContext.getCurrentContext();
HttpServletRequest request = ctx.getRequest();`
String header = request.getHeader("YOUR_HEADER_PARAM");
if ("YOUR_A_LOGIC".equals(header) ) {
ctx.put("serviceId", "serviceA");
//ctx.setRouteHost(new URL("http://Service_A_URL”));
} else { // "YOUR_B_LOGIC"
ctx.put("serviceId", "serviceB");
//ctx.setRouteHost(new URL("http://Service_B_URL”));
}
log.info(String.format("%s request to %s", request.getMethod(),
request.getRequestURL().toString()));
return null;
}
Im not sure 100% about the redirection part, but it's a beginning for your needs.
i added second option for redirection (commented lines), maybe one of the 2 options will help you.
Also see this example

Related

Spring Security, How we can create authorization rules antmatchers dynamically from database

Is it a good practice to store authorization rules antmatchers URLs and Role in database and create dynamically on server start?
When a request comes to application does it fetch rules every time from database in Configure method of the WebSecurityConfigurerAdapter if we use create them by fetching from database.
I'm not able to find any example that how we can create new rules dynamically just from database without changing application code.
Here is the sample code that I want to create rules dynamically
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/dashboard/**").hasAuthority("VIEW_DASHBOARD")
.antMatchers("/dashboard/**").hasAuthority("EDIT_DASHBOARD")
.antMatchers("/dashboard/**").hasAuthority("DELETE_DASHBOARD")
.antMatchers("/profiling/**").hasAuthority("VIEW_PROFILING")
.antMatchers("/profiling/**").hasAuthority("EDIT_PROFILING")
.antMatchers("/profiling/**").hasAuthority("DELETE_PROFILING");
}
you can create your AccessDecisionVoter
#Configuration
public class WebSecurityConfiguration extends WebSecurityConfigurerAdapter {
#Override
protected void configure(HttpSecurity http) throws Exception {
// you can inject AccessDecisionVoter in spring container
List<AccessDecisionVoter<?>> decisionVoters = new ArrayList<>();
decisionVoters.add(new AccessDecisionVoter<FilterInvocation>() {
#Override
public boolean supports(ConfigAttribute attribute) {
return true;
}
#Override
public boolean supports(Class<?> clazz) {
return true;
}
#Override
public int vote(Authentication authentication, FilterInvocation invocation, Collection<ConfigAttribute> attributes) {
String url = invocation.getRequestUrl();
AntPathMatcher m = new AntPathMatcher();
Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();
boolean matched = false;
// you can retrieve url pattern from database here
if (m.match("/**", url) && CollectionUtils.isEmpty(authorities) == false) {
matched = authorities.stream().anyMatch(a -> {
return a.getAuthority().equals("your_authority");
});
}
if (matched) {
return AccessDecisionVoter.ACCESS_GRANTED;
} else {
return AccessDecisionVoter.ACCESS_DENIED;
}
}
});
AffirmativeBased aa = new AffirmativeBased(decisionVoters);
http.authorizeRequests().accessDecisionManager(aa);
}
}

Injecting HttpService into a Mule 4 Custom Configuration Properties Provider

I'm working on making a custom properties provider to load the contents of a Spring cloud config server at startup. I need to make a single call at the initialization of the provider to fetch these properties, and would like to use the Mule HttpService in order to make the http client for this call, instead of creating my own. Unfortunately, whenever I try this, it seems the HttpService hasn't been created yet and so throws an NPE once it's referenced.
CustomConfigurationPropertiesProviderFactory.java
public class CustomConfigurationPropertiesProviderFactory implements ConfigurationPropertiesProviderFactory {
public static final String EXTENSION_NAMESPACE = "custom-properties";
public static final String CONFIGURATION_PROPERTIES_ELEMENT = "config";
public static final ComponentIdentifier CUSTOM_CONFIGURATION_PROPERTIES =
builder().namespace(EXTENSION_NAMESPACE).name(CONFIGURATION_PROPERTIES_ELEMENT).build();
#Inject
HttpService httpService;
#Override
public ComponentIdentifier getSupportedComponentIdentifier() {
return CUSTOM_CONFIGURATION_PROPERTIES;
}
#Override
public ConfigurationPropertiesProvider createProvider(ConfigurationParameters parameters,
ResourceProvider externalResourceProvider) {
String url = parameters.getStringParameter("url");
return new CustomConfigurationPropertiesProvider(url, httpService);
}
}
CustomConfigurationPropertiesProvider.java
public class CustomConfigurationPropertiesProvider implements ConfigurationPropertiesProvider {
private final static String PREFIX = "custom::";
private Properties properties = null;
public CustomConfigurationPropertiesProvider(String url, HttpService httpService) {
HttpClientConfiguration.Builder builder = new HttpClientConfiguration.Builder();
builder.setName("customProperties");
HttpClient client = httpService.getClientFactory().create(builder.build()); //NPE here
client.start();
// proceed to create and execute request, then load into properties
}
#Override
public Optional<ConfigurationProperty> getConfigurationProperty(String configurationAttributeKey) {
if (configurationAttributeKey.startsWith(PREFIX)) {
String effectiveKey = configurationAttributeKey.substring(PREFIX.length());
if (properties != null && !properties.isEmpty()) {
return Optional.of(new ConfigurationProperty() {
#Override
public Object getSource() {...}
#Override
public Object getRawValue() { return properties.getProperty(effectiveKey); }
#Override
public String getKey() { return effectiveKey; }
});
}
}
return Optional.empty();
}
}
What do I need to change to properly inject this service?
I've been following the advice from these two bits of documentation, for reference:
https://docs.mulesoft.com/mule-runtime/4.2/custom-configuration-properties-provider
https://docs.mulesoft.com/mule-sdk/1.1/mule-service-injection

Basic Authentication service called By Zuul

I'm Zuul as edge server. so all request pass by this edge server.
I have a micro-service A. all web services of A are protected by Basic Authentication.
How can we call the services of A b passing by Zuul proxy?
Should I add header for messages?
This is my Zuul filter:
public class BasicAuthorizationHeaderFilter extends ZuulFilter {
#Override
public String filterType() {
return "pre";
}
#Override
public int filterOrder() {
return 10;
}
#Override
public boolean shouldFilter() {
return true;
}
#Override
public Object run() {
RequestContext ctx = RequestContext.getCurrentContext();
ctx.getRequest().getRequestURL();
ctx.addZuulRequestHeader("Authorization", "Basic " + Utils.getBase64Credentials("user", "Token"));
return null;
}
}
Ideally the requester would have the token in the request.
If you want to have Zuul add the authentication token then you can create a ZuulFilter and use:
context.addZuulRequestHeader("Authorization", "base64encodedTokenHere");
Doing this would give open access to the services - which may not be wise.
#Component
public class PreFilter extends ZuulFilter {
private static final Logger LOG = LoggerFactory.getLogger(PreFilter.class);
#Override
public String filterType() {
return "pre";
}
#Override
public int filterOrder() {
return 1;
}
#Override
public boolean shouldFilter() {
return true;
}
#Override
public Object run() {
RequestContext ctx = RequestContext.getCurrentContext();
HttpServletRequest request = ctx.getRequest();
ctx.addZuulRequestHeader("Authorization", request.getHeader("Authorization"));
LOG.info("Parametres : {}", request.getParameterMap()
.entrySet()
.stream()
.map(e -> e.getKey() + "=" + Stream.of(e.getValue()).collect(Collectors.toList()))
.collect(Collectors.toList()));
LOG.info("Headers : {}", "Authorization" + "=" + request.getHeader("Authorization"));
LOG.info(String.format("%s request to %s", request.getMethod(), request.getRequestURL().toString()));
return null;
}
}
You can call (through Zuul) your service A like this :
https://login:password#zuulurl.com/serviceA
but firslty allow AUTHORIZATION header through Zuul for this specific service (route) with the property sensitiveHeaders in your properties file :
zuul.routes.serviceA.sensitiveHeaders=Cookie,Set-Cookie
or let it empty if you want to pass the Cookie headers too.
Here more informations about headers through Zuul
Use zuul's sensitive header property with the blank value,
zuul.sensitiveHeaders=
Above property will do the trick but if you want to have filters for Cookie headers
you can use that property with values,
zuul.sensitiveHeaders=Cookie,Set-Cookie
This change is little tricky.
#Override
public int filterOrder() {
return 1; // change the return value to more than 5 the above code will work.
}
try with the final code below:
#Component
public class PreFilter extends ZuulFilter {
private static final Logger LOG = LoggerFactory.getLogger(PreFilter.class);
#Override
public String filterType() {
return "pre";
}
#Override
public int filterOrder() {
return 10;
}
#Override
public boolean shouldFilter() {
return true;
}
#Override
public Object run() {
RequestContext ctx = RequestContext.getCurrentContext();
HttpServletRequest request = ctx.getRequest();
ctx.addZuulRequestHeader("Authorization", request.getHeader("Authorization"));
return null;
}
}

Spring Cloud ZUUL - custom POST route filter

We are trying to write custom POST route filter for ZUUL. We are using Spring Cloud Zuul. This is what we are trying to do -
There are more than one instance of same service (ServiceA) registered with Eureka. We make a rest API request in ServiceA via ZUUL and the API is serviced by any one of the instances registered with Eureka. What we want to know is the hostip of the instance which services that particulare request.
We implemented a POST filter but the RequestContext.getCurrentContext().getRouteHost is empty; is there any other way to get the hostip??
It works for me:
#Component
public class PostFilter extends ZuulFilter {
#Override
public String filterType() {
return "post";
}
#Override
public int filterOrder() {
return 1;
}
#Override
public boolean shouldFilter() {
return true;
}
#Override
public Object run() {
RequestContext ctx = RequestContext.getCurrentContext();
ctx.getResponseBody();
((IResponse) ctx.get("ribbonResponse")).getRequestedURI();
return null;
}
}

Locales as part of the URL in Spring MVC

I'm now looking for a framework for multilingual web-applications. At the moment it seems to me that the best choice is Spring MVC. But I faced the fact that all the guidelines for developers suggests to switch languages using LocaleChangeInterceptor in such way:
http://www.somesite.com/action/?locale=en
Unfortunately, there are a number of reasons why I would like avoid this. How could I make language code to be an essential part of URL? For example:
http://www.somesite.com/en/action
Thanks.
UPD: I've found following solution. It's not complete yet, but works. Solution consists in two parts - servlet filter and locale resolver bean. It's looks little bit hackish, but I do not see other way to solve this problem.
public class LocaleFilter implements Filter
{
...
private static final String DEFAULT_LOCALE = "en";
private static final String[] AVAILABLE_LOCALES = new String[] {"en", "ru"};
public LocaleFilter() {}
private List<String> getSevletRequestParts(ServletRequest request)
{
String[] splitedParts = ((HttpServletRequest) request).getServletPath().split("/");
List<String> result = new ArrayList<String>();
for (String sp : splitedParts)
{
if (sp.trim().length() > 0)
result.add(sp);
}
return result;
}
private Locale getLocaleFromRequestParts(List<String> parts)
{
if (parts.size() > 0)
{
for (String lang : AVAILABLE_LOCALES)
{
if (lang.equals(parts.get(0)))
{
return new Locale(lang);
}
}
}
return null;
}
#Override
public void doFilter(ServletRequest request, ServletResponse response,
FilterChain chain) throws IOException, ServletException
{
List<String> requestParts = this.getSevletRequestParts(request);
Locale locale = this.getLocaleFromRequestParts(requestParts);
if (locale != null)
{
request.setAttribute(LocaleFilter.class.getName() + ".LOCALE", locale);
StringBuilder sb = new StringBuilder();
for (int i = 1; i < requestParts.size(); i++)
{
sb.append('/');
sb.append((String) requestParts.get(i));
}
RequestDispatcher dispatcher = request.getRequestDispatcher(sb.toString());
dispatcher.forward(request, response);
}
else
{
request.setAttribute(LocaleFilter.class.getName() + ".LOCALE", new Locale(DEFAULT_LOCALE));
chain.doFilter(request, response);
}
}
...
}
public class FilterLocaleResolver implements LocaleResolver
{
private Locale DEFAULT_LOCALE = new Locale("en");
#Override
public Locale resolveLocale(HttpServletRequest request)
{
Locale locale = (Locale) request.getAttribute(LocaleFilter.class.getName() + ".LOCALE");
return (locale != null ? locale : DEFAULT_LOCALE);
}
#Override
public void setLocale(HttpServletRequest request, HttpServletResponse response, Locale locale)
{
request.setAttribute(LocaleFilter.class.getName() + ".LOCALE", locale);
}
}
So there is no need to map locale in each action in controllers. The following example will work fine:
#Controller
#RequestMapping("/test")
public class TestController
{
#RequestMapping("action")
public ModelAndView action(HttpServletRequest request, HttpServletResponse response)
{
ModelAndView mav = new ModelAndView("test/action");
...
return mav;
}
}
I implemented something very similar using a combination of Filter and Interceptor.
The filter extracts the first path variable and, if it's a valid locale it sets it as a request attribute, strips it from the beginning of the requested URI and forward the request to the new URI.
public class PathVariableLocaleFilter extends OncePerRequestFilter {
private static final Logger LOG = LoggerFactory.getLogger(PathVariableLocaleFilter.class);
#Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
String url = defaultString(request.getRequestURI().substring(request.getContextPath().length()));
String[] variables = url.split("/");
if (variables.length > 1 && isLocale(variables[1])) {
LOG.debug("Found locale {}", variables[1]);
request.setAttribute(LOCALE_ATTRIBUTE_NAME, variables[1]);
String newUrl = StringUtils.removeStart(url, '/' + variables[1]);
LOG.trace("Dispatching to new url \'{}\'", newUrl);
RequestDispatcher dispatcher = request.getRequestDispatcher(newUrl);
dispatcher.forward(request, response);
} else {
filterChain.doFilter(request, response);
}
}
private boolean isLocale(String locale) {
//validate the string here against an accepted list of locales or whatever
try {
LocaleUtils.toLocale(locale);
return true;
} catch (IllegalArgumentException e) {
LOG.trace("Variable \'{}\' is not a Locale", locale);
}
return false;
}
}
The interceptor is very similar to the LocaleChangeInterceptor, it tries to get the locale from the request attribute and, if the locale is found, it sets it to the LocaleResolver.
public class LocaleAttributeChangeInterceptor extends HandlerInterceptorAdapter {
public static final String LOCALE_ATTRIBUTE_NAME = LocaleAttributeChangeInterceptor.class.getName() + ".LOCALE";
#Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
Object newLocale = request.getAttribute(LOCALE_ATTRIBUTE_NAME);
if (newLocale != null) {
LocaleResolver localeResolver = RequestContextUtils.getLocaleResolver(request);
if (localeResolver == null) {
throw new IllegalStateException("No LocaleResolver found: not in a DispatcherServlet request?");
}
localeResolver.setLocale(request, response, StringUtils.parseLocaleString(newLocale.toString()));
}
// Proceed in any case.
return true;
}
}
Once you have them in place you need to configure Spring to use the interceptor and a LocaleResolver.
#Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new LocaleAttributeChangeInterceptor());
}
#Bean(name = "localeResolver")
public LocaleResolver getLocaleResolver() {
return new CookieLocaleResolver();
}
And add the filter to the AbstractAnnotationConfigDispatcherServletInitializer.
#Override
protected Filter[] getServletFilters() {
return new Filter[] { new PathVariableLocaleFilter() };
}
I haven't tested it thoroughly but it seems working so far and you don't have to touch your controllers to accept a {locale} path variable, it should just work out of the box. Maybe in the future we'll have 'locale as path variable/subfolder' Spring automagic solution as it seems more and more websites are adopting it and according to some it's the way to go.
I found myself in the same problem and after do a lot of research I finally manage to do it also using a Filter and a LocaleResolver. A step for step guide:
First set the Filter in the web.xml:
<filter>
<filter-name>LocaleFilter</filter-name>
<filter-class>yourCompleteRouteToTheFilter.LocaleUrlFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>LocaleFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
In the LocaleUrlFilter.java we use regex to:
add two attributes (Country code and Language Code) to the request that we will capture later on the LocaleResolver:
strip the language from the url
import java.io.IOException;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
public class LocaleUrlFilter implements Filter{
private static final Pattern localePattern = Pattern.compile("^/([a-z]{2})(?:/([a-z]{2}))?(/.*)?");
public static final String COUNTRY_CODE_ATTRIBUTE_NAME = LocaleUrlFilter.class.getName() + ".country";
public static final String LANGUAGE_CODE_ATTRIBUTE_NAME = LocaleUrlFilter.class.getName() + ".language";
#Override
public void init(FilterConfig arg0) throws ServletException {}
#Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) servletRequest;
String url = request.getRequestURI().substring(request.getContextPath().length());
Matcher matcher = localePattern.matcher(url);
if (matcher.matches()) {
// Set the language attributes that we will use in LocaleResolver and strip the language from the url
request.setAttribute(COUNTRY_CODE_ATTRIBUTE_NAME, matcher.group(1));
request.setAttribute(LANGUAGE_CODE_ATTRIBUTE_NAME, matcher.group(2));
request.getRequestDispatcher(matcher.group(3) == null ? "/" : matcher.group(3)).forward(servletRequest, servletResponse);
}
else filterChain.doFilter(servletRequest, servletResponse);
}
#Override
public void destroy() {}
}
Now the filter injected to the request two attributes that we will use to form the Locale and stripped the language from url to correctly process our requests. Now we will define a LocaleResolver to change the locale. For that first we modify our servlet.xml file:
<!-- locale Resolver configuration-->
<bean id="localeResolver" class="yourCompleteRouteToTheResolver.CustomLocaleResolver"></bean>
And in the CustomLocaleResolver.java we set the language accordingly. If there is no Language in the url we proceed using the getLocale method of the request:
import java.util.Locale;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.web.servlet.LocaleResolver;
/*
* Set the Locale defined in the LocaleUrlFiltes. If none is defined (in the url) return the request locale.
*/
public class CustomLocaleResolver implements LocaleResolver{
#Override
public Locale resolveLocale(HttpServletRequest servletRequest) {
final String countryCode = (String)servletRequest.getAttribute(LocaleUrlFilter.COUNTRY_CODE_ATTRIBUTE_NAME);
if (countryCode != null) {
String languageCode = (String)servletRequest.getAttribute(LocaleUrlFilter.LANGUAGE_CODE_ATTRIBUTE_NAME);
if (languageCode == null) {
return new Locale(countryCode);
}
return new Locale(languageCode, countryCode);
}
return servletRequest.getLocale();
}
#Override
public void setLocale(final HttpServletRequest servletRequest, final HttpServletResponse servletResponse, final Locale locale) {
throw new UnsupportedOperationException();
}
}
Doing this you won't need to change anything in your controllers and visiting "/en/home" will be the same as visiting "/home" and using your language_en.properties file. Hope it helps
I came across very same problem recently. So I would like to have stateless locale not depending on session or cookie or anything else than simply URL.
I tried filter/interceptor/localeResolver solutions suggested in previous answers however these did not suite my needs as I had:
static content (images etc ..)
parts of page not locale dependent (admin panel)
RestController inside same app
multipart file uploader
I also wanted to avoid duplicated content for SEO reasons (In particular I do not want my english content to be accessible from both paths: /landingPage and /en/landingPage).
The solution that worked best for me was to create LanguageAwareController and then inherit from it in all controllers that I wanted to support multiple locales.
#Controller
#RequestMapping(path = "/{lang}")
public class LanguageAwareController {
#Autowired
LocaleResolver localeResolver;
#ModelAttribute(name = "locale")
Locale getLocale(#PathVariable(name = "lang") String lang, HttpServletRequest request,
HttpServletResponse response){
Locale effectiveLocale = Arrays.stream(Locale.getAvailableLocales())
.filter(locale -> locale.getLanguage().equals(lang))
.findFirst()
.orElseGet(Locale::getDefault);
localeResolver.setLocale(request, response, effectiveLocale);
return effectiveLocale;
}
}
Usage in one of controllers:
#Controller
public class LandingPageController extends LanguageAwareController{
private Log log = LogFactory.getLog(LandingPageController.class);
#GetMapping("/")
public String welcomePage(Locale locale, #PathVariable(name = "lang") String lang ){
log.info(lang);
log.info(locale);
return "landing";
}
}
In spring 3.0 you can tell your controllers to look for path variables. e.g.
#RequestMapping("/{locale}/action")
public void action(#PathVariable String locale) {
...
}
In addition to the provided answers here's a way how to let Thymeleaf prepend the locale in path after context path automatically by implementing a ILinkBuilder:
#Bean
public ILinkBuilder pathVariableLocaleLinkBuilder() {
PathVariableLocaleLinkBuilder pathVariableLocaleLinkBuilder = new PathVariableLocaleLinkBuilder();
pathVariableLocaleLinkBuilder.setOrder(1);
return pathVariableLocaleLinkBuilder;
}
#Bean
SpringTemplateEngine templateEngine(ThymeleafProperties properties, ObjectProvider<ITemplateResolver> templateResolvers, ObjectProvider<IDialect> dialects, ObjectProvider<ILinkBuilder> linkBuilders) {
SpringTemplateEngine engine = new SpringTemplateEngine();
engine.setEnableSpringELCompiler(properties.isEnableSpringElCompiler());
engine.setRenderHiddenMarkersBeforeCheckboxes(properties.isRenderHiddenMarkersBeforeCheckboxes());
templateResolvers.orderedStream().forEach(engine::addTemplateResolver);
dialects.orderedStream().forEach(engine::addDialect);
linkBuilders.orderedStream().forEach(engine::addLinkBuilder);
return engine;
}
And here's the LinkBuilder itself:
public class PathVariableLocaleLinkBuilder extends AbstractLinkBuilder {
#Autowired
private LocaleResolver localeResolver;
#Override
public String buildLink(IExpressionContext context, String base, Map<String, Object> parameters) {
Validate.notNull(context, "Expression context cannot be null");
if (base == null) {
return null;
}
if (!isLinkBaseContextRelative(base)) {
return base;
}
if (!(context instanceof IWebContext)) {
throw new TemplateProcessingException(
"Link base \"" + base + "\" cannot be context relative (/...) unless the context " +
"used for executing the engine implements the " + IWebContext.class.getName() + " interface");
}
final HttpServletRequest request = ((IWebContext) context).getRequest();
return "/" + localeResolver.resolveLocale(request) + base;
}
private static boolean isLinkBaseContextRelative(final CharSequence linkBase) {
if (linkBase.length() == 0 || linkBase.charAt(0) != '/') {
return false;
}
return linkBase.length() == 1 || linkBase.charAt(1) != '/';
}
}

Resources