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

如何在Spring OAuth中为客户端不同资源配置差异化Scope安全策略

Awesome question—this kind of granular resource-scoped permissioning is exactly what teams need when outgrowing the default OAuth2 setup. Let’s walk through how to implement this in Spring Security, mirroring AWS IAM’s resource-specific permission logic.

1. Extend Client Details Storage (Core Change)

The default OAUTH_CLIENT_DETAILS table treats resource_ids and scope as flat lists, which can’t link specific scopes to specific resources. We need to add a join table to track this relationship, then customize how Spring loads client details.

Step 1: Add a New Database Table

Create a CLIENT_RESOURCE_SCOPES table to map clients to resources and their allowed scopes:

CREATE TABLE client_resource_scopes (
    client_id VARCHAR(255) NOT NULL,
    resource_id VARCHAR(255) NOT NULL,
    scopes VARCHAR(255) NOT NULL, -- e.g., "user,admin"
    PRIMARY KEY (client_id, resource_id),
    FOREIGN KEY (client_id) REFERENCES oauth_client_details(client_id)
);

For your example, insert:

INSERT INTO client_resource_scopes (client_id, resource_id, scopes)
VALUES ('client1', 'resource1', 'user'), ('client1', 'resource2', 'admin,user');

Step 2: Customize ClientDetails Class

Create a custom ClientDetails implementation to hold the resource-to-scope mapping:

public class CustomClientDetails extends BaseClientDetails {
    private Map<String, Set<String>> resourceScopes; // Key: resource ID, Value: allowed scopes

    // Getters and setters
    public Map<String, Set<String>> getResourceScopes() {
        return resourceScopes;
    }

    public void setResourceScopes(Map<String, Set<String>> resourceScopes) {
        this.resourceScopes = resourceScopes;
    }
}

Step 3: Override ClientDetailsService

Extend the default JDBC client details service to load the resource-specific scopes:

@Service
public class CustomJdbcClientDetailsService extends JdbcClientDetailsService {
    private final JdbcTemplate jdbcTemplate;

    public CustomJdbcClientDetailsService(DataSource dataSource) {
        super(dataSource);
        this.jdbcTemplate = new JdbcTemplate(dataSource);
    }

    @Override
    public ClientDetails loadClientByClientId(String clientId) throws ClientRegistrationException {
        // Load base client details first
        CustomClientDetails clientDetails = jdbcTemplate.queryForObject(
            "SELECT client_id, client_secret, scope, authorized_grant_types, " +
            "web_server_redirect_uri, authorities, access_token_validity, " +
            "refresh_token_validity, additional_information, autoapprove " +
            "FROM oauth_client_details WHERE client_id = ?",
            new Object[]{clientId},
            (rs, rowNum) -> {
                CustomClientDetails details = new CustomClientDetails();
                details.setClientId(rs.getString("client_id"));
                details.setClientSecret(rs.getString("client_secret"));
                // Populate other default fields...
                return details;
            }
        );

        // Load resource-specific scopes from the join table
        List<Map<String, Object>> resourceScopeRows = jdbcTemplate.queryForList(
            "SELECT resource_id, scopes FROM client_resource_scopes WHERE client_id = ?",
            clientId
        );

        Map<String, Set<String>> resourceScopes = new HashMap<>();
        for (Map<String, Object> row : resourceScopeRows) {
            String resourceId = (String) row.get("resource_id");
            String scopesStr = (String) row.get("scopes");
            Set<String> scopes = new HashSet<>(Arrays.asList(scopesStr.split(",")));
            resourceScopes.put(resourceId, scopes);
        }
        clientDetails.setResourceScopes(resourceScopes);
        return clientDetails;
    }
}

2. Implement Resource-Specific Scope Validation

Next, we need to validate that the client’s token has the correct scope for the resource being accessed. Customize the token validation logic:

Step 1: Create a Custom Scope Validator

@Component
public class ResourceSpecificScopeValidator implements OAuth2TokenValidator<OAuth2TokenValidationResult> {
    @Override
    public OAuth2TokenValidationResult validate(OAuth2TokenValidationContext context) {
        OAuth2Token token = context.getToken();
        if (!(token instanceof OAuth2AccessToken accessToken)) {
            return OAuth2TokenValidationResult.success();
        }

        // Get the current resource ID (we'll extract this from the request next)
        String currentResourceId = (String) context.getHttpRequest().getAttribute("current_resource_id");
        if (currentResourceId == null) {
            return OAuth2TokenValidationResult.failure(
                new OAuth2Error("invalid_request", "Could not identify target resource", null)
            );
        }

        // Fetch custom client details
        String clientId = accessToken.getClaimAsString("client_id");
        ClientDetails clientDetails = context.getRegisteredClientRepository().findById(clientId);
        if (!(clientDetails instanceof CustomClientDetails customClientDetails)) {
            return OAuth2TokenValidationResult.failure(
                new OAuth2Error("invalid_client", "Client details not found", null)
            );
        }

        // Check if the client has allowed scopes for this resource
        Set<String> allowedScopes = customClientDetails.getResourceScopes().get(currentResourceId);
        if (allowedScopes == null || allowedScopes.isEmpty()) {
            return OAuth2TokenValidationResult.failure(
                new OAuth2Error("insufficient_scope", "No scopes allowed for this resource", null)
            );
        }

        // Verify token has at least one valid scope for the resource
        Set<String> tokenScopes = accessToken.getScopes();
        boolean hasValidScope = tokenScopes.stream().anyMatch(allowedScopes::contains);
        if (!hasValidScope) {
            return OAuth2TokenValidationResult.failure(
                new OAuth2Error("insufficient_scope", "Invalid scope for target resource", null)
            );
        }

        return OAuth2TokenValidationResult.success();
    }
}

Step 2: Extract the Current Resource ID

Add a filter to identify which resource the request is targeting (e.g., via URL path):

@Component
public class ResourceIdExtractorFilter extends OncePerRequestFilter {
    private final Map<String, String> pathToResourceMap = Map.of(
        "/api/resource1/**", "resource1",
        "/api/resource2/**", "resource2"
    );

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        String requestPath = request.getRequestURI();
        String resourceId = pathToResourceMap.entrySet().stream()
            .filter(entry -> new AntPathMatcher().match(entry.getKey(), requestPath))
            .map(Map.Entry::getValue)
            .findFirst()
            .orElse(null);

        if (resourceId != null) {
            request.setAttribute("current_resource_id", resourceId);
        }
        filterChain.doFilter(request, response);
    }
}

Step 3: Register Validator in Resource Server

Update your resource server config to use the custom validator:

@Configuration
@EnableResourceServer
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {
    private final ResourceSpecificScopeValidator scopeValidator;
    private final ResourceIdExtractorFilter resourceIdFilter;

    public ResourceServerConfig(ResourceSpecificScopeValidator scopeValidator, ResourceIdExtractorFilter resourceIdFilter) {
        this.scopeValidator = scopeValidator;
        this.resourceIdFilter = resourceIdFilter;
    }

    @Override
    public void configure(HttpSecurity http) throws Exception {
        http.addFilterBefore(resourceIdFilter, UsernamePasswordAuthenticationFilter.class)
            .authorizeRequests()
            .antMatchers("/api/**").authenticated();
    }

    @Override
    public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
        resources.tokenServices(customTokenServices());
    }

    @Bean
    public ResourceServerTokenServices customTokenServices() {
        DefaultTokenServices tokenServices = new DefaultTokenServices();
        tokenServices.setTokenStore(jdbcTokenStore()); // Use your existing token store
        tokenServices.setTokenValidators(List.of(scopeValidator));
        return tokenServices;
    }

    @Bean
    public TokenStore jdbcTokenStore(DataSource dataSource) {
        return new JdbcTokenStore(dataSource);
    }
}

3. Bonus: Method-Level Security

For even finer control, add a custom SpEL expression to check scopes per resource in your controllers:

@Component
public class CustomSecurityExpressionRoot extends SecurityExpressionRoot implements MethodSecurityExpressionOperations {
    public CustomSecurityExpressionRoot(Authentication authentication) {
        super(authentication);
    }

    public boolean hasScopeForResource(String resourceId, String scope) {
        OAuth2Authentication auth = (OAuth2Authentication) getAuthentication();
        CustomClientDetails clientDetails = (CustomClientDetails) auth.getOAuth2Request().getClientDetails();
        Set<String> allowedScopes = clientDetails.getResourceScopes().get(resourceId);
        return allowedScopes != null && allowedScopes.contains(scope);
    }
}

Then use it in your controller:

@RestController
@RequestMapping("/api/resource1")
public class Resource1Controller {
    @GetMapping("/users")
    @PreAuthorize("@customSecurityExpressionRoot.hasScopeForResource('resource1', 'user')")
    public List<User> getUsers() {
        // Your logic here
    }
}

内容的提问来源于stack exchange,提问作者Joey Trang

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.27 04:28:15