一个开发者对 Spring Security 拦截机制的深度追问(续)

作者:旷野说
关键词:Spring Security、Servlet Filter、403 异常、安全架构、过滤器链、口诀记忆

🕵️‍♂️ 三、“静默 403”不是玄学,而是可解的谜题

为搞明白 ExceptionTranslationFilter 的“温柔一刀”——它把所有安全异常都悄悄吞掉,换成一个干净的 403 响应。
对用户友好,对开发者“不友好”。

但好消息是:这个问题完全可解。下面是我总结的四步调试法,从“看到异常”到“精准定位”,再到“生产友好响应”,一气呵成。


✅ 第一步:先开 DEBUG 日志——让安全决策“说话”

这是成本最低、见效最快的方式。只需在 application.yml 中加一行:

logging:
  level:
    org.springframework.security: DEBUG

开启后,你会看到类似这样的日志:

DEBUG o.s.s.w.a.i.FilterSecurityInterceptor - 
  Secure object: FilterInvocation: URL: /api/v1/orders/create
  Attributes: [authenticated]

DEBUG o.s.s.w.a.i.FilterSecurityInterceptor - 
  Previously Authenticated: User [name=admin, roles=[USER, ADMIN]]

DEBUG o.s.s.access.vote.AffirmativeBased - 
  Voter: WebExpressionVoter, returned: -1  👈 权限投票失败!

AccessDeniedException: Access is denied

💡 关键信息

  • 请求的 URL 是什么?
  • 当前用户是谁?
  • 权限校验在哪一步失败?
  • FilterSecurityInterceptor(URL 层)还是 MethodSecurityInterceptor(方法层)抛出的?

这是日常开发的首选手段——无侵入、开箱即用。


✅ 第二步:自定义异常处理器——让 403 “会说话”

DEBUG 日志适合后台看,但前端也需要知道为什么被拒
于是,我实现了两个处理器:

1. CustomAccessDeniedHandler(处理 403)
@Component
public class CustomAccessDeniedHandler implements AccessDeniedHandler {
    private static final Logger log = LoggerFactory.getLogger(CustomAccessDeniedHandler.class);

    @Override
    public void handle(HttpServletRequest req, HttpServletResponse res, AccessDeniedException e) throws IOException {
        String uri = req.getRequestURI();
        Authentication auth = SecurityContextHolder.getContext().getAuthentication();
        String user = auth != null ? auth.getName() : "anonymous";

        // 📝 记录完整上下文,便于排查
        log.warn("ACCESS DENIED | URI: {} | User: {} | Cause: {}", uri, user, e.getMessage());

        // 💬 返回结构化错误,前端可友好提示
        res.setStatus(403);
        res.setContentType("application/json;charset=UTF-8");
        res.getWriter().write("""
            {
              "code": 403,
              "message": "权限不足",
              "detail": "当前用户无权访问此资源",
              "timestamp": "%s"
            }
            """.formatted(Instant.now()));
    }
}
2. CustomAuthenticationEntryPoint(处理 401)
@Component
public class CustomAuthenticationEntryPoint implements AuthenticationEntryPoint {
    @Override
    public void commence(HttpServletRequest req, HttpServletResponse res, AuthenticationException ex) throws IOException {
        log.warn("UNAUTHENTICATED | URI: {} | Error: {}", req.getRequestURI(), ex.getMessage());
        res.setStatus(401);
        res.getWriter().write("""
            {"code":401,"message":"请先登录","detail":"Token missing or invalid"}
            """);
    }
}
3. 在 SecurityConfig 中注册:
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
    http
        .exceptionHandling(e -> e
            .accessDeniedHandler(customAccessDeniedHandler)
            .authenticationEntryPoint(customAuthenticationEntryPoint)
        )
        .csrf().disable()
        .authorizeHttpRequests(authz -> authz
            .requestMatchers("/login").permitAll()
            .anyRequest().authenticated()
        )
        .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
        .addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);

    return http.build();
}

效果

  • 前端收到 带错误码和提示的 JSON,不再一脸懵;
  • 后台日志留下 完整上下文,支持快速排查。

✅ 第三步:开发环境临时“放行异常”——看原始堆栈

有时候,连日志都不够用。我想直接看到异常堆栈

于是,我在开发环境加了一个开关:

@Value("${app.security.debug:false}")
private boolean securityDebug;

@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
    // ... 其他配置

    // 🔧 仅在生产关闭异常转换,开发环境直接抛异常!
    if (!securityDebug) {
        http.exceptionHandling(e -> e
            .accessDeniedHandler(accessDeniedHandler)
            .authenticationEntryPoint(authenticationEntryPoint)
        );
    }

    return http.build();
}

启动时开启:

# application-dev.yml
app:
  security:
    debug: true

效果
直接看到 AccessDeniedException 完整堆栈,一眼看出是 Web 层还是方法层拦截

⚠️ 注意:此模式仅用于本地开发,切勿上线


✅ 第四步:快速判断拦截层级——是 Filter 还是 AOP?

这是我总结的快速诊断表

判断依据Filter 拦截(URL 权限)AOP 拦截(方法权限)
Controller 是否执行?❌ 不执行✅ 执行
Service 方法是否进?❌ 不进❌ 不进(但在 Controller 之后被拦)
日志关键词FilterInvocation: URL: /xxxMethod: public void ...
调试技巧Controller 第一行加日志 → 没输出Controller 日志有输出,Service 没进 → 方法级拦截

💡 口诀
“403 无日志?查 Filter;进了 Controller 还 403?查 @PreAuthorize!”


🧠 终极口诀:调试 403 的黄金五步法

一开日志看过程,
二写 Handler 记上下文,
三用开关看堆栈,
四判层级分 Filter 与 AOP,
五记口诀不迷路!


通过这套组合拳,我终于把“神秘 403”变成了可追踪、可解释、可修复的普通 bug。
Spring Security 不再是黑盒,而是一条透明、可控的安全流水线

安全,本该如此清晰。 🔐

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值