1、场景
最近照着《Spring Security实战》学习,学到第18章,使用Keycloak作为授权服务器,使用
org.springframework.boot:spring-boot-starter-oauth2-resource-server
实现资源服务器,调用资源服务器的接口返回403,具体错误信息如下:
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"
那就检查scope吧
2、检查scope
2.1 查看access_token(JWK)
到这个网址查看明文的令牌:JSON Web Tokens - jwt.io
看看 scope 都有哪些值。
2.2 资源服务配置
@Beanpublic SecurityFilterChain filterChain(HttpSecurity http) throws Exception {http.authorizeHttpRequests(authorize -> authorize
// .requestMatchers(HttpMethod.POST, "/workout/").hasAuthority("SCOPE_fitnessapp").requestMatchers(HttpMethod.POST, "/workout/").access(hasScope("fitnessapp")).requestMatchers(HttpMethod.DELETE, "/**").hasAuthority("fitnessadmin").anyRequest().authenticated()).oauth2ResourceServer(oauth2 -> oauth2.jwt(Customizer.withDefaults()));return http.build();}
上一步看到 scope 包含 fitnessapp,那就设置
access(hasScope("fitnessapp"))
注:实际上资源服务器不设置 scope 也是可以的,因为根本原因不在 scope!!!
3、根本原因以及解决办法
3.1 开启 Spring Security 的日志
在 application.yml 添加配置:
logging:level:org.springframework.security: DEBUG
再次调用接口,发现这样一段信息:
ExpressionAuthorizationDecision [granted=false, expressionAttribute=#workout.user == authentication.name]
出问题的代码就是下面这个方法:
@PreAuthorize("#workout.user == authentication.name")
public void saveWorkout(Workout workout) {workoutRepository.save(workout);
}
于是打印看认证后获取的用户名
String name = SecurityContextHolder.getContext().getAuthentication().getName();
System.out.println("### authentication.name=" + name);
果然,name不是预期的用户名,而是一段UUID字符串,问DeepSeek这是怎么回事,接着再细看JWK的明文,name 就是token中的 sub 字段的值。
3.2 解决办法
登录Keycloak控制台添加映射器,Client scopes -> fitnessapp -> Mappers
然后重新获取token,看看 sub 字段的值是不是对应的用户名,是的话就没问题了。
最后使用新的token重新调用资源服务器接口,返回200,调用成功!