
本文详解 Spring Security 中 JWT 认证返回 401 的常见原因,重点分析请求匹配规则(requestMatchers)与 HTTP 方法、路径模式的匹配逻辑,并提供可落地的配置修正方案与调试建议。
本文详解 spring security 中 jwt 认证返回 401 的常见原因,重点分析请求匹配规则(`requestmatchers`)与 http 方法、路径模式的匹配逻辑,并提供可落地的配置修正方案与调试建议。
在基于 Spring Security + JWT 的无状态认证架构中,出现“Token 解析成功但依然返回 401 Unauthorized”是一个高频且易被忽视的问题。从你提供的代码可见:JwtAuthorizationFilter 中 logger.info(auth.getName()) 已正确输出用户名,说明 JWT 解析、UserDetailsService 加载、UsernamePasswordAuthenticationToken 创建均无异常——问题不出在鉴权逻辑本身,而出在 Spring Security 的访问控制决策环节。
? 根本原因:请求匹配规则未覆盖目标接口
你原始的授权配置如下:
.authorizeHttpRequests(auth -> {
auth.requestMatchers(HttpMethod.GET, "/**").permitAll()
.requestMatchers(HttpMethod.PUT, "/login").permitAll()
.requestMatchers(HttpMethod.PUT, "admin/**").authenticated();
})
该配置存在两个关键问题:
HTTP 方法不匹配:登录接口通常使用 POST /login(而非 PUT),导致 /login 请求未被 permitAll() 放行,直接进入认证链;而此时请求头中尚未携带有效 Token(因为还没登录),JwtAuthorizationFilter 返回 null,后续 FilterChain 继续执行时,因无认证上下文且无匹配放行规则,最终触发 HttpStatusEntryPoint(HttpStatus.UNAUTHORIZED) → 401。
路径匹配范围过窄:requestMatchers(HttpMethod.PUT, "admin/**") 仅对 PUT 方法且路径以 admin/ 开头的请求要求认证,但实际受保护的资源(如 GET /api/users、POST /admin/create)可能使用其他 HTTP 方法(GET/POST/DELETE),这些请求完全不匹配任何规则,将落入默认拒绝策略(Spring Security 6+ 默认为 denyAll()),直接返回 401。
✅ 正确做法是采用分层授权策略:
- 明确放行公开端点(如 POST /login, GET /health);
- 对剩余所有受保护请求统一要求认证(anyRequest().authenticated())。
修正后的配置如下:
.authorizeHttpRequests(auth -> {
auth.requestMatchers(HttpMethod.GET, "/**").permitAll() // 静态资源等
.requestMatchers(HttpMethod.POST, "/login").permitAll() // ✅ 登录必须用 POST
.anyRequest().authenticated(); // ✅ 所有其他请求均需认证
})
⚠️ 其他关键注意事项
过滤器顺序至关重要:确保 JwtAuthorizationFilter 在 SecurityContextPersistenceFilter 之后、AuthorizationFilter(即 FilterSecurityInterceptor)之前执行。你的配置中 .addFilter(new JwtAuthorizationFilter(...)) 是正确的,但需确认它不能放在 .addFilter(authenticationFilter()) 之后覆盖认证上下文(当前顺序合理)。
-
SecurityContextHolder 设置时机:你的 doFilterInternal 中设置了上下文,但需确保没有其他过滤器(如自定义异常处理)重置了它。可在 AuthorizationFilter 前添加日志验证:
@Component public class DebugFilter implements Filter { @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { Authentication auth = SecurityContextHolder.getContext().getAuthentication(); System.out.println("Before auth check: " + (auth != null ? auth.getName() : "ANONYMOUS")); chain.doFilter(request, response); } } JWT 签名密钥一致性:确保 secret 在 JwtAuthorizationFilter 和签发 Token 的 JwtUtil 中完全一致(包括空格、大小写),否则 JWT.require(...).verify() 会抛出 SignatureVerificationException,但你的日志未体现该异常,说明此处正常。
-
CORS 配置影响:若前端跨域调用,OPTIONS 预检请求可能被拦截。确保 cors().and() 启用且允许凭证(allowCredentials(true))和所需 Header(如 Authorization):
http.cors(cors -> cors.configurationSource(request -> { CorsConfiguration config = new CorsConfiguration(); config.setAllowedOrigins(Arrays.asList("http://localhost:3000")); config.setAllowCredentials(true); config.setAllowedHeaders(Arrays.asList("Authorization", "Content-Type")); return config; }))
✅ 最终推荐的安全配置片段
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
return http
.cors(Customizer.withDefaults()) // 或按需配置
.csrf(AbstractHttpConfigurer::disable)
.sessionManagement(session -> session
.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.authorizeHttpRequests(auth -> auth
.requestMatchers(HttpMethod.GET, "/", "/swagger-ui/**", "/v3/api-docs/**").permitAll()
.requestMatchers(HttpMethod.POST, "/login", "/register").permitAll()
.anyRequest().authenticated()
)
.exceptionHandling(ex -> ex
.authenticationEntryPoint(new HttpStatusEntryPoint(HttpStatus.UNAUTHORIZED)))
.addFilterBefore(authenticationFilter(), UsernamePasswordAuthenticationFilter.class)
.addFilterAfter(new JwtAuthorizationFilter(
authenticationManager(http.getSharedObject(AuthenticationConfiguration.class)),
userDetailsManager(), secret),
UsernamePasswordAuthenticationFilter.class)
.build();
}
通过精准匹配请求方法与路径、明确放行策略、验证过滤器链行为,即可彻底解决“Token 有效却 401”的问题。记住:Spring Security 的授权决策发生在过滤器链后半段,一切前置逻辑(解析 Token、设上下文)都只是为它提供依据——而规则本身,才是最终判决者。











