Spring授权服务:如何扩展RegisteredClient支持自定义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
相关产品推荐
相关产品推荐

