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

SecurityFilterChain中基于角色的授权失效问题求助

问题描述

我正在尝试基于角色保护接口端点,现有ADMIN、ESTUDIANTE(学生)、EMPRESA(企业)三种角色,每个用户仅拥有一种,且角色未存储在数据库中。我希望仅ADMIN角色能新增CarreraEntity实例,但在SecurityFilterChain中配置端点权限时,出现403禁止访问或非授权/无权限用户可访问的异常情况。

SecurityConfig类代码

@Configuration
@EnableWebSecurity
public class SecurityConfig {
    private final AuthenticationProvider authProvider;
    private final JwtAuthenticationFilter jwtAuthenticationFilter;

    @Autowired
    public SecurityConfig(AuthenticationProvider authProvider, JwtAuthenticationFilter jwtAuthenticationFilter) {
        this.authProvider = authProvider;
        this.jwtAuthenticationFilter = jwtAuthenticationFilter;
    }

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        return http
                .csrf(AbstractHttpConfigurer::disable)
                .authorizeHttpRequests(authRequest ->
                        authRequest
                                .requestMatchers("/api/v1/auth/**").permitAll()
                                .requestMatchers("/api/v1/auth/contrasenia").hasAnyRole(
                                        Role.ADMIN.name(), Role.EMPRESA.name(), Role.ESTUDIANTE.name()
                                )
                                .requestMatchers(HttpMethod.GET, "/api/v1/carreras/**").permitAll()
                                .requestMatchers(HttpMethod.POST, "/api/v1/carreras/**").hasRole(Role.ADMIN.name())
                                .anyRequest().authenticated()
                )
                .sessionManagement(sessionManager ->
                        sessionManager
                                .sessionCreationPolicy(SessionCreationPolicy.STATELESS))
                .authenticationProvider(authProvider)
                .addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class)
                .build();
    }
}

出现问题的端点代码

@GetMapping
public ResponseEntity<Set<CarreraDTO>> getCarreras() {
    return ResponseEntity.ok(carreraService.getCarreras());
}

// TODO: return 201
@PostMapping(consumes = "application/json")
public ResponseEntity<CarreraDTO> postCarrera(@RequestBody CarreraRequest request) {
    return ResponseEntity.ok(carreraService.postCarrera(request));
}

JWT声明中分配角色的代码

public String getToken(UserDetails user) {
    HashMap<String, Object> claims = new HashMap<>();

    String role = user.getAuthorities()
                .stream()
                .findFirst()
                .map(GrantedAuthority::getAuthority)
                .orElseThrow();
    claims.put("role", role);

    return getToken(claims, user);
}

private String getToken(HashMap<String,Object> map, UserDetails user) {
    return Jwts.builder()
                .claims(map)
                .subject(user.getUsername())
                .issuedAt(new Date(System.currentTimeMillis()))
                .expiration(new Date(System.currentTimeMillis() + 1000 * 60 * 60 * 24))
                .signWith(this.getKey(), Jwts.SIG.HS256)
                .compact();
}

JwtAuthenticationFilter类代码

@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {
    private final JwtService jwtService;
    private final UserDetailsService userDetailsService;

    @Autowired
    public JwtAuthenticationFilter(JwtService jwtService, @Qualifier("userDetailsService") UserDetailsService userDetailsService) {
        this.jwtService = jwtService;
        this.userDetailsService = userDetailsService;
    }

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        final String token = this.getTokenFromRequest(request);
        final String username;

        if (token == null) {
            filterChain.doFilter(request, response);
            return;
        }

        username = jwtService.getUsernameFromToken(token);

        if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
            UserDetails userDetails = userDetailsService.loadUserByUsername(username);

            if (jwtService.isTokenValid(token, userDetails)) {
                UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken(
                        userDetails,
                        null,
                        userDetails.getAuthorities()
                );

                authToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));

                SecurityContextHolder.getContext().setAuthentication(authToken);
            }
        }

        filterChain.doFilter(request, response);
    }

    private String getTokenFromRequest(HttpServletRequest request) {
        final String authHeader = request.getHeader(HttpHeaders.AUTHORIZATION);

        return jwtService.getTokenFromAuthorizationHeader(authHeader).orElse(null);
    }
}

我尝试过使用方法安全注解(@PermitAll、@PreAuthorize),也试过hasAuthority(),但这些方案都无法解决问题,不知道该如何处理。


解决方案

问题核心出在Spring Security角色前缀规则和JWT权限解析逻辑上,以下是针对性修复步骤:

1. 统一权限匹配规则

Spring Security的hasRole()方法会自动给角色名拼接ROLE_前缀校验,如果你Role枚举值是ADMIN而非ROLE_ADMIN,会直接导致权限不匹配。两种修复方式二选一:

  • 方式一:修改Role枚举
    给所有角色值加上ROLE_前缀:
    public enum Role {
        ROLE_ADMIN,
        ROLE_ESTUDIANTE,
        ROLE_EMPRESA
    }
    
  • 方式二:改用hasAuthority()
    hasAuthority()不会自动添加前缀,直接匹配权限字符串,修改SecurityConfig配置:
    .requestMatchers("/api/v1/auth/contrasenia").hasAnyAuthority(
            Role.ADMIN.name(), Role.EMPRESA.name(), Role.ESTUDIANTE.name()
    )
    .requestMatchers(HttpMethod.POST, "/api/v1/carreras/**").hasAuthority(Role.ADMIN.name())
    

2. 从JWT直接提取权限

当前你依赖UserDetailsService加载权限,但角色未存数据库,UserDetailsService无法正确返回权限。需要直接从JWT的role声明中提取权限:

修改JwtAuthenticationFilter逻辑

if (jwtService.isTokenValid(token, userDetails)) {
    // 从JWT中提取角色
    String role = jwtService.extractRoleFromToken(token);
    // 构造权限集合
    Collection<GrantedAuthority> authorities = Collections.singletonList(new SimpleGrantedAuthority(role));
    
    UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken(
            userDetails,
            null,
            authorities
    );

    authToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
    SecurityContextHolder.getContext().setAuthentication(authToken);
}

在JwtService添加提取角色方法

public String extractRoleFromToken(String token) {
    Claims claims = Jwts.parser()
            .verifyWith(getKey())
            .build()
            .parseSignedClaims(token)
            .getPayload();
    return claims.get("role", String.class);
}

3. 启用方法安全注解(可选)

如果要使用@PreAuthorize等注解,需在SecurityConfig上添加@EnableMethodSecurity(Spring Security 6+版本):

@Configuration
@EnableWebSecurity
@EnableMethodSecurity
public class SecurityConfig {
    // 现有代码不变
}

之后可直接在接口方法上使用:

@PreAuthorize("hasAuthority('ADMIN')")
@PostMapping(consumes = "application/json")
public ResponseEntity<CarreraDTO> postCarrera(@RequestBody CarreraRequest request) {
    return ResponseEntity.ok(carreraService.postCarrera(request));
}

4. 调试建议

  • 开启Spring Security debug日志,查看权限校验过程:
    在application.properties中添加:
    logging.level.org.springframework.security=DEBUG
    
  • 在JwtAuthenticationFilter中打印当前用户权限,确认是否正确加载:
    System.out.println("当前用户权限:" + authToken.getAuthorities());
    

内容的提问来源于stack exchange,提问作者Gerónimo González Martino

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.17 01:20:56