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

如何在Spring Authorization Server 1.0.0中动态注册客户端?

Spring Authorization Server 1.0.0 客户端动态注册实现方案

Spring Authorization Server 1.0.0 本身未内置标准的客户端动态注册端点,但可以基于框架提供的核心组件自定义实现,以下是具体步骤:


1. 配置可动态存储的客户端仓库

默认的InMemoryClientRegistrationRepository仅支持内存存储,无法持久化动态添加的客户端。推荐使用官方提供的JdbcRegisteredClientRepository(基于数据库存储),或自定义实现RegisteredClientRepository接口。

方式一:使用官方JDBC仓库

引入Spring JDBC依赖后,直接配置Bean即可:

@Bean
public RegisteredClientRepository registeredClientRepository(JdbcTemplate jdbcTemplate) {
    return new JdbcRegisteredClientRepository(jdbcTemplate);
}

同时需要初始化官方提供的数据库表结构(可从Spring Authorization Server的GitHub仓库获取DDL脚本,包含oauth2_registered_client等核心表)。

方式二:自定义仓库(可选)

如果需要特殊存储逻辑,可自行实现RegisteredClientRepository接口,示例如下:

@Repository
public class CustomRegisteredClientRepository implements RegisteredClientRepository {
    private final JdbcTemplate jdbcTemplate;

    public CustomRegisteredClientRepository(JdbcTemplate jdbcTemplate) {
        this.jdbcTemplate = jdbcTemplate;
    }

    @Override
    public void save(RegisteredClient registeredClient) {
        // 实现插入/更新数据库逻辑,需将RegisteredClient对象序列化为数据库字段
        String sql = "INSERT INTO oauth2_registered_client (id, client_id, client_secret, client_name, redirect_uris, scopes) VALUES (?, ?, ?, ?, ?, ?) ON DUPLICATE KEY UPDATE ...";
        jdbcTemplate.update(sql,
            registeredClient.getId(),
            registeredClient.getClientId(),
            registeredClient.getClientSecret(),
            registeredClient.getClientName(),
            String.join(",", registeredClient.getRedirectUris()),
            String.join(",", registeredClient.getScopes())
        );
    }

    @Override
    public RegisteredClient findById(String id) {
        // 根据ID查询并反序列化为RegisteredClient对象
        String sql = "SELECT * FROM oauth2_registered_client WHERE id = ?";
        return jdbcTemplate.queryForObject(sql, (rs, rowNum) -> 
            RegisteredClient.withId(rs.getString("id"))
                .clientId(rs.getString("client_id"))
                .clientSecret(rs.getString("client_secret"))
                .clientName(rs.getString("client_name"))
                .redirectUris(uris -> uris.addAll(Arrays.asList(rs.getString("redirect_uris").split(","))))
                .scopes(scopes -> scopes.addAll(Arrays.asList(rs.getString("scopes").split(","))))
                .build(), id);
    }

    @Override
    public RegisteredClient findByClientId(String clientId) {
        // 根据clientId查询逻辑
        String sql = "SELECT * FROM oauth2_registered_client WHERE client_id = ?";
        return jdbcTemplate.queryForObject(sql, (rs, rowNum) -> 
            RegisteredClient.withId(rs.getString("id"))
                .clientId(rs.getString("client_id"))
                .clientSecret(rs.getString("client_secret"))
                .build(), clientId);
    }
}

2. 自定义动态注册端点

创建REST接口接收客户端注册请求,调用RegisteredClientRepository完成客户端存储:

@RestController
@RequestMapping("/oauth2/register-client")
public class ClientRegistrationController {
    private final RegisteredClientRepository clientRepository;
    private final PasswordEncoder passwordEncoder;

    public ClientRegistrationController(RegisteredClientRepository clientRepository, PasswordEncoder passwordEncoder) {
        this.clientRepository = clientRepository;
        this.passwordEncoder = passwordEncoder;
    }

    @PostMapping
    public ResponseEntity<RegisteredClient> registerClient(@RequestBody ClientRegRequest request) {
        // 参数验证
        if (request.getClientName() == null || request.getRedirectUris().isEmpty()) {
            return ResponseEntity.badRequest().body(null);
        }

        // 生成安全的客户端凭证
        String clientId = UUID.randomUUID().toString();
        String clientSecret = passwordEncoder.encode(UUID.randomUUID().toString());

        // 构建RegisteredClient对象
        RegisteredClient client = RegisteredClient.withId(UUID.randomUUID().toString())
            .clientId(clientId)
            .clientSecret(clientSecret)
            .clientName(request.getClientName())
            .clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC)
            .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
            .authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN)
            .redirectUris(uris -> uris.addAll(request.getRedirectUris()))
            .scopes(scopes -> scopes.addAll(request.getScopes()))
            .clientSettings(ClientSettings.builder().requireAuthorizationConsent(true).build())
            .tokenSettings(TokenSettings.builder().accessTokenTimeToLive(Duration.ofHours(1)).build())
            .build();

        // 保存客户端
        clientRepository.save(client);
        return ResponseEntity.ok(client);
    }

    // 自定义请求DTO
    public static class ClientRegRequest {
        private String clientName;
        private List<String> redirectUris;
        private List<String> scopes;

        // Getters & Setters
        public String getClientName() { return clientName; }
        public void setClientName(String clientName) { this.clientName = clientName; }
        public List<String> getRedirectUris() { return redirectUris; }
        public void setRedirectUris(List<String> redirectUris) { this.redirectUris = redirectUris; }
        public List<String> getScopes() { return scopes; }
        public void setScopes(List<String> scopes) { this.scopes = scopes; }
    }
}

同时需要配置密码编码器:

@Bean
public PasswordEncoder passwordEncoder() {
    return new BCryptPasswordEncoder();
}

3. 保护注册端点

动态注册接口不能公开访问,需在Spring Security配置中限制访问权限:

@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
    http
        .authorizeHttpRequests(auth -> auth
            .requestMatchers("/oauth2/register-client").hasRole("ADMIN") // 仅允许管理员角色访问
            .anyRequest().authenticated()
        )
        .formLogin(withDefaults());

    // 应用Spring Authorization Server默认安全配置
    OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(http);

    return http.build();
}

4. 测试动态注册

发送POST请求到/oauth2/register-client,请求体示例:

{
    "clientName": "动态注册客户端",
    "redirectUris": ["http://localhost:8081/login/oauth2/code/dynamic-client"],
    "scopes": ["openid", "profile"]
}

响应会返回包含clientId和clientSecret的客户端信息,后续即可使用该凭证进行OAuth2授权流程。


注意事项

  • 生产环境需启用HTTPS,避免客户端凭证泄露
  • 可根据业务需求扩展注册请求参数(如自定义令牌过期时间、授权同意开关等)
  • 需添加完善的参数校验、异常处理和日志记录

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.11 03:21:11