乐途乐途
主页
  • 计算机基础

    • TCP/IP
    • Linux
    • HTTP
  • 数据库

    • SQL
    • MySQL 5.7
  • 编程语言

    • C
    • C++
    • Java SE
    • Python2
    • Python3
  • 数据格式

    • JSON
    • XML
  • 认证与安全

    • JWT
  • 工具

    • Markdown
  • Git

    • GitFlow
  • Quartz

    • Quartz
  • Java

    • Maven 入门
    • Maven 进阶
    • MyBatis
    • Spring
    • Spring MVC
  • Java

    • Spring Boot
    • Spring Cloud
    • Spring Cloud Alibaba
    • Spring Security
    • Spring AI
    • Spring Batch
    • Kafka
    • Java 设计模式
  • 缓存

    • Redis
  • 搜索引擎

    • Elasticsearch
  • 分布式协调

    • ZooKeeper
联系
阿里云
主页
  • 计算机基础

    • TCP/IP
    • Linux
    • HTTP
  • 数据库

    • SQL
    • MySQL 5.7
  • 编程语言

    • C
    • C++
    • Java SE
    • Python2
    • Python3
  • 数据格式

    • JSON
    • XML
  • 认证与安全

    • JWT
  • 工具

    • Markdown
  • Git

    • GitFlow
  • Quartz

    • Quartz
  • Java

    • Maven 入门
    • Maven 进阶
    • MyBatis
    • Spring
    • Spring MVC
  • Java

    • Spring Boot
    • Spring Cloud
    • Spring Cloud Alibaba
    • Spring Security
    • Spring AI
    • Spring Batch
    • Kafka
    • Java 设计模式
  • 缓存

    • Redis
  • 搜索引擎

    • Elasticsearch
  • 分布式协调

    • ZooKeeper
联系
阿里云
  • 学习路径
  • 第1章 Spring Security 基础

    • 本章定位
    • Spring Security 是什么
    • DelegatingFilterProxy
    • 安全过滤器链
    • SecurityFilterChain
    • 过滤器执行顺序
  • 第2章 认证

    • 本章定位
    • Authentication
    • 认证流程
    • AuthenticationManager
    • ProviderManager
    • DaoAuthenticationProvider
    • UserDetails
    • UserDetailsService
    • PasswordEncoder
    • BCryptPasswordEncoder
    • DelegatingPasswordEncoder
    • 表单登录
    • SecurityContext
    • SecurityContextHolder
    • UsernamePasswordAuthenticationToken
  • 第3章 授权

    • 本章定位
    • 授权模型
    • GrantedAuthority
    • AccessDecisionManager
    • AccessDecisionVoter
    • URL 级别授权
    • 方法级别安全
    • @PreAuthorize
    • @PostAuthorize
    • @PreFilter
    • @PostFilter
    • @Secured
    • RoleHierarchy
  • 第4章 过滤器链

    • 本章定位
    • FilterChainProxy
    • SecurityContextHolderFilter
    • LogoutFilter
    • BasicAuthenticationFilter
    • CsrfFilter
    • CorsFilter
    • HeaderWriterFilter
    • AnonymousAuthenticationFilter
    • RequestCacheAwareFilter
    • ExceptionTranslationFilter
    • FilterSecurityInterceptor
  • 第5章 会话管理

    • 本章定位
    • 会话管理
    • SessionFixation
    • 会话并发控制
    • SessionCreationPolicy
    • RememberMe
  • 第6章 JWT

    • 本章定位
    • JWT
    • JwtDecoder
    • JWT 认证
    • JwtAuthenticationConverter
  • 第7章 OAuth2

    • 本章定位
    • OAuth2 基础
    • OAuth2 Client
    • OAuth2 Resource Server
    • 第三方登录配置
  • 第8章 攻击防护

    • 本章定位
    • CSRF 跨站请求伪造防护
    • CORS 跨域防护
    • Clickjacking 点击劫持防护
    • 安全响应头
    • Session Fixation 会话固定防护
  • 第9章 测试

    • 本章定位
    • 安全测试
    • @WithMockUser
    • 最佳实践

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/infoGET /api/user/info401 {code:401, message:"未认证,请先登录"}
普通用户访问 /api/admin/configGET /api/admin/config403 {code:403, message:"权限不足,无法访问该资源"}
匿名访问 /api/public/noticeGET /api/public/notice200 正常返回
未认证访问 /web/pageGET /web/page302 重定向到 /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_USER403 "权限不足"已认证用户无权限,走 AccessDeniedHandler
GET /member/profile已登录 ROLE_USER200 正常访问通过授权检查
GET /admin/super已登录 ROLE_USER403 "权限不足"已认证用户无 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 时会被视为未认证处理,因为匿名用户并非真正的已认证身份。

上一页
RequestCacheAwareFilter
下一页
FilterSecurityInterceptor