为什么你的 Spring Security JWT 过滤器不返回 401?一个常见的坑与解决方案

999次阅读
没有评论

共计 2658 个字符,预计需要花费 7 分钟才能阅读完成。


问题背景

在基于 Spring Security 实现 JWT 认证时,开发者通常需要自定义一个过滤器(如 JwtAuthFilter)来拦截请求并验证 Token。但许多人会遇到一个看似简单的陷阱:当请求未携带 Token 时,预期的 401 UNAUTHORIZED 状态码没有返回,反而可能得到 200 OK403 FORBIDDEN,甚至重定向到登录页面(302 FOUND)。

以下是一个典型的“不生效”代码片段:

if (authHeader == null || !authHeader.startsWith("Bearer")) {response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
    filterChain.doFilter(request, response);
    return;
}

看起来逻辑很直接:如果请求头中没有 Bearer Token,设置 401 状态码并终止流程。但实际测试时会发现,客户端并未收到 401


问题原因

  1. 过滤器链未终止
    调用 filterChain.doFilter(request, response) 后,请求会继续被后续的过滤器和 Spring Security 默认逻辑处理。例如:
  • Spring Security 的默认行为可能会将未认证的请求重定向到登录页面(返回 302),覆盖你设置的 401
  • 如果请求路径是公开的(如 /login),后续处理可能会返回 200
  1. 仅设置状态码不够
    response.setStatus() 仅修改状态码,但不会立即提交响应。如果后续流程向响应体中写入内容(如 HTML、JSON),状态码可能会被重置为 200

解决方案

1. 终止过滤器链
不要调用 filterChain.doFilter()!直接返回并发送错误响应:

if (authHeader == null || !authHeader.startsWith("Bearer")) {response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Missing or invalid token");
    return; // 关键:不再继续执行过滤器链
}

2. 使用 sendError 替代 setStatus
response.sendError() 会直接提交响应并终止请求处理,确保客户端收到明确的错误状态:

// ❌ 错误方式:仅设置状态码
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);

// ✅ 正确方式:发送错误并终止
response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Invalid token");

完整代码修复

以下是修正后的 JwtAuthFilter 关键逻辑:

@Slf4j
@Component
public class JwtAuthFilter extends OncePerRequestFilter {

    @Override
    protected void doFilterInternal(
        HttpServletRequest request,
        HttpServletResponse response,
        FilterChain filterChain
    ) throws ServletException, IOException {final String authHeader = request.getHeader("Authorization");

        // 1. 检查认证头是否存在且格式正确
        if (authHeader == null || !authHeader.startsWith("Bearer")) {response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Missing or invalid token");
            return; // 直接终止,不再执行后续逻辑
        }

        // 2. 提取并验证 Token
        try {String jwt = authHeader.substring(7);
            String username = jwtUtil.extractUsername(jwt);
            // ... 验证逻辑 ...
        } catch (Exception e) {response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Invalid token");
            return;
        }

        // 3. 验证通过,继续执行后续过滤器
        filterChain.doFilter(request, response);
    }
}

其他注意事项

1. 过滤器的注册位置
确保 JwtAuthFilter 被添加到 Spring Security 过滤器链的正确位置。通常需要将其放在 UsernamePasswordAuthenticationFilter 之前:

@Configuration
@EnableWebSecurity
public class SecurityConfig {

    @Autowired
    private JwtAuthFilter jwtAuthFilter;

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
            .addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class)
            .authorizeRequests(auth -> auth
                .antMatchers("/api/public/**").permitAll()
                .anyRequest().authenticated()
            );
        return http.build();}
}

2. 路径匹配检查
如果某些路径(如 /login)被配置为公开访问,确保这些路径不会被其他安全规则覆盖。

3. 测试验证
使用工具(如 Postman 或 curl)发送无 Token 的请求,观察响应状态码和日志:

curl -v http://localhost:8080/api/protected

总结

  • 终止过滤器链:未通过认证时,立即返回并调用 response.sendError(),避免后续处理覆盖状态码。
  • 正确注册过滤器:确保过滤器位于 Spring Security 链的合适位置。
  • 明确错误响应:使用 sendError() 而非仅设置状态码。

延伸阅读

正文完
 0
评论(没有评论)