spring security 5 oauth2 资源服务器无法正确处理用户授权 报错insufficient_scope
现象
客户端通过授权码模式获取不透明令牌(opaque token),使用令牌访问资源服务器
资源服务器安全配置只能处理客户端scope授权,如果添加用户授权的判定规则,则报错
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”
原因
spring security 5 默认的令牌校验逻辑只处理scope,没有处理用户授权
源码
- 资源服务器不透明令牌默认配置
org.springframework.boot.autoconfigure.security.oauth2.resource.servlet.OAuth2ResourceServerOpaqueTokenConfiguration
@Configuration(proxyBeanMethods = false)
@ConditionalOnMissingBean(OpaqueTokenIntrospector.class)
static class OpaqueTokenIntrospectionClientConfiguration {
@Bean
@ConditionalOnProperty(name = "spring.security.oauth2.resourceserver.opaquetoken.introspection-uri")
NimbusOpaqueTokenIntrospector opaqueTokenIntrospector(OAuth2ResourceServerProperties properties) {
OAuth2ResourceServerProperties.Opaquetoken opaqueToken = properties.getOpaquetoken();
return new NimbusOpaqueTokenIntrospector(opaqueToken.getIntrospectionUri(), opaqueToken.getClientId(),
opaqueToken.getClientSecret());
}
}
- 内省和验证不透明令牌
org.springframework.security.oauth2.server.resource.introspection.NimbusOpaqueTokenIntrospector
private OAuth2AuthenticatedPrincipal convertClaimsSet(TokenIntrospectionSuccessResponse response) {
Collection<GrantedAuthority> authorities = new ArrayList<>();
Map<String, Object> claims = response.toJSONObject();
...
// 处理scope授权,加上前缀"SCOPE_"
if (response.getScope() != null) {
List<String> scopes = Collections.unmodifiableList(response.getScope().toStringList());
claims.put(OAuth2IntrospectionClaimNames.SCOPE, scopes);
for (String scope : scopes) {
authorities.add(new SimpleGrantedAuthority(this.authorityPrefix + scope));
}
}
// 授权中只包含scope,用户授权直接作为一般属性传递了
return new OAuth2IntrospectionAuthenticatedPrincipal(claims, authorities);
}
解决
- 自定义内省器
import com.nimbusds.oauth2.sdk.ParseException;
import com.nimbusds.oauth2.sdk.util.JSONUtils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.oauth2.core.OAuth2AuthenticatedPrincipal;
import org.springframework.security.oauth2.server.resource.introspection.NimbusOpaqueTokenIntrospector;
import org.springframework.security.oauth2.server.resource.introspection.OAuth2IntrospectionAuthenticatedPrincipal;
import org.springframework.web.client.RestOperations;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
@Slf4j
public class DefaultOpaqueTokenIntrospector extends NimbusOpaqueTokenIntrospector {
public DefaultOpaqueTokenIntrospector(String introspectionUri, String clientId, String clientSecret) {
super(introspectionUri, clientId, clientSecret);
}
public DefaultOpaqueTokenIntrospector(String introspectionUri, RestOperations restOperations) {
super(introspectionUri, restOperations);
}
@Override
public OAuth2AuthenticatedPrincipal introspect(String token) {
OAuth2AuthenticatedPrincipal principal = super.introspect(token);
try {
List<String> userAuthorities = JSONUtils.to(principal.getAttribute("authorities"), List.class);
Collection<GrantedAuthority> authorities = new ArrayList<>();
authorities.addAll(principal.getAuthorities());
for (String userAuthority : userAuthorities) {
authorities.add(new SimpleGrantedAuthority(userAuthority));
}
return new OAuth2IntrospectionAuthenticatedPrincipal(principal.getAttributes(), authorities);
} catch (ParseException e) {
log.warn("令牌用户授权信息解析失败", e);
}
return principal;
}
}
- 配置
@Bean
@ConditionalOnProperty(name = "spring.security.oauth2.resourceserver.opaquetoken.introspection-uri")
NimbusOpaqueTokenIntrospector opaqueTokenIntrospector(OAuth2ResourceServerProperties properties) {
OAuth2ResourceServerProperties.Opaquetoken opaqueToken = properties.getOpaquetoken();
return new DefaultOpaqueTokenIntrospector(opaqueToken.getIntrospectionUri(), opaqueToken.getClientId(),
opaqueToken.getClientSecret());
}
```