Related
I'm currently migrating from Spring Security SAML Extension to Spring Security SAML2 and use case requires language code to be sent in Extensions -element.
With Spring Security SAML Extension this was done by:
Extending SAMLEntryPoint and storing locale as relayState to SAMLMessageContext like this:
public class CustomSAMLEntryPoint extends SAMLEntryPoint {
private String relayState;
#Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authenticationException) throws IOException, ServletException {
//read your request parameter
setRelayState(request.getParameter("locale"));
super.commence(request, response, authenticationException);
}
#Override
protected WebSSOProfileOptions getProfileOptions(SAMLMessageContext samlMessageContext, AuthenticationException authenticationException) throws MetadataProviderException {
//set the relayState to your SAML message context
samlMessageContext.setRelayState(getRelayState());
return super.getProfileOptions(samlMessageContext, authenticationException);
}
private void setRelayState(String relayState) {
this.relayState = relayState;
}
private String getRelayState() {
return relayState;
}
}
Extending WebSSOProfileImpl and using previously set relayState value to generate Extensions -element:
public class CustomWebSSOProfileImpl extends WebSSOProfileImpl {
#Override
protected AuthnRequest getAuthnRequest(SAMLMessageContext context, WebSSOProfileOptions options, AssertionConsumerService assertionConsumer, SingleSignOnService bindingService) throws SAMLException, MetadataProviderException {
AuthnRequest authnRequest = super.getAuthnRequest(context, options, assertionConsumer, bindingService);
authnRequest.setExtensions(buildExtensions(context.getRelayState()));
return authnRequest;
}
}
How could this same functionality be done with Spring Security Core SAML2? Is there some similar way than using SAMLMessageContext and relayState?
I could customize AuthenticationEntryPoint as well as authentication request creation but there seems to be no way to move locale between these two.
public AuthenticationEntryPoint authenticationEntryPoint() {
final AuthenticationEntryPoint authenticationEntryPoint = new LoginUrlAuthenticationEntryPoint(
"/saml2/authenticate/sp");
return (request, response, exception) -> {
String locale = request.getParameter("locale");
// Where shoud locale be stored???
authenticationEntryPoint.commence(request, response, exception);
};
}
#Bean
public Saml2AuthenticationRequestFactory authenticationRequestFactory() {
final OpenSamlAuthenticationRequestFactory authenticationRequestFactory = new OpenSamlAuthenticationRequestFactory();
authenticationRequestFactory.setAuthenticationRequestContextConverter(context -> {
final AuthnRequest request = new AuthnRequestBuilder().buildObject();
request.setAssertionConsumerServiceURL(context.getAssertionConsumerServiceUrl());
request.setDestination(context.getDestination());
request.setID("A" + UUID.randomUUID());
request.setIssueInstant(new DateTime());
final Issuer issuer = new IssuerBuilder().buildObject();
issuer.setValue(context.getIssuer());
request.setIssuer(issuer);
// Where can locale be read from???
request.setExtensions(buildLanguageExtensions(???);
return request;
});
return authenticationRequestFactory;
}
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;
}
}
I'm developing a web application, based on Spring-Boot - 1.1.6, Spring -Security -3.2.5 and more.
I'm using Java based configuration:
#Configuration
#EnableWebMvcSecurity
public class SecurityCtxConfig extends WebSecurityConfigurerAdapter {
#Bean
DelegatingAuthenticationEntryPoint delegatingAuthenticationEntryPoint() {
LinkedHashMap<RequestMatcher, AuthenticationEntryPoint> map = new LinkedHashMap<RequestMatcher, AuthenticationEntryPoint>();
Http403ForbiddenEntryPoint defaultEntryPoint = new Http403ForbiddenEntryPoint();
map.put(AnyRequestMatcher.INSTANCE, defaultEntryPoint);
DelegatingAuthenticationEntryPoint retVal = new DelegatingAuthenticationEntryPoint(map);
retVal.setDefaultEntryPoint(defaultEntryPoint);
return retVal;
}
#Override
protected void configure(HttpSecurity http) throws Exception {
ExceptionHandlingConfigurer<HttpSecurity> exceptionHandling = http.exceptionHandling();
exceptionHandling.authenticationEntryPoint(delegatingAuthenticationEntryPoint());
http.logout().logoutSuccessHandler(new LogoutSuccessHandler() {
#Override
public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication arg2)
throws IOException, ServletException {
response.setStatus(HttpServletResponse.SC_OK);
}
});
}
}
The requirement is to return Http status 401 in case that the session cookie is invalid or missing(no matter the reason)
I see the InvalidSessionStrategy but I don't find a way to set it on the SessionManagementFilter.
Can some one please instract me how to implement my plan or another one that will fulfill the requirement
Using SpringBoot this works for me:
#Configuration
#EnableWebSecurity
public class UISecurityConfig extends WebSecurityConfigurerAdapter {
#Override
protected void configure(HttpSecurity http) throws Exception {
...
http.addFilterAfter(expiredSessionFilter(), SessionManagementFilter.class);
...
}
private Filter expiredSessionFilter() {
SessionManagementFilter smf = new SessionManagementFilter(new HttpSessionSecurityContextRepository());
smf.setInvalidSessionStrategy((request, response) -> response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Session go BOOM!"));
return smf;
}
}
We had the exact same problem and I did this hack to solve it (yes I know, this is a hack, therefore the name...).
I create a BeanPostProcessor and search for the SessionManagementFilter to reconfigure it...
#Bean
public HackyBeanPostProcessor myBeanPostProcessor() {
return new HackyBeanPostProcessor();
}
protected static class HackyBeanPostProcessor implements BeanPostProcessor {
#Override
public Object postProcessBeforeInitialization(Object bean, String beanName) {
// FIXME check if a new spring-security version allows this in an
// other way (current: 3.2.5.RELEASE)
if (bean instanceof SessionManagementFilter) {
SessionManagementFilter filter = (SessionManagementFilter) bean;
filter.setInvalidSessionStrategy(new InvalidSessionStrategy() {
#Override
public void onInvalidSessionDetected(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException {
response.sendError(HttpServletResponse.SC_UNAUTHORIZED);
}
});
}
return bean;
}
#Override
public Object postProcessAfterInitialization(Object bean, String beanName) {
return bean;
}
}
Since I'm using AspectJ (I mean, compile time weaving and not Spring AOP), it was quite easy to hack the SessionManagementFilter creation by setting my custom InvalidSessionStrategy after the SessionManagementFilter is constructed:
#Aspect
public class SessionManagementAspect {
private static final Log logger = LogFactory.getLog();
#AfterReturning("execution( org.springframework.security.web.session.SessionManagementFilter.new(..))&&this(smf)")
public void creation(JoinPoint pjp, SessionManagementFilter smf) throws Throwable {
logger.debug("Adding/Replacing the invalid session detection policy to return 401 in case of an invalid session");
smf.setInvalidSessionStrategy(new InvalidSessionStrategy() {
#Override
public void onInvalidSessionDetected(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException {
logInvalidSession(request, "invalid cookie");
if (!response.isCommitted())
response.sendError(HttpStatus.UNAUTHORIZED.value());
}
});
}
}
If you are not using AspectJ, try adding #Component and add this Aspect to your context, it might work if the SessionManagementFilter is a bean (Since Spring-AOP applias only on spring beans)
I am working on an application where I would like to include dynamic XHTML content from a stream. To handle this I wrote a taghandler extension which dumps the dynamic XHTML content to output component as
UIOutput htmlChild = (UIOutput) ctx.getFacesContext().getApplication().createComponent(UIOutput.COMPONENT_TYPE);
htmlChild.setValue(new String(outputStream.toByteArray(), "utf-8"));
This works fine for XHTML content which has no JSF tags. If I have JSF tags in my dynamic XHTML content like <h:inputText value="#{bean.item}"/>, then they're printed as plain text. I want them to render as input fields. How can I achieve this?
Essentially, you should be using an <ui:include> in combination with a custom ResourceHandler which is able to return the resource in flavor of an URL. So when having an OutputStream, you should really be writing it to a (temp) file so that you can get an URL out of it.
E.g.
<ui:include src="/dynamic.xhtml" />
with
public class DynamicResourceHandler extends ResourceHandlerWrapper {
private ResourceHandler wrapped;
public DynamicResourceHandler(ResourceHandler wrapped) {
this.wrapped = wrapped;
}
#Override
public ViewResource createViewResource(FacesContext context, String resourceName) {
if (resourceName.equals("/dynamic.xhtml")) {
try {
File file = File.createTempFile("dynamic-", ".xhtml");
try (Writer writer = new FileWriter(file)) {
writer
.append("<ui:composition")
.append(" xmlns:ui='http://java.sun.com/jsf/facelets'")
.append(" xmlns:h='http://java.sun.com/jsf/html'")
.append(">")
.append("<p>Hello from a dynamic include!</p>")
.append("<p>The below should render as a real input field:</p>")
.append("<p><h:inputText /></p>")
.append("</ui:composition>");
}
final URL url = file.toURI().toURL();
return new ViewResource(){
#Override
public URL getURL() {
return url;
}
};
}
catch (IOException e) {
throw new FacesException(e);
}
}
return super.createViewResource(context, resourceName);
}
#Override
public ResourceHandler getWrapped() {
return wrapped;
}
}
(warning: basic kickoff example! this creates a new temp file on every request, a reuse/cache system should be invented on your own)
which is registered in faces-config.xml as follows
<application>
<resource-handler>com.example.DynamicResourceHandler</resource-handler>
</application>
Note: all of above is JSF 2.2 targeted. For JSF 2.0/2.1 users stumbling upon this answer, you should use ResourceResolver instead for which an example is available in this answer: Obtaining Facelets templates/files from an external filesystem or database. Important note: ResourceResolver is deprecated in JSF 2.2 in favor of ResourceHandler#createViewResource().
My solution for JSF 2.2 and custom URLStream Handler
public class DatabaseResourceHandlerWrapper extends ResourceHandlerWrapper {
private ResourceHandler wrapped;
#Inject
UserSessionBean userBeean;
public DatabaseResourceHandlerWrapper(ResourceHandler wrapped) {
this.wrapped = wrapped;
}
#Override
public Resource createResource(String resourceName, String libraryName) {
return super.createResource(resourceName, libraryName); //To change body of generated methods, choose Tools | Templates.
}
#Override
public ViewResource createViewResource(FacesContext context, String resourceName) {
if (resourceName.startsWith("/dynamic.xhtml?")) {
try {
String query = resourceName.substring("/dynamic.xhtml?".length());
Map<String, String> params = splitQuery(query);
//do some query to get content
String content = "<ui:composition"
+ " xmlns='http://www.w3.org/1999/xhtml' xmlns:ui='http://java.sun.com/jsf/facelets'"
+ " xmlns:h='http://java.sun.com/jsf/html'> MY CONTENT"
+ "</ui:composition>";
final URL url = new URL(null, "string://helloworld", new MyCustomHandler(content));
return new ViewResource() {
#Override
public URL getURL() {
return url;
}
};
} catch (IOException e) {
throw new FacesException(e);
}
}
return super.createViewResource(context, resourceName);
}
public static Map<String, String> splitQuery(String query) throws UnsupportedEncodingException {
Map<String, String> params = new LinkedHashMap<>();
String[] pairs = query.split("&");
for (String pair : pairs) {
int idx = pair.indexOf("=");
params.put(URLDecoder.decode(pair.substring(0, idx), "UTF-8"), URLDecoder.decode(pair.substring(idx + 1), "UTF-8"));
}
return params;
}
#Override
public ResourceHandler getWrapped() {
return wrapped;
}
static class MyCustomHandler extends URLStreamHandler {
private String content;
public MyCustomHandler(String content) {
this.content = content;
}
#Override
protected URLConnection openConnection(URL u) throws IOException {
return new UserURLConnection(u, content);
}
private static class UserURLConnection extends URLConnection {
private String content;
public UserURLConnection(URL url, String content) {
super(url);
this.content = content;
}
#Override
public void connect() throws IOException {
}
#Override
public InputStream getInputStream() throws IOException {
return new ByteArrayInputStream(content.getBytes("UTF-8"));
}
}
}
}
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) != '/';
}
}