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

Spring授权服务:如何扩展RegisteredClient支持自定义app_id字段的动态注册?

扩展Spring Authorization Server动态客户端注册支持独立app_id字段的实现方案

核心思路是通过自定义RegisteredClient实体、重写JDBC存储逻辑、扩展注册请求处理链,实现app_id的接收、独立列存储与查询能力,完全避开将自定义字段塞进JSON格式的client_settings。

1. 修改数据库表结构

在oauth2_registered_client表新增app_id列,添加唯一索引保障查询效率:

ALTER TABLE oauth2_registered_client 
ADD COLUMN app_id VARCHAR(255) UNIQUE COMMENT '自定义客户端标识';

CREATE INDEX idx_oauth2_rc_app_id ON oauth2_registered_client(app_id);

2. 扩展RegisteredClient实体

创建自定义实现类,继承原RegisteredClient的所有属性并新增app_id,确保能携带该字段流转:

public class CustomRegisteredClient implements RegisteredClient {
    private final RegisteredClient delegate;
    private final String appId;

    private CustomRegisteredClient(Builder builder) {
        this.delegate = builder.delegateBuilder.build();
        this.appId = builder.appId;
    }

    // 新增app_id getter
    public String getAppId() {
        return appId;
    }

    // 委托实现RegisteredClient的所有接口方法
    @Override
    public String getId() { return delegate.getId(); }
    @Override
    public String getClientId() { return delegate.getClientId(); }
    @Override
    public Instant getClientIdIssuedAt() { return delegate.getClientIdIssuedAt(); }
    @Override
    public String getClientSecret() { return delegate.getClientSecret(); }
    @Override
    public Instant getClientSecretExpiresAt() { return delegate.getClientSecretExpiresAt(); }
    @Override
    public String getClientName() { return delegate.getClientName(); }
    @Override
    public Set<ClientAuthenticationMethod> getClientAuthenticationMethods() { return delegate.getClientAuthenticationMethods(); }
    @Override
    public Set<AuthorizationGrantType> getAuthorizationGrantTypes() { return delegate.getAuthorizationGrantTypes(); }
    @Override
    public Set<String> getRedirectUris() { return delegate.getRedirectUris(); }
    @Override
    public Set<String> getPostLogoutRedirectUris() { return delegate.getPostLogoutRedirectUris(); }
    @Override
    public Set<String> getScopes() { return delegate.getScopes(); }
    @Override
    public ClientSettings getClientSettings() { return delegate.getClientSettings(); }
    @Override
    public TokenSettings getTokenSettings() { return delegate.getTokenSettings(); }

    // 自定义Builder,复用原RegisteredClient的Builder逻辑
    public static Builder builder() {
        return new Builder();
    }

    public static class Builder {
        private final RegisteredClient.Builder delegateBuilder = RegisteredClient.withId(UUID.randomUUID().toString());
        private String appId;

        public Builder appId(String appId) {
            this.appId = appId;
            return this;
        }

        // 复用原Builder的所有方法
        public Builder clientId(String clientId) {
            delegateBuilder.clientId(clientId);
            return this;
        }

        public Builder clientSecret(String clientSecret) {
            delegateBuilder.clientSecret(clientSecret);
            return this;
        }

        // 按需添加其他必要的Builder方法(如clientName、scopes等)

        public CustomRegisteredClient build() {
            return new CustomRegisteredClient(this);
        }
    }
}

3. 自定义注册请求处理

3.1 扩展注册请求DTO

封装包含app_id的注册请求对象:

public class CustomOAuth2ClientRegistrationRequest extends OAuth2ClientRegistrationRequest {
    private final String appId;

    private CustomOAuth2ClientRegistrationRequest(Builder builder) {
        super(builder);
        this.appId = builder.appId;
    }

    public String getAppId() {
        return appId;
    }

    public static Builder builder() {
        return new Builder();
    }

    public static class Builder extends OAuth2ClientRegistrationRequest.Builder {
        private String appId;

        public Builder appId(String appId) {
            this.appId = appId;
            return this;
        }

        @Override
        public CustomOAuth2ClientRegistrationRequest build() {
            return new CustomOAuth2ClientRegistrationRequest(this);
        }
    }
}

3.2 实现请求转换器

将HTTP请求中的app_id参数绑定到自定义请求对象:

@Component
public class CustomClientRegistrationRequestConverter implements OAuth2ClientRegistrationRequestConverter {
    private final OAuth2ClientRegistrationRequestConverter defaultConverter = new DefaultOAuth2ClientRegistrationRequestConverter();

    @Override
    public OAuth2ClientRegistrationRequest convert(HttpServletRequest request) {
        OAuth2ClientRegistrationRequest defaultReq = defaultConverter.convert(request);
        String appId = request.getParameter("app_id");

        return CustomOAuth2ClientRegistrationRequest.builder()
                .clientId(defaultReq.getClientId())
                .clientSecret(defaultReq.getClientSecret())
                .clientName(defaultReq.getClientName())
                .clientAuthenticationMethods(defaultReq.getClientAuthenticationMethods())
                .authorizationGrantTypes(defaultReq.getAuthorizationGrantTypes())
                .redirectUris(defaultReq.getRedirectUris())
                .scopes(defaultReq.getScopes())
                .clientSettings(defaultReq.getClientSettings())
                .tokenSettings(defaultReq.getTokenSettings())
                .appId(appId)
                .build();
    }
}

4. 重写JDBC存储逻辑

继承默认的JdbcRegisteredClientRepository,扩展save逻辑以支持app_id的独立存储,并新增按app_id查询的方法:

@Repository
public class CustomJdbcRegisteredClientRepository extends JdbcRegisteredClientRepository {
    // 重写INSERT/UPDATE SQL,新增app_id字段
    private static final String INSERT_SQL = """
            INSERT INTO oauth2_registered_client (
                client_id, client_id_issued_at, client_secret, client_secret_expires_at, client_name,
                client_authentication_methods, authorization_grant_types, redirect_uris,
                post_logout_redirect_uris, scopes, client_settings, token_settings, app_id
            ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
            """;

    private static final String UPDATE_SQL = """
            UPDATE oauth2_registered_client SET
                client_id_issued_at = ?, client_secret = ?, client_secret_expires_at = ?, client_name = ?,
                client_authentication_methods = ?, authorization_grant_types = ?, redirect_uris = ?,
                post_logout_redirect_uris = ?, scopes = ?, client_settings = ?, token_settings = ?, app_id = ?
            WHERE client_id = ?
            """;

    // 新增按app_id查询的SQL
    private static final String SELECT_BY_APP_ID_SQL = """
            SELECT * FROM oauth2_registered_client WHERE app_id = ?
            """;

    private final JdbcOperations jdbcOperations;
    private final RegisteredClientRepositoryRowMapper rowMapper = new RegisteredClientRepositoryRowMapper();
    private final ObjectMapper objectMapper = new ObjectMapper();

    public CustomJdbcRegisteredClientRepository(JdbcOperations jdbcOperations) {
        super(jdbcOperations);
        this.jdbcOperations = jdbcOperations;
    }

    @Override
    public void save(RegisteredClient registeredClient) {
        Assert.notNull(registeredClient, "registeredClient cannot be null");
        String appId = null;
        if (registeredClient instanceof CustomRegisteredClient customClient) {
            appId = customClient.getAppId();
        }

        // 先尝试更新,无记录则插入
        int updateCount = jdbcOperations.update(UPDATE_SQL,
                registeredClient.getClientIdIssuedAt(),
                registeredClient.getClientSecret(),
                registeredClient.getClientSecretExpiresAt(),
                registeredClient.getClientName(),
                String.join(",", registeredClient.getClientAuthenticationMethods().stream().map(ClientAuthenticationMethod::getValue).toList()),
                String.join(",", registeredClient.getAuthorizationGrantTypes().stream().map(AuthorizationGrantType::getValue).toList()),
                String.join(",", registeredClient.getRedirectUris()),
                String.join(",", registeredClient.getPostLogoutRedirectUris()),
                String.join(",", registeredClient.getScopes()),
                objectMapper.writeValueAsString(registeredClient.getClientSettings().getSettings()),
                objectMapper.writeValueAsString(registeredClient.getTokenSettings().getSettings()),
                appId,
                registeredClient.getClientId()
        );

        if (updateCount == 0) {
            jdbcOperations.update(INSERT_SQL,
                    registeredClient.getClientId(),
                    registeredClient.getClientIdIssuedAt(),
                    registeredClient.getClientSecret(),
                    registeredClient.getClientSecretExpiresAt(),
                    registeredClient.getClientName(),
                    String.join(",", registeredClient.getClientAuthenticationMethods().stream().map(ClientAuthenticationMethod::getValue).toList()),
                    String.join(",", registeredClient.getAuthorizationGrantTypes().stream().map(AuthorizationGrantType::getValue).toList()),
                    String.join(",", registeredClient.getRedirectUris()),
                    String.join(",", registeredClient.getPostLogoutRedirectUris()),
                    String.join(",", registeredClient.getScopes()),
                    objectMapper.writeValueAsString(registeredClient.getClientSettings().getSettings()),
                    objectMapper.writeValueAsString(registeredClient.getTokenSettings().getSettings()),
                    appId
            );
        }
    }

    // 新增按app_id查询客户端的方法
    public RegisteredClient findByAppId(String appId) {
        Assert.hasText(appId, "appId cannot be empty");
        return jdbcOperations.queryForObject(SELECT_BY_APP_ID_SQL, rowMapper, appId);
    }
}

5. 自定义注册服务

实现OAuth2ClientRegistrationService,将请求中的app_id封装到CustomRegisteredClient后存储:

@Service
public class CustomOAuth2ClientRegistrationService implements OAuth2ClientRegistrationService {
    private final CustomJdbcRegisteredClientRepository clientRepository;

    public CustomOAuth2ClientRegistrationService(CustomJdbcRegisteredClientRepository clientRepository) {
        this.clientRepository = clientRepository;
    }

    @Override
    public RegisteredClient register(OAuth2ClientRegistrationRequest request) {
        CustomOAuth2ClientRegistrationRequest customReq = (CustomOAuth2ClientRegistrationRequest) request;

        CustomRegisteredClient client = CustomRegisteredClient.builder()
                .clientId(UUID.randomUUID().toString()) // 若允许客户端指定clientId,可替换为customReq.getClientId()
                .clientName(customReq.getClientName())
                .clientSecret(customReq.getClientSecret())
                .clientAuthenticationMethods(customReq.getClientAuthenticationMethods())
                .authorizationGrantTypes(customReq.getAuthorizationGrantTypes())
                .redirectUris(customReq.getRedirectUris())
                .scopes(customReq.getScopes())
                .clientSettings(customReq.getClientSettings())
                .tokenSettings(customReq.getTokenSettings())
                .appId(customReq.getAppId())
                .build();

        clientRepository.save(client);
        return client;
    }

    // 按需实现findById、update、delete等方法
    @Override
    public RegisteredClient findById(String id) {
        return clientRepository.findById(id);
    }

    @Override
    public void update(RegisteredClient registeredClient) {
        clientRepository.save(registeredClient);
    }

    @Override
    public void deleteById(String id) {
        clientRepository.deleteById(id);
    }
}

6. 配置Authorization Server

替换默认的请求转换器、注册服务与Repository:

@Configuration
@EnableAuthorizationServer
public class AuthorizationServerConfiguration {
    @Bean
    public OAuth2ClientRegistrationRequestConverter clientRegistrationRequestConverter() {
        return new CustomClientRegistrationRequestConverter();
    }

    @Bean
    public RegisteredClientRepository registeredClientRepository(JdbcOperations jdbcOperations) {
        return new CustomJdbcRegisteredClientRepository(jdbcOperations);
    }

    @Bean
    public OAuth2ClientRegistrationService clientRegistrationService(RegisteredClientRepository repository) {
        return new CustomOAuth2ClientRegistrationService((CustomJdbcRegisteredClientRepository) repository);
    }

    // 其他Authorization Server配置(如端点、安全规则等)
}

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.12 20:54:52