ExceptionTranslationFilter
定义与作用
ExceptionTranslationFilter 是 Spring Security 过滤器链中的异常转译层,位于授权过滤器(AuthorizationFilter / FilterSecurityInterceptor)之前。它的核心职责是捕获下游过滤器抛出的安全异常,并将之转译为 HTTP 响应:
AuthenticationException(未认证) → 调用AuthenticationEntryPoint,通常返回 401 或重定向到登录页。AccessDeniedException(无权限) → 调用AccessDeniedHandler,通常返回 403。
核心原理
1. 异常捕获机制
ExceptionTranslationFilter 本身并不做认证或授权判断,而是将下游过滤器(主要是 FilterSecurityInterceptor / AuthorizationFilter)包裹在 try-catch 块中,拦截两类异常:
// ExceptionTranslationFilter 核心逻辑(简化)
public void doFilter(HttpServletRequest request,
HttpServletResponse response,
FilterChain chain) {
try {
chain.doFilter(request, response); // 执行后续过滤器
} catch (AuthenticationException ex) {
// 当前请求未认证,触发登录入口
handleAuthenticationException(request, response, ex);
} catch (AccessDeniedException ex) {
// 当前请求已认证但无权限
if (isAnonymous() || isRememberMe()) {
// 匿名/RememberMe 用户视为未认证,先要求登录
handleAuthenticationException(request, response,
new InsufficientAuthenticationException("..."));
} else {
handleAccessDeniedException(request, response, ex);
}
}
}
2. 请求缓存与恢复
当未认证用户访问受保护资源时,ExceptionTranslationFilter 在调用 AuthenticationEntryPoint 之前,会先将当前请求保存到 RequestCache(默认 HttpSessionRequestCache)。用户登录成功后,RequestCacheAwareFilter 从缓存中恢复该请求,实现**"登录后回到原页面"**的体验。
完整示例一:自定义 401/403 响应(前后端分离)
场景说明
前后端分离架构中,前端通过 AJAX 请求 API。未认证时应返回 401 JSON(而非重定向到登录页),无权限时应返回 403 JSON。需要自定义 AuthenticationEntryPoint 和 AccessDeniedHandler。
操作前配置(默认行为,不适合 API)
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/admin/**").hasRole("ADMIN")
.anyRequest().authenticated()
)
.formLogin(Customizer.withDefaults()); // 默认:未认证时 302 到 /login
return http.build();
}
问题分析:前端 AJAX 收到 302 重定向后,会跟随重定向到 HTML 登录页,导致 API 响应变成 HTML 字符串,前端无法解析,用户体验极差。
操作后配置(自定义 JSON 响应)
@Configuration
@EnableWebSecurity
public class ApiExceptionConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/public/**").permitAll()
.requestMatchers("/api/admin/**").hasRole("ADMIN")
.anyRequest().authenticated()
)
.exceptionHandling(exceptions -> exceptions
// 未认证 → 返回 401 JSON
.authenticationEntryPoint(new AuthenticationEntryPoint() {
@Override
public void commence(HttpServletRequest request,
HttpServletResponse response,
AuthenticationException authException)
throws IOException {
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
response.setContentType("application/json;charset=UTF-8");
response.getWriter().write(
"{\"code\":401,\"message\":\"未认证,请先登录\"}"
);
}
})
// 无权限 → 返回 403 JSON
.accessDeniedHandler(new AccessDeniedHandler() {
@Override
public void handle(HttpServletRequest request,
HttpServletResponse response,
AccessDeniedException accessDeniedException)
throws IOException {
response.setStatus(HttpServletResponse.SC_FORBIDDEN);
response.setContentType("application/json;charset=UTF-8");
response.getWriter().write(
"{\"code\":403,\"message\":\"权限不足,无法访问该资源\"}"
);
}
})
)
.formLogin(Customizer.withDefaults()); // Web 页面仍用表单
return http.build();
}
}
结果分析
| 场景 | 请求 | 响应 |
|---|---|---|
未认证访问 /api/user/info | GET /api/user/info | 401 {code:401, message:"未认证,请先登录"} |
普通用户访问 /api/admin/config | GET /api/admin/config | 403 {code:403, message:"权限不足,无法访问该资源"} |
匿名访问 /api/public/notice | GET /api/public/notice | 200 正常返回 |
未认证访问 /web/page | GET /web/page | 302 重定向到 /login(表单登录仍生效) |
完整示例二:匿名用户访问受保护资源的行为
场景说明
AnonymousAuthenticationFilter 在链中为未登录用户填充了 AnonymousAuthenticationToken,此时用户并非 "null",而是具有 ROLE_ANONYMOUS 角色。ExceptionTranslationFilter 对匿名用户的 AccessDeniedException 处理有特殊逻辑:匿名用户遇到权限不足时,被视为未认证,触发 AuthenticationEntryPoint(401/登录),而不是直接返回 403。
操作前配置(匿名用户被直接 403)
// 错误理解:以为匿名用户会被 AuthenticationEntryPoint 处理
// 实际上,如果 AnonymousAuthenticationFilter 在 ExceptionTranslationFilter 之后被错误配置...
// 默认顺序下,匿名用户确实先经过 ExceptionTranslationFilter 再到达授权层
实际上默认顺序正确,关键在于理解以下行为差异:
操作后配置(验证匿名用户的处理路径)
@Configuration
@EnableWebSecurity
public class AnonymousExceptionConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(auth -> auth
.requestMatchers("/public/**").permitAll()
.requestMatchers("/member/**").hasAnyRole("USER", "ADMIN")
.anyRequest().authenticated()
)
.exceptionHandling(exceptions -> exceptions
.authenticationEntryPoint((request, response, authException) -> {
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
response.setContentType("application/json;charset=UTF-8");
response.getWriter().write(
"{\"code\":401,\"message\":\"需要登录\"}"
);
})
.accessDeniedHandler((request, response, accessDeniedException) -> {
response.setStatus(HttpServletResponse.SC_FORBIDDEN);
response.setContentType("application/json;charset=UTF-8");
response.getWriter().write(
"{\"code\":403,\"message\":\"权限不足\"}"
);
})
)
.formLogin(Customizer.withDefaults());
return http.build();
}
// 配置内存用户用于测试
@Bean
public UserDetailsService userDetailsService() {
InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
manager.createUser(User.withDefaultPasswordEncoder()
.username("user")
.password("password")
.roles("USER")
.build());
return manager;
}
}
测试验证与结果分析
| 请求 | 用户身份 | 响应 | 原因 |
|---|---|---|---|
GET /member/profile | 未登录(匿名) | 401 "需要登录" | 匿名用户被 ExceptionTranslationFilter 识别为未认证,走 AuthenticationEntryPoint |
GET /member/profile | 已登录但无 ROLE_USER | 403 "权限不足" | 已认证用户无权限,走 AccessDeniedHandler |
GET /member/profile | 已登录 ROLE_USER | 200 正常访问 | 通过授权检查 |
GET /admin/super | 已登录 ROLE_USER | 403 "权限不足" | 已认证用户无 ROLE_ADMIN,走 AccessDeniedHandler |
易错场景:异常处理器配置位置错误导致不生效
错误配置
@Bean
public SecurityFilterChain apiSecurity(HttpSecurity http) throws Exception {
http
.securityMatcher("/api/**")
.authorizeHttpRequests(auth -> auth
.anyRequest().authenticated()
)
.httpBasic(Customizer.withDefaults())
.exceptionHandling(exceptions -> exceptions
.authenticationEntryPoint(new CustomEntryPoint())
);
return http.build();
}
@Bean
public SecurityFilterChain webSecurity(HttpSecurity http) throws Exception {
http
.securityMatcher("/web/**")
.authorizeHttpRequests(auth -> auth
.anyRequest().authenticated()
)
.formLogin(Customizer.withDefaults());
// 注意:webSecurity 没有配置 exceptionHandling!
return http.build();
}
陷阱:当用户通过 /web/** 的表单登录链访问时,如果后续请求切换到 /api/**,未认证时的异常响应行为取决于哪个链生效。ExceptionTranslationFilter 是各 SecurityFilterChain 内部的过滤器,每个链有独立的实例和配置。如果某条链未配置 exceptionHandling,它会使用默认的 AuthenticationEntryPoint(如 LoginUrlAuthenticationEntryPoint),导致 API 链的自定义处理器不生效。
正确做法
每个独立的 SecurityFilterChain 如果面向不同客户端(如 Web 和 API),都应该独立配置异常处理策略:
@Bean
@Order(1)
public SecurityFilterChain apiSecurity(HttpSecurity http) throws Exception {
http
.securityMatcher("/api/**")
.authorizeHttpRequests(auth -> auth.anyRequest().authenticated())
.httpBasic(Customizer.withDefaults())
.exceptionHandling(exceptions -> exceptions
.authenticationEntryPoint((req, res, ex) -> {
res.setStatus(401);
res.setContentType("application/json;charset=UTF-8");
res.getWriter().write("{\"error\":\"unauthenticated\"}");
})
.accessDeniedHandler((req, res, ex) -> {
res.setStatus(403);
res.setContentType("application/json;charset=UTF-8");
res.getWriter().write("{\"error\":\"forbidden\"}");
})
);
return http.build();
}
@Bean
@Order(2)
public SecurityFilterChain webSecurity(HttpSecurity http) throws Exception {
http
.securityMatcher("/web/**")
.authorizeHttpRequests(auth -> auth.anyRequest().authenticated())
.formLogin(Customizer.withDefaults())
.exceptionHandling(exceptions -> exceptions
// Web 端保持默认行为:未认证重定向到登录页
// 也可以显式配置 .accessDeniedPage("/403")
);
return http.build();
}
面试考点:
ExceptionTranslationFilter与FilterSecurityInterceptor的交互关系? 答:ExceptionTranslationFilter位于FilterSecurityInterceptor之前(上游)。FilterSecurityInterceptor执行授权决策时,如果当前用户未认证则抛出AuthenticationException,已认证但无权限则抛出AccessDeniedException。ExceptionTranslationFilter的try-catch捕获这些异常,分别调用AuthenticationEntryPoint(未认证)或AccessDeniedHandler(无权限)转译为 HTTP 响应。注意:匿名用户遇到AccessDeniedException时会被视为未认证处理,因为匿名用户并非真正的已认证身份。