Spring Boot中在DispatcherServlet上下文外获取Locale的方法
问题原因
Spring的DispatcherServlet会在处理请求时,通过配置的LocaleResolver(比如CookieLocaleResolver)解析请求中的Locale,并将其存入LocaleContextHolder(基于ThreadLocal)。但Spring Security的认证流程(包括UsernamePasswordAuthenticationFilter和AuthenticationFailureHandler)执行时机早于DispatcherServlet的Locale解析逻辑,导致此时LocaleContextHolder中只有浏览器默认的Locale,而非Cookie中存储的用户自定义Locale。
最优解决方案:提前通过过滤器设置Locale上下文
最通用的解决方式是编写一个自定义过滤器,在认证过滤器执行前就解析Locale并设置到LocaleContextHolder中,这样整个认证流程的所有组件都能通过LocaleContextHolder.getLocale()获取正确的Locale。
1. 自定义Locale上下文过滤器
public class PreAuthenticationLocaleFilter extends OncePerRequestFilter { private final LocaleResolver localeResolver; public PreAuthenticationLocaleFilter(LocaleResolver localeResolver) { this.localeResolver = localeResolver; } @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { // 从请求中解析用户自定义Locale Locale userLocale = localeResolver.resolveLocale(request); // 设置到LocaleContextHolder LocaleContextHolder.setLocale(userLocale); try { // 继续执行后续过滤器链 filterChain.doFilter(request, response); } finally { // 清理ThreadLocal,避免内存泄漏 LocaleContextHolder.resetLocaleContext(); } } }
2. 配置过滤器顺序
在Spring Security配置中,将这个自定义过滤器放在UsernamePasswordAuthenticationFilter之前执行:
@Configuration @EnableWebSecurity public class SecurityConfig { private final LocaleResolver localeResolver; public SecurityConfig(LocaleResolver localeResolver) { this.localeResolver = localeResolver; } @Bean public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { http // 注入自定义过滤器,确保在认证过滤器前执行 .addFilterBefore(new PreAuthenticationLocaleFilter(localeResolver), UsernamePasswordAuthenticationFilter.class) .formLogin(form -> form .failureHandler(customAuthenticationFailureHandler()) ); return http.build(); } @Bean public CustomAuthenticationFailureHandler customAuthenticationFailureHandler(MessageSource messageSource) { return new CustomAuthenticationFailureHandler(messageSource); } }
3. 修改认证失败处理器
此时无需再注入LocaleResolver,直接使用LocaleContextHolder.getLocale()即可拿到正确的Locale:
public class CustomAuthenticationFailureHandler extends SimpleUrlAuthenticationFailureHandler { private final MessageSource messageSource; public CustomAuthenticationFailureHandler(MessageSource messageSource) { this.messageSource = messageSource; } @Override public void onAuthenticationFailure( HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException { Locale locale = LocaleContextHolder.getLocale(); String errorMessage = messageSource.getMessage("login.error", null, locale); // 后续业务逻辑... super.onAuthenticationFailure(request, response, exception); } }
备选方案:直接在处理器中注入LocaleResolver
如果不想新增过滤器,可以直接在认证失败处理器中注入LocaleResolver,手动解析Locale。这种方式适合仅在单个组件中需要Locale的场景:
public class CustomAuthenticationFailureHandler extends SimpleUrlAuthenticationFailureHandler { private final MessageSource messageSource; private final LocaleResolver localeResolver; public CustomAuthenticationFailureHandler(MessageSource messageSource, LocaleResolver localeResolver) { this.messageSource = messageSource; this.localeResolver = localeResolver; } @Override public void onAuthenticationFailure( HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException { Locale locale = localeResolver.resolveLocale(request); String errorMessage = messageSource.getMessage("login.error", null, locale); // 后续业务逻辑... super.onAuthenticationFailure(request, response, exception); } }
方案对比
- 自定义过滤器方案:一次配置,整个认证流程通用,代码更简洁一致,符合Spring上下文管理的设计思路,推荐使用。
- 直接注入LocaleResolver方案:实现简单,但仅适用于单个组件,若其他认证相关组件也需要Locale,会产生重复代码。
内容的提问来源于stack exchange,提问作者Slevin

