Spring Boot 3.2+Spring Security 6.2 JWT认证后403错误求助
Spring Boot 3.2 + Spring Security 6.2 403空响应与JWT昵称传递解决方案
一、403空响应的排查与修复
1. 检查SecurityFilterChain权限规则
首先确认安全配置是否给目标接口开放了正确权限,前后端分离场景下通常需禁用CSRF,同时明确接口的认证要求:
@Bean public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { http .csrf(csrf -> csrf.disable()) .authorizeHttpRequests(auth -> auth .requestMatchers("/auth/login", "/auth/register").permitAll() // 放行登录注册接口 .requestMatchers("/posts/create").authenticated() // 发帖接口要求认证 .anyRequest().authenticated() // 其他未声明接口默认需认证 ) .oauth2ResourceServer(oauth2 -> oauth2.jwt(Customizer.withDefaults())); return http.build(); }
如果使用了自定义JWT过滤器,要确保它被正确添加到过滤器链前端:
http.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
2. 验证JWT认证上下文填充逻辑
403空响应大概率是SecurityContextHolder未正确存入认证信息,检查JWT过滤器的核心逻辑:
@Component public class JwtAuthFilter extends OncePerRequestFilter { private final JwtDecoder jwtDecoder; public JwtAuthFilter(JwtDecoder jwtDecoder) { this.jwtDecoder = jwtDecoder; } @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { String token = extractToken(request); if (token != null) { try { Jwt jwt = jwtDecoder.decode(token); // 从JWT中提取昵称并构造认证对象 String nickname = jwt.getClaim("nickname").asString(); Authentication auth = new UsernamePasswordAuthenticationToken(nickname, null, Collections.emptyList()); SecurityContextHolder.getContext().setAuthentication(auth); } catch (JwtException e) { // 无效JWT直接返回401,避免走到后续逻辑触发403 response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); return; } } filterChain.doFilter(request, response); } private String extractToken(HttpServletRequest request) { String bearer = request.getHeader("Authorization"); return bearer != null && bearer.startsWith("Bearer ") ? bearer.substring(7) : null; } }
3. 配置自定义403响应
空内容的403是因为Spring Security默认不返回JSON格式响应,添加异常处理器让返回更明确:
@Bean public AccessDeniedHandler accessDeniedHandler() { return (request, response, ex) -> { response.setContentType(MediaType.APPLICATION_JSON_VALUE); response.setStatus(HttpServletResponse.SC_FORBIDDEN); response.getWriter().write("{\"code\":403,\"msg\":\"无权访问\"}"); }; }
在SecurityFilterChain中绑定该处理器:
http.exceptionHandling(ex -> ex.accessDeniedHandler(accessDeniedHandler()));
二、将JWT昵称传入DTO的两种实现方式
1. 直接从SecurityContextHolder提取(快速实现)
在Controller中直接获取认证信息填充DTO:
@PostMapping("/posts/create") public ResponseEntity<PostDTO> createPost(@RequestBody PostCreateDTO dto) { // 从认证上下文取昵称 String author = (String) SecurityContextHolder.getContext().getAuthentication().getPrincipal(); dto.setAuthor(author); PostDTO savedPost = postService.savePost(dto); return ResponseEntity.ok(savedPost); }
2. 自定义参数解析器(优雅解耦)
定义注解标记需要获取昵称的参数:
@Target(ElementType.PARAMETER) @Retention(RetentionPolicy.RUNTIME) public @interface CurrentAuthor { }
实现参数解析器:
@Component public class CurrentAuthorResolver implements HandlerMethodArgumentResolver { @Override public boolean supportsParameter(MethodParameter parameter) { return parameter.hasParameterAnnotation(CurrentAuthor.class); } @Override public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception { Authentication auth = SecurityContextHolder.getContext().getAuthentication(); if (auth == null || !auth.isAuthenticated()) { throw new AccessDeniedException("用户未登录"); } // 若使用自定义UserDetails,可强转后取昵称;此处假设认证主体为JWT对象 Jwt jwt = (Jwt) auth.getPrincipal(); return jwt.getClaim("nickname").asString(); } }
注册解析器到Spring MVC:
@Configuration public class WebMvcConfig implements WebMvcConfigurer { private final CurrentAuthorResolver authorResolver; public WebMvcConfig(CurrentAuthorResolver authorResolver) { this.authorResolver = authorResolver; } @Override public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) { resolvers.add(authorResolver); } }
在Controller中直接使用注解:
@PostMapping("/posts/create") public ResponseEntity<PostDTO> createPost(@RequestBody PostCreateDTO dto, @CurrentAuthor String author) { dto.setAuthor(author); PostDTO savedPost = postService.savePost(dto); return ResponseEntity.ok(savedPost); }
三、关键注意点
- 确保生成JWT时已将昵称存入Claims,比如登录接口的token生成逻辑:
private String generateJwt(User user) { Map<String, Object> claims = new HashMap<>(); claims.put("nickname", user.getNickname()); // 必须包含该字段 return Jwts.builder() .setClaims(claims) .setSubject(user.getUsername()) .setIssuedAt(new Date()) .setExpiration(new Date(System.currentTimeMillis() + 3600 * 1000)) .signWith(SignatureAlgorithm.HS256, "your-secret-key") .compact(); } - 检查跨域过滤器是否在JWT过滤器之前执行,避免请求被提前拦截:
@Bean public CorsFilter corsFilter() { CorsConfiguration config = new CorsConfiguration(); config.setAllowCredentials(true); config.addAllowedOriginPattern("*"); config.addAllowedHeader("*"); config.addAllowedMethod("*"); UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); source.registerCorsConfiguration("/**", config); return new CorsFilter(source); }
内容的提问来源于stack exchange,提问作者NightSky
相关产品推荐
相关产品推荐

