Spring Boot Security中/oauth2/token空Scope返回全Scope的配置需求
解决Spring Boot Security OAuth2 Token接口默认返回全部Scope的问题
针对你遇到的/oauth2/token接口在client_credentials模式下,不传Scope就默认返回客户端所有权限的问题,不需要自定义过滤器(之前的过滤器没生效是因为OAuth2的请求处理逻辑有特定拦截链),可以通过以下两种方式实现需求:
方法一:修改Scope解析逻辑(轻量方案)
通过自定义OAuth2ClientCredentialsTokenEndpointFilter,重写Scope解析方法,当请求中无Scope参数时直接返回空集合,替代框架默认的"填充客户端所有Scope"逻辑:
import org.springframework.security.oauth2.server.authorization.web.OAuth2ClientCredentialsTokenEndpointFilter; import org.springframework.util.StringUtils; import javax.servlet.http.HttpServletRequest; import java.util.Collections; import java.util.Set; public class CustomClientCredentialsTokenFilter extends OAuth2ClientCredentialsTokenEndpointFilter { @Override protected Set<String> resolveScopes(HttpServletRequest request) { String scopeParam = request.getParameter("scope"); // 无Scope参数时返回空集合,而非默认的null if (!StringUtils.hasText(scopeParam)) { return Collections.emptySet(); } return super.resolveScopes(request); } }
然后在授权服务器配置中替换默认的Filter:
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.oauth2.server.authorization.config.annotation.web.configuration.OAuth2AuthorizationServerConfiguration; import org.springframework.security.web.SecurityFilterChain; @Configuration public class AuthorizationServerConfig { @Bean public SecurityFilterChain authorizationServerFilterChain(HttpSecurity http) throws Exception { // 应用OAuth2授权服务器默认配置 OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(http); // 替换默认的ClientCredentials Token端点Filter http.addFilterBefore(new CustomClientCredentialsTokenFilter(), org.springframework.security.web.authentication.AuthenticationFilter.class); return http.build(); } }
方法二:自定义认证Provider(灵活扩展方案)
如果需要更复杂的权限控制逻辑,可以自定义ClientCredentialsAuthenticationProvider,直接控制授权的Scope集合:
import org.springframework.security.authentication.AuthenticationProvider; import org.springframework.security.core.Authentication; import org.springframework.security.core.AuthenticationException; import org.springframework.security.oauth2.core.*; import org.springframework.security.oauth2.server.authorization.OAuth2Authorization; import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService; import org.springframework.security.oauth2.server.authorization.OAuth2TokenType; import org.springframework.security.oauth2.server.authorization.authentication.OAuth2ClientCredentialsAuthenticationToken; import org.springframework.security.oauth2.server.authorization.client.RegisteredClient; import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository; import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenGenerator; import org.springframework.util.CollectionUtils; import java.util.Collections; import java.util.Set; import java.util.stream.Collectors; public class CustomClientCredentialsAuthProvider implements AuthenticationProvider { private final RegisteredClientRepository registeredClientRepository; private final OAuth2AuthorizationService authorizationService; private final OAuth2TokenGenerator<? extends OAuth2Token> tokenGenerator; public CustomClientCredentialsAuthProvider(RegisteredClientRepository registeredClientRepository, OAuth2AuthorizationService authorizationService, OAuth2TokenGenerator<? extends OAuth2Token> tokenGenerator) { this.registeredClientRepository = registeredClientRepository; this.authorizationService = authorizationService; this.tokenGenerator = tokenGenerator; } @Override public Authentication authenticate(Authentication authentication) throws AuthenticationException { OAuth2ClientCredentialsAuthenticationToken authToken = (OAuth2ClientCredentialsAuthenticationToken) authentication; RegisteredClient registeredClient = registeredClientRepository.findByClientId(authToken.getPrincipal().toString()); if (registeredClient == null) { throw new OAuth2AuthenticationException(OAuth2ErrorCodes.INVALID_CLIENT); } // 核心逻辑:无请求Scope时授权空集合,否则只授权客户端已配置的请求Scope Set<String> requestedScopes = authToken.getScopes(); Set<String> authorizedScopes = CollectionUtils.isEmpty(requestedScopes) ? Collections.emptySet() : registeredClient.getScopes().stream() .filter(requestedScopes::contains) .collect(Collectors.toSet()); // 生成Token并保存授权信息 OAuth2TokenContext tokenContext = OAuth2TokenContext.builder() .registeredClient(registeredClient) .principal(authToken) .authorizedScopes(authorizedScopes) .tokenType(OAuth2TokenType.ACCESS_TOKEN) .build(); OAuth2Token accessToken = tokenGenerator.generate(tokenContext); if (accessToken == null) { throw new OAuth2AuthenticationException(OAuth2ErrorCodes.SERVER_ERROR); } OAuth2Authorization authorization = OAuth2Authorization.withRegisteredClient(registeredClient) .principalName(registeredClient.getClientId()) .authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS) .authorizedScopes(authorizedScopes) .token((OAuth2AccessToken) accessToken, metadata -> {}) .build(); authorizationService.save(authorization); return new OAuth2ClientCredentialsAuthenticationToken( registeredClient, null, authorizedScopes, accessToken); } @Override public boolean supports(Class<?> authentication) { return OAuth2ClientCredentialsAuthenticationToken.class.isAssignableFrom(authentication); } }
然后在配置中替换默认的Provider:
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.oauth2.server.authorization.config.annotation.web.configuration.OAuth2AuthorizationServerConfiguration; import org.springframework.security.oauth2.server.authorization.config.annotation.web.configurers.OAuth2AuthorizationServerConfigurer; import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository; import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenGenerator; import org.springframework.security.web.SecurityFilterChain; @Configuration public class AuthorizationServerConfig { @Bean public SecurityFilterChain authorizationServerFilterChain(HttpSecurity http, RegisteredClientRepository registeredClientRepository, OAuth2AuthorizationService authorizationService, OAuth2TokenGenerator<? extends OAuth2Token> tokenGenerator) throws Exception { OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(http); http.getConfigurer(OAuth2AuthorizationServerConfigurer.class) .tokenEndpoint(tokenEndpoint -> tokenEndpoint .authenticationProvider( new CustomClientCredentialsAuthProvider( registeredClientRepository, authorizationService, tokenGenerator ) ) ); return http.build(); } }
可选:确保JWT返回空Scope字段
如果需要JWT的Payload中明确返回空的scope数组(而非不包含该字段),可以自定义Token增强器:
import org.springframework.security.oauth2.server.authorization.token.JwtEncodingContext; import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenEnhancer; import java.util.Collections; public class EmptyScopeTokenEnhancer implements OAuth2TokenEnhancer { @Override public void enhance(JwtEncodingContext context) { if (context.getAuthorizedScopes().isEmpty()) { context.getClaims().claim("scope", Collections.emptyList()); } } }
然后配置Token生成器时添加该增强器:
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.oauth2.server.authorization.token.JwtGenerator; import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenGenerator; import org.springframework.security.oauth2.server.authorization.token.DelegatingOAuth2TokenGenerator; import com.nimbusds.jose.jwk.source.JWKSource; import com.nimbusds.jose.proc.SecurityContext; @Configuration public class TokenConfig { @Bean public OAuth2TokenGenerator<?> tokenGenerator(JWKSource<SecurityContext> jwkSource) { JwtGenerator jwtGenerator = new JwtGenerator(jwkSource); jwtGenerator.setJwtCustomizer(new EmptyScopeTokenEnhancer()); return new DelegatingOAuth2TokenGenerator(jwtGenerator); } }
验证效果
- 当请求不带
scope参数时,响应的scope字段为空,JWT解码后scope为空数组(或按配置显示); - 当请求传入合法
scope时,正常返回对应权限,JWT也包含指定Scope。
内容的提问来源于stack exchange,提问作者sujith M
相关产品推荐
相关产品推荐

