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

Spring Security基于角色访问控制返回403错误求助

Spring Security JWT授权403问题排查与解决

我正在开发一个集成Spring Security与JWT令牌授权的小型应用,遇到如下问题:使用hasRole()或hasPermission()方法区分请求访问权限时,即使授权用户拥有符合限制的角色,请求仍返回403状态码。已尝试添加ROLE_前缀、使用纯字符串格式定义角色、同时使用hasRole()和hasAuthority()方法,但问题依旧。之前按此方式实现时一切正常。

UserDetails与UserDetailsService实现

@RequiredArgsConstructor
public class UserDetailsImpl implements UserDetails {

  private final User user;

  @Override
  public Collection<? extends GrantedAuthority> getAuthorities() {
    return user.getRoles();
  }

  @Override
  public String getPassword() {
    return user.getPassword();
  }

  @Override
  public String getUsername() {
    return user.getUsername();
  }

  @Override
  public boolean isAccountNonExpired() {
    return user.getIsEnable();
  }

  @Override
  public boolean isAccountNonLocked() {
    return user.getIsEnable();
  }

  @Override
  public boolean isCredentialsNonExpired() {
    return user.getIsEnable();
  }

  @Override
  public boolean isEnabled() {
    return user.getIsEnable();
  }
}
@Service
@RequiredArgsConstructor
public class UserDetailsServiceImpl implements UserDetailsService {
  private final UserRepository userRepository;

  @Override
  public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
    String exceptionMsg = String.format("User with username '%s' not found", username);
    return new UserDetailsImpl(userRepository.findUserByUsername(username)
            .orElseThrow(() -> new UsernameNotFoundException(exceptionMsg)));
  }
}

User实体与Roles枚举

@Entity(name = "users")
@AllArgsConstructor
@NoArgsConstructor
@Getter
@Setter
@EqualsAndHashCode(of = {"id", "username"})
public class User {
  @Transient
  private final String MAIL_REGEX =
          "^\\w+([\\.-]?\\w+)*@\\w+([\\.-]?\\w+)*(\\.\\w{2,3})+$";

  @Id
  @GeneratedValue(strategy = IDENTITY)
  private Long id;

  @Column(unique = true, nullable = false)
  @NotBlank(message = "Username cannot be empty")
  @NotNull(message = "Username cannot be empty")
  @Size(min = 1, max = 90)
  private String username;

  @Column(nullable = false)
  @NotBlank(message = "Password cannot be empty")
  @NotNull(message = "Password cannot be empty")
  @Size(min = 8, max = 100)
  private String password;

  @Column(unique = true, nullable = false)
  @Email(message = "Invalid email address", regexp = MAIL_REGEX)
  @NotBlank(message = "Email cannot be empty")
  private String email;

  @ElementCollection(fetch = EAGER)
  private Set<Role> roles = Set.of(Role.USER, Role.ADMIN, Role.SUPER_ADMIN);

  @Column(nullable = false)
  private Boolean isEnable = true;
}
public enum Role implements GrantedAuthority {
  USER, ADMIN, SUPER_ADMIN;

  @Override
  public String getAuthority() {
    return name();
  }
}

安全配置

@Configuration
public class SecurityConfig {
  @Value("${jwt.public.key}")
  RSAPublicKey publicKey;

  @Value("${jwt.private.key}")
  RSAPrivateKey privateKey;

  @Bean
  PasswordEncoder encoder() {
    return new BCryptPasswordEncoder(10);
  }

  @Bean
  public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
    http.csrf().disable();
    http.authorizeHttpRequests().requestMatchers("/login", "/register").permitAll();
    http.authorizeHttpRequests().requestMatchers("/hello").hasAuthority("USER");
    http.httpBasic(Customizer.withDefaults());
    http.oauth2ResourceServer(OAuth2ResourceServerConfigurer::jwt);
    http.sessionManagement(session ->
            session.sessionCreationPolicy(STATELESS));
    http.exceptionHandling()
            .authenticationEntryPoint(new BearerTokenAuthenticationEntryPoint())
            .accessDeniedHandler(new BearerTokenAccessDeniedHandler());

    return http.build();
  }

  @Bean
  JwtDecoder jwtDecoder() {
    return NimbusJwtDecoder.withPublicKey(publicKey).build();
  }

  @Bean
  JwtEncoder jwtEncoder() {
    JWK jwk = new RSAKey.Builder(publicKey).privateKey(privateKey).build();
    JWKSource<SecurityContext> jwkSource = new ImmutableJWKSet<>(new JWKSet(jwk));
    return new NimbusJwtEncoder(jwkSource);
  }
}

问题排查与解决方案

核心问题在于JWT令牌未正确包含用户权限信息,或Spring Security未从JWT中正确解析权限。使用OAuth2 Resource Server的JWT模式时,Spring Security不会自动调用UserDetailsService加载权限,而是直接从JWT声明中提取。

1. 确保JWT生成时包含权限声明

在登录接口生成JWT时,必须将用户角色/权限写入令牌的自定义声明(如authorities):

@Autowired
private JwtEncoder jwtEncoder;

public String generateToken(UserDetails userDetails) {
    List<String> authorityStrings = userDetails.getAuthorities().stream()
            .map(GrantedAuthority::getAuthority)
            .toList();

    JwtClaimsSet claims = JwtClaimsSet.builder()
            .issuer("your-app")
            .issuedAt(Instant.now())
            .expiresAt(Instant.now().plusMinutes(60))
            .subject(userDetails.getUsername())
            .claim("authorities", authorityStrings) // 关键:写入权限列表
            .build();

    return jwtEncoder.encode(JwtEncoderParameters.from(claims)).getTokenValue();
}

2. 配置Spring Security解析JWT权限

默认情况下,Spring Security从scope/scp声明提取权限并添加SCOPE_前缀。若权限存在authorities声明中,需自定义转换器:

@Bean
public JwtAuthenticationConverter jwtAuthenticationConverter() {
    JwtGrantedAuthoritiesConverter grantedAuthoritiesConverter = new JwtGrantedAuthoritiesConverter();
    grantedAuthoritiesConverter.setAuthoritiesClaimName("authorities"); // 指定权限声明名
    grantedAuthoritiesConverter.setAuthorityPrefix(""); // 关闭默认前缀(若不需要)

    JwtAuthenticationConverter converter = new JwtAuthenticationConverter();
    converter.setJwtGrantedAuthoritiesConverter(grantedAuthoritiesConverter);
    return converter;
}

在SecurityFilterChain中绑定转换器:

http.oauth2ResourceServer(oauth2 -> oauth2
        .jwt(jwt -> jwt.jwtAuthenticationConverter(jwtAuthenticationConverter())));

3. 权限匹配验证

  • 使用hasAuthority("USER")时,确保JWT中权限字符串为USER(无前缀);
  • 使用hasRole("USER")时,需权限字符串为ROLE_USER,可设置grantedAuthoritiesConverter.setAuthorityPrefix("ROLE_"),或生成JWT时主动添加前缀。

4. 其他排查点

  • 检查数据库中用户roles集合是否存储正确,无大小写或格式错误;
  • 用JWT解析工具(如jwt.io)验证令牌中authorities声明是否存在且内容正确;
  • 确认UserDetailsImpl.getAuthorities()返回的角色集合非空且格式正确。

内容的提问来源于stack exchange,提问作者StrahSvid19

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.09 13:15:29