Reputation: 853
I'm working on web application that uses Spring (MVC), Hibernate, Spring Security and ZK as frontend. I'm using latest releases of all libraries (3.1.2 Spring, 3.1.3 Spring Security, 4.1.7 Hibernate) and I have a problem with internationalization (i18n). I'll get into the details after configuration (relevant parts only):
web.xml:
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>/WEB-INF/spring/root-context.xml /WEB-INF/spring/spring-security.xml</param-value>
</context-param>
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
<servlet>
<servlet-name>appServlet</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>
/WEB-INF/spring/appServlet/servlet-context.xml
</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>appServlet</servlet-name>
<url-pattern>/</url-pattern>
</servlet-mapping>
<servlet>
<servlet-name>zkLoader</servlet-name>
<servlet-class>org.zkoss.zk.ui.http.DHtmlLayoutServlet</servlet-class>
<init-param>
<param-name>update-uri</param-name>
<param-value>/zkau</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet>
<servlet-name>auEngine</servlet-name>
<servlet-class>org.zkoss.zk.au.http.DHtmlUpdateServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>zkLoader</servlet-name>
<url-pattern>*.zul</url-pattern>
</servlet-mapping>
<servlet-mapping>
<servlet-name>auEngine</servlet-name>
<url-pattern>/zkau/*</url-pattern>
</servlet-mapping>
<listener>
<description>ZK JSP Tags environment initiation </description>
<display-name>ZK JSP Initiator</display-name>
<listener-class>org.zkoss.jsp.spec.JspFactoryContextListener</listener-class>
</listener>
<filter>
<filter-name>springSecurityFilterChain</filter-name>
<filter-class> org.springframework.web.filter.DelegatingFilterProxy</filter-class>
</filter>
<filter-mapping>
<filter-name>springSecurityFilterChain</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
servlet-context:
<beans:bean id="localeResolver" class="org.springframework.web.servlet.i18n.SessionLocaleResolver">
<beans:property name="defaultLocale" value="hr" />
</beans:bean>
<interceptors>
<beans:bean id="localeChangeInterceptor" class="org.springframework.web.servlet.i18n.LocaleChangeInterceptor">
<beans:property name="paramName" value="lang" />
</beans:bean>
</interceptors>
<beans:bean id="messageSource" class="org.springframework.context.support.ReloadableResourceBundleMessageSource">
<beans:property name="basename" value="classpath:message" />
</beans:bean>
spring-security.xml:
<http pattern="/resources/**" security="none" />
<http auto-config="true">
<intercept-url pattern="/login*" access="IS_AUTHENTICATED_ANONYMOUSLY" />
<intercept-url pattern="/**" access="ROLE_USER" />
<form-login login-page="/login" default-target-url="/" authentication-failure-url="/loginfailed" />
</http>
root-context.xml:
Nothing relevant to the problem here, just datasource definition, sessionfactory and bean declarations.
Now on to the problem:
I have 2 files: message_en.properties and message_hr.properties that both reside in src/main/resources directory. I have created this project using "Spring template project" and then chose "Spring MVC project" (Using STS 2.9.2). I have read about how to customize Spring Security messages and those that need to be overridden are put in message.properties files with custom message attached. The one I'm using for testing is:
Original message in spring-security-core.jar AbstractUserDetailsAuthenticationProvider.badCredentials = Bad credentials
Overridden in message_en.properties:
AbstractUserDetailsAuthenticationProvider.badCredentials = Invalid user name or password
Overridden in message_hr.properties:
AbstractUserDetailsAuthenticationProvider.badCredentials = Bla Bla Bla
Scenario 1:
If I leave everything as described above in the configuration files, changing lang parameter in url bar or just by clicking on the links I have in the login page, properly reads all the messages from my custom message_xx.properties except the SS one's. So instead of giving me "Invalid user name or password" or "Bla Bla Bla" I get "Bad credentials".
Scenario 2:
If I move messageSource bean from servlet-context.xml to the spring-security.xml it loads proper error message but no matter what locale is set it always reads "Bla Bla Bla". This happens even if I change lang param of localeChangeInterceptor bean.
What am I suppose to do here to make this work properly?
Forgot to mention: In order to get Spring Security message I'm using this in jsp page
${sessionScope["SPRING_SECURITY_LAST_EXCEPTION"].message}
Upvotes: 4
Views: 5505
Reputation: 616
Based on A.Masson suggestion, I made a change to support any type of LocalResolver implementation.
/**
* Transfert the value of user Locale into LocaleContextHolder which is used by spring-security
*/
public class FilterI18nSpringSecurity implements Filter {
private static final Logger LOG = LoggerFactory.getLogger(FilterI18nSpringSecurity.class);
private WebApplicationContext springContext;
/**
* @see javax.servlet.Filter#doFilter(javax.servlet.ServletRequest,
* javax.servlet.ServletResponse, javax.servlet.FilterChain)
*/
public void doFilter(ServletRequest pRequest, ServletResponse pResponse, FilterChain pFilterChain) throws IOException, ServletException {
if (!(pRequest instanceof HttpServletRequest)) {
pFilterChain.doFilter(pRequest, pResponse);
return;
}
LocaleResolver bean = springContext.getBean(LocaleResolver.class);
Locale locale = bean.resolveLocale((HttpServletRequest) pRequest);
LOG.info("Locale -> " + locale);
LocaleContextHolder.setLocale(locale, true);
pFilterChain.doFilter(pRequest, pResponse);
}
/**
* @see javax.servlet.Filter#init(javax.servlet.FilterConfig)
*/
public void init(FilterConfig filterConfig) throws ServletException {
springContext = WebApplicationContextUtils.getWebApplicationContext(filterConfig.getServletContext());
}
/**
* @see javax.servlet.Filter#destroy()
*/
public void destroy() {
}
}
EDIT: Previous approach didn't works in some circumstances. I changed to this one, getting better behavior. I used the Spring exception class name as a message key, so then I can map these messages in my current property file. (my template engine is thymeleaf)
In the page:
<th:block th:if="${session.SPRING_SECURITY_LAST_EXCEPTION != null && param.error != null}">
<div th:replace="fragments/alert :: alert (type='danger', message=#{${session.SPRING_SECURITY_LAST_EXCEPTION.class.name}})">Alert</div>
</th:block>
messages.properties:
org.springframework.security.authentication.BadCredentialsException=Usuario o contraseña...
org.springframework.security.authentication.LockedException=Usuario bloqueado
org.springframework.security.authentication.DisabledException=El usuario está deshabilitado
Upvotes: 0
Reputation: 2477
Got the same problem. After reading this from Spring Security i18n I created a Filter which:
This filter must appear before the filter-mapping of springSecurityFilterChain whithin web.xml. That way when spring-security calls LocaleContextHolder.getLocale() it retrieves the right one. In your case you can retrieve the locale from the session since you are using org.springframework.web.servlet.i18n.SessionLocaleResolver.
In spring context xml file (you can still use SessionLocaleResolver instead):
<!-- Saves a locale change using a cookie -->
<bean id="localeResolver" class="org.springframework.web.servlet.i18n.CookieLocaleResolver" />
Java Filter code (important line is LocaleContextHolder.setLocale because spring-security makes use of LocaleContextHolder.getLocale):
package com.xyz;
import java.io.IOException;
import java.util.Locale;
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.Cookie;
import javax.servlet.http.HttpServletRequest;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.i18n.LocaleContextHolder;
import org.springframework.web.servlet.i18n.CookieLocaleResolver;
import org.springframework.web.util.WebUtils;
/**
* Transfert the value of user Locale into LocaleContextHolder which is used by spring-security
*/
public class FilterI18nCookie implements Filter {
private static final Logger LOG = LoggerFactory.getLogger(FiltreI18nCookie.class);
/**
* @see javax.servlet.Filter#doFilter(javax.servlet.ServletRequest,
* javax.servlet.ServletResponse, javax.servlet.FilterChain)
*/
public void doFilter(ServletRequest pRequest, ServletResponse pResponse, FilterChain pFilterChain) throws IOException, ServletException {
if (!(pRequest instanceof HttpServletRequest)) {
pFilterChain.doFilter(pRequest, pResponse);
return;
}
HttpServletRequest request = (HttpServletRequest) pRequest;
Cookie cookie = WebUtils.getCookie(request, CookieLocaleResolver.LOCALE_REQUEST_ATTRIBUTE_NAME);
if (cookie != null) {
Locale locale = org.springframework.util.StringUtils.parseLocaleString(cookie.getValue());
if (locale != null) {
LOG.info("Locale cookie: [" + cookie.getValue() + "] == '" + locale + "'");
request.setAttribute(CookieLocaleResolver.LOCALE_REQUEST_ATTRIBUTE_NAME, locale);
LocaleContextHolder.setLocale(locale, true);
}
}
pFilterChain.doFilter(pRequest, pResponse);
}
/**
* @see javax.servlet.Filter#init(javax.servlet.FilterConfig)
*/
public void init(FilterConfig filterConfig) throws ServletException {
}
/**
* @see javax.servlet.Filter#destroy()
*/
public void destroy() {
}
}
Inside the web.xml file (the filter-mapping definition of custom filter FilterI18nCookie appreas before the springSecurityFilterChain):
<filter>
<filter-name>FilterI18nCookie</filter-name>
<filter-class>com.xyz.FilterI18nCookie</filter-class>
</filter>
<filter>
<filter-name>springSecurityFilterChain</filter-name>
<filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
</filter>
<servlet>
<servlet-name>mvc-dispatcher</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<load-on-startup>2</load-on-startup>
</servlet>
<filter-mapping>
<filter-name>FilterI18nCookie</filter-name>
<url-pattern>/ *</url-pattern>
</filter-mapping>
<filter-mapping>
<filter-name>springSecurityFilterChain</filter-name>
<url-pattern>/ *</url-pattern>
</filter-mapping>
<servlet-mapping>
<servlet-name>mvc-dispatcher</servlet-name>
<url-pattern>/</url-pattern>
</servlet-mapping>
Upvotes: 4