You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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);
        }

    }
}

问题分析
  1. 请求匹配顺序冲突:Spring Security的规则是从上到下匹配,匹配到即停止。当前配置中,requestMatchers("/api/receitas/**").permitAll()会覆盖前面的POST /api/receitas/add权限规则,导致权限校验逻辑混乱。
  2. 跨域Cookie携带问题:@CrossOrigin(origins = "*")不支持跨域请求携带Cookie,POST请求可能无法传递JWT Cookie,导致认证失败。
  3. 多余的CSRF过滤器:已经禁用CSRF,但仍添加了CsrfTokenResponseHeaderBindingFilter,可能干扰请求处理。
  4. 角色前缀匹配问题: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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.15 13:47:32