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

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"", ...]

代码问题分析

  1. 实体关联映射错误:AppUser的@ManyToMany注解误用了@JoinColumn,多对多关系需要通过中间表关联,当前映射会导致权限查询异常。
  2. 权限获取方法名错误:CustomUserDetailsService中调用appUser.getAuthority(),但实体中定义的权限集合字段是authorities,方法名不匹配导致无法获取权限。
  3. JWT权限存储格式不兼容:JwtUtil直接将GrantedAuthority对象存入JWT的authorities声明,Spring Security默认无法解析该格式。
  4. JWT权限解析未自定义:Spring Security OAuth2资源服务器默认从scope/scp字段解析权限,而我们的权限存在authorities字段,未配置对应转换器。
  5. 密钥不一致: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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.12 02:35:55