Spring JWT认证使用.hasAuthority()返回insufficient scope错误排查
解决Spring JWT认证中insufficient scope权限验证失败问题
问题现象
调用受保护接口时返回insufficient_scope错误,日志显示认证通过但权限集合为空:
JwtAuthenticationToken [Principal=org.springframework.security.oauth2.jwt.Jwt@77bce91c, Credentials=[PROTECTED], Authenticated=true, Details=WebAuthenticationDetails [RemoteIpAddress=127.0.0.1, SessionId=null], Granted Authorities=[]] [WWW-Authenticate:"Bearer error="insufficient_scope", error_description="The request requires higher privileges than provided by the access token.", error_uri="https://tools.ietf.org/html/rfc6750#section-3.1"", ...]
代码问题分析
- 实体关联映射错误:
AppUser的@ManyToMany注解误用了@JoinColumn,多对多关系需要通过中间表关联,当前映射会导致权限查询异常。 - 权限获取方法名错误:
CustomUserDetailsService中调用appUser.getAuthority(),但实体中定义的权限集合字段是authorities,方法名不匹配导致无法获取权限。 - JWT权限存储格式不兼容:
JwtUtil直接将GrantedAuthority对象存入JWT的authorities声明,Spring Security默认无法解析该格式。 - JWT权限解析未自定义:Spring Security OAuth2资源服务器默认从
scope/scp字段解析权限,而我们的权限存在authorities字段,未配置对应转换器。 - 密钥不一致:
JwtUtil和JwtSecretProvider各自生成密钥,可能导致JWT解析异常(虽认证通过,但权限解析受影响)。
修复步骤
1. 修正实体关联映射
修改AppUser的多对多映射,使用@JoinTable定义中间表:
@Entity @Table(name="app_user") public class AppUser { // ... 其他字段 @NotNull @ManyToMany(cascade = CascadeType.ALL) @JoinTable( name = "user_authority", joinColumns = @JoinColumn(name = "app_user_id"), inverseJoinColumns = @JoinColumn(name = "authority_id") ) private Set<Authority> authorities = new HashSet<>(); // ... getter/setter }
修改Authority的多对一映射,移除不必要的级联操作:
@Entity @Table(name="authority") public class Authority { // ... 其他字段 @JsonBackReference @NotNull @ManyToOne @JoinColumn(name="app_user_id", columnDefinition = "INT NOT NULL") private AppUser appUser; // ... getter/setter }
2. 修正权限获取方法名
在CustomUserDetailsService中,将appUser.getAuthority()改为appUser.getAuthorities():
@Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { Optional<AppUser> optionalAppUser = appUserRepository.findByUsername(username); if (optionalAppUser.isEmpty()) throw new UsernameNotFoundException("User not found while trying to load user"); AppUser appUser = optionalAppUser.get(); List<GrantedAuthority> authorities = appUser.getAuthorities() .stream() .map(authority -> new SimpleGrantedAuthority(authority.getAuthority().name())) .collect(Collectors.toList()); return User.builder() .username(appUser.getUsername()) .password(appUser.getEncryptedPassword().replace("{bcrypt}", "")) .authorities(authorities) .build(); }
3. 调整JWT权限存储格式
修改JwtUtil,将权限转换为字符串列表存入JWT:
@Component public class JwtUtil { private final int expirationTime = 50 * 60 * 60 * 10; private final CustomUserDetailsService customUserDetailsService; private final byte[] secret; @Autowired public JwtUtil(CustomUserDetailsService customUserDetailsService, JwtSecretProvider jwtSecretProvider) { this.customUserDetailsService = customUserDetailsService; this.secret = jwtSecretProvider.getSecretKey(); } public String generateToken(String username) { UserDetails userDetails = customUserDetailsService.loadUserByUsername(username); return createToken(userDetails); } private String createToken(UserDetails userDetails) { // 将权限转换为字符串列表 List<String> authorityStrings = userDetails.getAuthorities() .stream() .map(GrantedAuthority::getAuthority) .collect(Collectors.toList()); return Jwts.builder() .setSubject(userDetails.getUsername()) .claim("authorities", authorityStrings) .setIssuedAt(new Date(System.currentTimeMillis())) .setExpiration(new Date(System.currentTimeMillis() + expirationTime)) .signWith(Keys.hmacShaKeyFor(secret), SignatureAlgorithm.HS256) .compact(); } // ... 其他方法 }
4. 配置JWT权限解析转换器
在SecurityConfig中添加自定义JwtAuthenticationConverter,指定从authorities字段解析权限:
@Configuration @EnableWebSecurity public class SecurityConfig { // ... 其他注入和方法 @Bean public JwtAuthenticationConverter jwtAuthenticationConverter() { JwtGrantedAuthoritiesConverter grantedAuthoritiesConverter = new JwtGrantedAuthoritiesConverter(); // 指定权限字段名 grantedAuthoritiesConverter.setAuthoritiesClaimName("authorities"); // 取消权限前缀(因为我们的权限是直接的ESSENTIAL/PREMIUM) grantedAuthoritiesConverter.setAuthorityPrefix(""); JwtAuthenticationConverter converter = new JwtAuthenticationConverter(); converter.setJwtGrantedAuthoritiesConverter(grantedAuthoritiesConverter); return converter; } @Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http.csrf(AbstractHttpConfigurer::disable) .formLogin(AbstractHttpConfigurer::disable) .authorizeHttpRequests(auth -> auth .requestMatchers(HttpMethod.POST, "/user").permitAll() .requestMatchers("/user/login").permitAll() .anyRequest().hasAnyAuthority("ESSENTIAL", "PREMIUM") ) .sessionManagement(sess -> sess.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) .oauth2ResourceServer(oauth2 -> oauth2 .jwt(jwt -> jwt .decoder(jwtDecoder()) .jwtAuthenticationConverter(jwtAuthenticationConverter()) ) ) .httpBasic(Customizer.withDefaults()); return http.build(); } }
5. 统一JWT密钥
确保JwtSecretProvider只生成一次密钥:
@Component public class JwtSecretProvider { private final byte[] secret; public JwtSecretProvider() { this.secret = Keys.secretKeyFor(SignatureAlgorithm.HS256).getEncoded(); } @Override public byte[] getSecretKey() { return secret; } }
验证
运行测试用例,此时JWT中的authorities字段会被正确解析为GrantedAuthority集合,接口调用时权限验证通过,不再返回insufficient_scope错误。
内容的提问来源于stack exchange,提问作者Patrick Kim
相关产品推荐
相关产品推荐

