共计 2658 个字符,预计需要花费 7 分钟才能阅读完成。
问题背景
在基于 Spring Security 实现 JWT 认证时,开发者通常需要自定义一个过滤器(如 JwtAuthFilter)来拦截请求并验证 Token。但许多人会遇到一个看似简单的陷阱:当请求未携带 Token 时,预期的 401 UNAUTHORIZED 状态码没有返回,反而可能得到 200 OK、403 FORBIDDEN,甚至重定向到登录页面(302 FOUND)。
以下是一个典型的“不生效”代码片段:
if (authHeader == null || !authHeader.startsWith("Bearer")) {response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
filterChain.doFilter(request, response);
return;
}
看起来逻辑很直接:如果请求头中没有 Bearer Token,设置 401 状态码并终止流程。但实际测试时会发现,客户端并未收到 401。
问题原因
- 过滤器链未终止
调用filterChain.doFilter(request, response)后,请求会继续被后续的过滤器和 Spring Security 默认逻辑处理。例如:
- Spring Security 的默认行为可能会将未认证的请求重定向到登录页面(返回
302),覆盖你设置的401。 - 如果请求路径是公开的(如
/login),后续处理可能会返回200。
- 仅设置状态码不够
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 替代 setStatusresponse.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()而非仅设置状态码。
延伸阅读