Spring Authorization Server 动态注册OAuthClient
站长
· 阅读数 9
Spring Authorization Server
到目前为止,我注册OAuth客户端的时候,都是在代码里写死的。在实际生产环境中这样肯定是不行的,往往要按需动态添加OAuth客户端,提供给不同业务方去使用。很幸运的是Spring Authorization Server本身内置了该功能,不过需要我们配置一番。
1.注册配置
首先需要编写一个配置生成类:
/**
* @author hundanli
* @version 1.0.0
* @date 2024/3/12 9:50
*/
@Configuration
public class ClientRegistrationConfig {
@Autowired
private RegisteredClientRepository registeredClientRepository;
@Autowired
private PasswordEncoder passwordEncoder;
@PostConstruct
private void initClientRegistrationClient() {
String clientId = "registrar";
RegisteredClient registrarClient = RegisteredClient.withId(UUID.randomUUID().toString())
.clientId(clientId)
.clientSecret(passwordEncoder.encode("123456"))
.clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC)
.authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS)
.scope("client.create")
.scope("client.read")
.build();
if (registeredClientRepository.findByClientId(clientId) != null) {
return;
}
registeredClientRepository.save(registrarClient);
}
public static Consumer<List<AuthenticationProvider>> configureCustomClientMetadataConverters() {
List<String> customClientMetadata = Arrays.asList("logo_uri", "contacts");
return (authenticationProviders) -> {
CustomRegisteredClientConverter registeredClientConverter =
new CustomRegisteredClientConverter(customClientMetadata);
CustomClientRegistrationConverter clientRegistrationConverter =
new CustomClientRegistrationConverter(customClientMetadata);
authenticationProviders.forEach((authenticationProvider) -> {
if (authenticationProvider instanceof OidcClientRegistrationAuthenticationProvider) {
OidcClientRegistrationAuthenticationProvider provider = (OidcClientRegistrationAuthenticationProvider) authenticationProvider;
provider.setRegisteredClientConverter(registeredClientConverter);
provider.setClientRegistrationConverter(clientRegistrationConverter);
}
if (authenticationProvider instanceof OidcClientConfigurationAuthenticationProvider) {
OidcClientConfigurationAuthenticationProvider provider = (OidcClientConfigurationAuthenticationProvider) authenticationProvider;
provider.setClientRegistrationConverter(clientRegistrationConverter);
}
});
};
}
private static class CustomRegisteredClientConverter
implements Converter<OidcClientRegistration, RegisteredClient> {
private final List<String> customClientMetadata;
private final OidcClientRegistrationRegisteredClientConverter delegate;
private CustomRegisteredClientConverter(List<String> customClientMetadata) {
this.customClientMetadata = customClientMetadata;
this.delegate = new OidcClientRegistrationRegisteredClientConverter();
}
@Override
public RegisteredClient convert(OidcClientRegistration clientRegistration) {
RegisteredClient registeredClient = this.delegate.convert(clientRegistration);
ClientSettings.Builder clientSettingsBuilder = ClientSettings.withSettings(
registeredClient.getClientSettings().getSettings());
if (!CollectionUtils.isEmpty(this.customClientMetadata)) {
clientRegistration.getClaims().forEach((claim, value) -> {
if (this.customClientMetadata.contains(claim)) {
clientSettingsBuilder.setting(claim, value);
}
});
}
return RegisteredClient.from(registeredClient)
.clientSettings(clientSettingsBuilder.build())
.build();
}
}
private static class CustomClientRegistrationConverter
implements Converter<RegisteredClient, OidcClientRegistration> {
private final List<String> customClientMetadata;
private final RegisteredClientOidcClientRegistrationConverter delegate;
private CustomClientRegistrationConverter(List<String> customClientMetadata) {
this.customClientMetadata = customClientMetadata;
this.delegate = new RegisteredClientOidcClientRegistrationConverter();
}
@Override
public OidcClientRegistration convert(RegisteredClient registeredClient) {
OidcClientRegistration clientRegistration = this.delegate.convert(registeredClient);
Map<String, Object> claims = new HashMap<>(clientRegistration.getClaims());
if (!CollectionUtils.isEmpty(this.customClientMetadata)) {
ClientSettings clientSettings = registeredClient.getClientSettings();
claims.putAll(this.customClientMetadata.stream()
.filter(metadata -> clientSettings.getSetting(metadata) != null)
.collect(Collectors.toMap(Function.identity(), clientSettings::getSetting)));
}
return OidcClientRegistration.withClaims(claims).build();
}
}
}
这里面干的事情是,定义了RegisteredClient和OidcClientRegistration两个类型之间互相转换的转换器,然后提供了一个静态方法,将这两个转换器配置到认证处理器中去,最后再往数据库中注册一个用户动态创建OidcClient的OidcClient客户端,他的授权类型是client_credentials类型,这样就可以通过用这个初始客户端的client_id和client_secret去调用AuthServer的API去创建其他OidcClient。
2.授权配置
在授权配置中需要调用上述静态方法去配置clientRegistration端点:
/**
* 授权服务器端点配置
*/
@Bean
@Order(1)
public SecurityFilterChain authorizationServerSecurityFilterChain(
HttpSecurity http,
OAuth2AuthorizationService authorizationService,
OAuth2TokenGenerator<?> tokenGenerator,
RegisteredClientRepository registeredClientRepository,
UserDetailsService userDetailsService,
PasswordEncoder passwordEncoder) throws Exception {
// 配置默认的设置,忽略认证端点的csrf校验
OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(http);
// 开启OpenID Connect 1.0协议相关端点
http.getConfigurer(OAuth2AuthorizationServerConfigurer.class)
.oidc(Customizer.withDefaults());
// 开启动态客户端注册
http.getConfigurer(OAuth2AuthorizationServerConfigurer.class)
.oidc(oidc -> oidc.clientRegistrationEndpoint(clientRegistrationEndpoint -> {
clientRegistrationEndpoint
.authenticationProviders(ClientRegistrationConfig.configureCustomClientMetadataConverters());
}));
// ...
// 当未登录时访问认证端点时重定向至login页面
http.exceptionHandling((exceptions) -> exceptions
.defaultAuthenticationEntryPointFor(
new LoginUrlAuthenticationEntryPoint("/login"),
new MediaTypeRequestMatcher(MediaType.TEXT_HTML)
))
// 处理使用access token访问用户信息端点和客户端注册端点
.oauth2ResourceServer((resourceServer) -> resourceServer
.jwt(Customizer.withDefaults()));
return http.build();
}
完成以上配置后就可以启动AuthServer了。
3.动态注册
接下来我们就可以调用/connect/register接口去动态添加客户端了,以下是先获取access_token再注册client的示例:
/**
* @author hundanli
* @version 1.0.0
* @date 2024/3/12 10:49
*/
public class ClientRegistrarTests {
private final WebClient webClient = WebClient.create("http://127.0.0.1:8000");
static class ClientRegistrationRequest {
@JsonProperty("client_name")
String clientName;
@JsonProperty("grant_types")
List<String> grantTypes;
@JsonProperty("redirect_uris")
List<String> redirectUris;
@JsonProperty("logo_uri")
String logoUri;
@JsonProperty("contacts")
List<String> contacts;
@JsonProperty("scope")
String scope;
public ClientRegistrationRequest(String clientName, List<String> grantTypes, List<String> redirectUris, String logoUri, List<String> contacts, String scope) {
this.clientName = clientName;
this.grantTypes = grantTypes;
this.redirectUris = redirectUris;
this.logoUri = logoUri;
this.contacts = contacts;
this.scope = scope;
}
}
static class ClientRegistrationResponse {
@JsonProperty("registration_access_token")
String registrationAccessToken;
@JsonProperty("registration_client_uri")
String registrationClientUri;
@JsonProperty("client_name")
String clientName;
@JsonProperty("client_id")
String clientId;
@JsonProperty("client_secret")
String clientSecret;
@JsonProperty("grant_types")
List<String> grantTypes;
@JsonProperty("redirect_uris")
List<String> redirectUris;
@JsonProperty("logo_uri")
String logoUri;
@JsonProperty("contacts")
List<String> contacts;
@JsonProperty("scope")
String scope;
public ClientRegistrationResponse(String registrationAccessToken, String registrationClientUri, String clientName, String clientId, String clientSecret, List<String> grantTypes, List<String> redirectUris, String logoUri, List<String> contacts, String scope) {
this.registrationAccessToken = registrationAccessToken;
this.registrationClientUri = registrationClientUri;
this.clientName = clientName;
this.clientId = clientId;
this.clientSecret = clientSecret;
this.grantTypes = grantTypes;
this.redirectUris = redirectUris;
this.logoUri = logoUri;
this.contacts = contacts;
this.scope = scope;
}
}
static class AccessToken {
@JsonProperty("access_token")
String accessToken;
@JsonProperty("token_type")
String tokenType;
@JsonProperty("expires_in")
Integer expiresIn;
@JsonProperty("scope")
String scope;
public AccessToken(String accessToken, String tokenType, Integer expiresIn, String scope) {
this.accessToken = accessToken;
this.tokenType = tokenType;
this.expiresIn = expiresIn;
this.scope = scope;
}
}
@Test
public void exampleRegistration() {
String clientId = "registrar";
String clientSecret = "123456";
MultiValueMap<String, String> formMap = new LinkedMultiValueMap<>();
formMap.add("grant_type", "client_credentials");
formMap.add("scope", "client.create");
String basicAuth = Base64.getEncoder().encodeToString((clientId + ":" + clientSecret).getBytes(StandardCharsets.UTF_8));
// 获取access_token
AccessToken accessToken = webClient.post().uri("/oauth2/token")
.header("Authorization", "Basic " + basicAuth)
.header("Content-Type", MediaType.MULTIPART_FORM_DATA_VALUE)
.body(BodyInserters.fromFormData(formMap))
.retrieve()
.bodyToMono(AccessToken.class)
.block();
assert accessToken != null;
String initialAccessToken = accessToken.accessToken;
// (3)
ClientRegistrationRequest clientRegistrationRequest = new ClientRegistrationRequest( // (4)
"client-1",
Collections.singletonList(AuthorizationGrantType.AUTHORIZATION_CODE.getValue()),
Arrays.asList("https://client.example.org/callback", "https://client.example.org/callback2"),
"https://client.example.org/logo",
Arrays.asList("contact-1", "contact-2"),
"openid email profile"
);
ClientRegistrationResponse clientRegistrationResponse =
registerClient(initialAccessToken, clientRegistrationRequest);
// (5)
assert (clientRegistrationResponse.clientName.contentEquals("client-1"));
// (6)
assert (!Objects.isNull(clientRegistrationResponse.clientSecret));
assert (clientRegistrationResponse.scope.contains("openid"));
assert (clientRegistrationResponse.scope.contains("profile"));
assert (clientRegistrationResponse.scope.contains("email"));
assert (clientRegistrationResponse.grantTypes.contains(AuthorizationGrantType.AUTHORIZATION_CODE.getValue()));
assert (clientRegistrationResponse.redirectUris.contains("https://client.example.org/callback"));
assert (clientRegistrationResponse.redirectUris.contains("https://client.example.org/callback2"));
assert (!clientRegistrationResponse.registrationAccessToken.isEmpty());
assert (!clientRegistrationResponse.registrationClientUri.isEmpty());
assert (clientRegistrationResponse.logoUri.contentEquals("https://client.example.org/logo"));
assert (clientRegistrationResponse.contacts.size() == 2);
assert (clientRegistrationResponse.contacts.contains("contact-1"));
assert (clientRegistrationResponse.contacts.contains("contact-2"));
String registrationAccessToken = clientRegistrationResponse.registrationAccessToken;
// (7)
String registrationClientUri = clientRegistrationResponse.registrationClientUri;
ClientRegistrationResponse retrievedClient = retrieveClient(registrationAccessToken, registrationClientUri);
// (8)
assert (retrievedClient.clientName.contentEquals("client-1"));
// (9)
assert (!Objects.isNull(retrievedClient.clientId));
assert (!Objects.isNull(retrievedClient.clientSecret));
assert (clientRegistrationResponse.scope.contains("openid"));
assert (clientRegistrationResponse.scope.contains("profile"));
assert (clientRegistrationResponse.scope.contains("email"));
assert (retrievedClient.grantTypes.contains(AuthorizationGrantType.AUTHORIZATION_CODE.getValue()));
assert (retrievedClient.redirectUris.contains("https://client.example.org/callback"));
assert (retrievedClient.redirectUris.contains("https://client.example.org/callback2"));
assert (retrievedClient.logoUri.contentEquals("https://client.example.org/logo"));
assert (retrievedClient.contacts.size() == 2);
assert (retrievedClient.contacts.contains("contact-1"));
assert (retrievedClient.contacts.contains("contact-2"));
assert (Objects.isNull(retrievedClient.registrationAccessToken));
assert (!retrievedClient.registrationClientUri.isEmpty());
}
public ClientRegistrationResponse registerClient(String initialAccessToken, ClientRegistrationRequest request) {
// (10)
return this.webClient
.post()
.uri("/connect/register")
.contentType(MediaType.APPLICATION_JSON)
.accept(MediaType.APPLICATION_JSON)
.header(HttpHeaders.AUTHORIZATION, "Bearer " + initialAccessToken)
.body(Mono.just(request), ClientRegistrationRequest.class)
.retrieve()
.bodyToMono(ClientRegistrationResponse.class)
.block();
}
public ClientRegistrationResponse retrieveClient(String registrationAccessToken, String registrationClientUri) {
// (11)
return this.webClient
.get()
.uri(registrationClientUri)
.header(HttpHeaders.AUTHORIZATION, "Bearer " + registrationAccessToken)
.retrieve()
.bodyToMono(ClientRegistrationResponse.class)
.block();
}
}
执行这个exampleRegistration单元测试,顺利的话,就能看到数据库中添加了新的OAuth客户端了。