Spring Security+JWT下POST路由权限验证异常求助
问题描述
我配置了Spring Security应用,想要实现仅拥有CHEF角色的用户能通过POST方法访问/api/receitas/add添加数据,已经禁用了CSRF但未生效。调用该POST接口时返回"Unauthorized error: Full authentication is required to access this resource",但调用带有相同@PreAuthorize("hasRole('CHEF')")注解的GET接口/api/receitas/chef却能正常返回200。
安全配置类
@Configuration @EnableMethodSecurity public class WebSecurityConfig { @Autowired UserDetailsServiceCustom userDetailsService; @Autowired private AuthEntryPointJwt unauthorizedHandler; @Bean public AuthTokenFilter authenticationJwtTokenFilter() { return new AuthTokenFilter(); } @Bean public DaoAuthenticationProvider authenticationProvider() { DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider(); authProvider.setUserDetailsService(userDetailsService); authProvider.setPasswordEncoder(passwordEncoder()); return authProvider; } @Bean public AuthenticationManager authenticationManager(AuthenticationConfiguration authConfig) throws Exception { return authConfig.getAuthenticationManager(); } @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } @Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http.csrf(csrf -> csrf.disable()) .exceptionHandling(exception -> exception.authenticationEntryPoint(unauthorizedHandler)) .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) .authorizeHttpRequests(auth -> auth.requestMatchers("/api/auth/**").permitAll() .requestMatchers(HttpMethod.POST, "/api/receitas/add").hasAnyRole("CHEF") .requestMatchers("/api/receitas/**").permitAll() .anyRequest().authenticated()); http.authenticationProvider(authenticationProvider()); http.addFilterBefore(authenticationJwtTokenFilter(), UsernamePasswordAuthenticationFilter.class); http.addFilterAfter(new CsrfTokenResponseHeaderBindingFilter(), CsrfFilter.class); return http.build(); } }
认证入口类
@Component public class AuthEntryPointJwt implements AuthenticationEntryPoint { private static final Logger logger = LoggerFactory.getLogger(AuthEntryPointJwt.class); @Override public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException { logger.error("Unauthorized error: {}", authException.getMessage()); response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Error: Unauthorized"); } }
控制器类
@RestController @RequestMapping("/api/receitas") @CrossOrigin(origins = "*", maxAge = 3600) public class ReceitaController { @Autowired AuthenticationManager authenticationManager; @Autowired private ReceitaService receitaService; @GetMapping("/listar") public ResponseEntity<?> listar() { List<Receita> receitas = receitaService.findAll(); return new ResponseEntity<List<Receita>>(receitas, HttpStatus.OK); } @GetMapping("/chef") @PreAuthorize("hasRole('CHEF')") public ResponseEntity<?> teste() { return new ResponseEntity<String>("Chef autorizado", HttpStatus.OK); } @PostMapping("/add") @PreAuthorize("hasRole('CHEF')") public ResponseEntity<?> salvar(@RequestBody CreateReceitaDTO receita) { System.out.println("tentando fazer " + receita.getName()); try { Receita newReceita = receitaService.createReceita(receita); return new ResponseEntity<Receita>(newReceita, HttpStatus.CREATED); } catch (Exception e) { ExceptionResponse exception = new ExceptionResponse(e.getMessage(), HttpStatus.BAD_REQUEST); return new ResponseEntity<ExceptionResponse>(exception, HttpStatus.BAD_REQUEST); } } }
JWT工具类
@Component public class JwtUtils { private static final Logger logger = LoggerFactory.getLogger(JwtUtils.class); @Value("${teles.app.jwtSecret}") private String jwtSecret; @Value("${teles.app.jwtExpirationMs}") private int jwtExpirationMs; @Value("${teles.app.jwtCookieName}") private String jwtCookie; public String getJwtFromCookies(HttpServletRequest request) { Cookie cookie = WebUtils.getCookie(request, jwtCookie); if (cookie != null) { return cookie.getValue(); } else { return null; } } public ResponseCookie generateJwtCookie(UserDetailsCustom userPrincipal) { String jwt = generateTokenFromUsername(userPrincipal.getUsername()); ResponseCookie cookie = ResponseCookie.from(jwtCookie, jwt).path("/api").maxAge(24 * 60 * 60).httpOnly(true) .build(); return cookie; } public ResponseCookie getCleanJwtCookie() { ResponseCookie cookie = ResponseCookie.from(jwtCookie, null).path("/api").build(); return cookie; } public String getUserNameFromJwtToken(String token) { return Jwts.parserBuilder().setSigningKey(key()).build() .parseClaimsJws(token).getBody().getSubject(); } private Key key() { return Keys.hmacShaKeyFor(Decoders.BASE64.decode(jwtSecret)); } public boolean validateJwtToken(String authToken) { try { Jwts.parserBuilder().setSigningKey(key()).build().parse(authToken); return true; } catch (MalformedJwtException e) { logger.error("Invalid JWT token: {}", e.getMessage()); } catch (ExpiredJwtException e) { logger.error("JWT token is expired: {}", e.getMessage()); } catch (UnsupportedJwtException e) { logger.error("JWT token is unsupported: {}", e.getMessage()); } catch (IllegalArgumentException e) { logger.error("JWT claims string is empty: {}", e.getMessage()); } return false; } public String generateTokenFromUsername(String username) { try { return Jwts.builder() .setSubject(username) .setIssuedAt(new Date()) .setExpiration(new Date((new Date()).getTime() + jwtExpirationMs)) .signWith(key(), SignatureAlgorithm.HS256) .compact(); } catch (Exception e) { throw new RuntimeException("Error while generation token", e); } } }
问题分析
- 请求匹配顺序冲突:Spring Security的规则是从上到下匹配,匹配到即停止。当前配置中,
requestMatchers("/api/receitas/**").permitAll()会覆盖前面的POST/api/receitas/add权限规则,导致权限校验逻辑混乱。 - 跨域Cookie携带问题:
@CrossOrigin(origins = "*")不支持跨域请求携带Cookie,POST请求可能无法传递JWT Cookie,导致认证失败。 - 多余的CSRF过滤器:已经禁用CSRF,但仍添加了
CsrfTokenResponseHeaderBindingFilter,可能干扰请求处理。 - 角色前缀匹配问题:
hasRole会自动添加ROLE_前缀,如果用户角色存储格式不匹配,会导致权限校验失败。
解决方案
1. 调整安全配置,移除冲突规则
依赖@EnableMethodSecurity提供的方法级权限控制,移除URL级的冗余规则,避免顺序冲突:
@Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http.csrf(csrf -> csrf.disable()) .exceptionHandling(exception -> exception.authenticationEntryPoint(unauthorizedHandler)) .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) .authorizeHttpRequests(auth -> auth.requestMatchers("/api/auth/**").permitAll() .anyRequest().authenticated()); http.authenticationProvider(authenticationProvider()); http.addFilterBefore(authenticationJwtTokenFilter(), UsernamePasswordAuthenticationFilter.class); // 移除多余的CSRF相关过滤器 // http.addFilterAfter(new CsrfTokenResponseHeaderBindingFilter(), CsrfFilter.class); return http.build(); }
2. 修复跨域配置,支持Cookie携带
将@CrossOrigin改为指定域名并开启凭证支持:
@CrossOrigin(origins = "http://你的前端域名", maxAge = 3600, allowCredentials = true)
同时前端请求时需配置withCredentials: true(如Axios:axios.post('/api/receitas/add', data, { withCredentials: true }));使用Postman测试时,确保请求携带正确的JWT Cookie。
3. 确认JWT过滤器正常工作
确保AuthTokenFilter的doFilterInternal方法正确从Cookie获取JWT并完成认证,示例实现如下:
public class AuthTokenFilter extends OncePerRequestFilter { @Autowired private JwtUtils jwtUtils; @Autowired private UserDetailsServiceCustom userDetailsService; private static final Logger logger = LoggerFactory.getLogger(AuthTokenFilter.class); @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { try { String jwt = jwtUtils.getJwtFromCookies(request); if (jwt != null && jwtUtils.validateJwtToken(jwt)) { String username = jwtUtils.getUserNameFromJwtToken(jwt); UserDetails userDetails = userDetailsService.loadUserByUsername(username); UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken( userDetails, null, userDetails.getAuthorities()); authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); SecurityContextHolder.getContext().setAuthentication(authentication); } } catch (Exception e) { logger.error("无法设置用户认证: {}", e); } filterChain.doFilter(request, response); } }
4. 统一角色权限格式
- 如果用户角色存储为
CHEF,将控制器中的@PreAuthorize改为hasAuthority("CHEF"); - 如果使用
hasRole("CHEF"),确保UserDetailsCustom的getAuthorities()返回带有ROLE_前缀的权限,比如new SimpleGrantedAuthority("ROLE_CHEF")。
内容的提问来源于stack exchange,提问作者Felipe Teles
相关产品推荐
相关产品推荐

