跳到主要内容

SAS-Opaque-Token

写在前面的话

因为 Spring Boot 3.x 是目前最新的版本,整合spring-security-oauth2-authorization-server的资料很少,所以产生了这篇文章,主要为想尝试Spring Boot高版本,想整合最新的spring-security-oauth2-authorization-server的初学者,旨在为大家提供一个简单上手的参考,如果哪里写得不对或可以优化的还请大家踊跃评论指正。

前面一篇文章《spring-security-oauth2-authorization-server(一)SpringBoot3.1.3整合》主要介绍了如何简单搭建一个认证服务器,这一篇算番外篇主要结合官网的描述简单分析一下Token的几种生成策略,以及如何自定义Token生成器来生成我们在OAuth2.0中常见的形如这样:Bearer 237d224d-1bdc-4d48-855a-f6abb37e378f的不透明OpaqueToken

整个项目的配置还是复用的上一篇。

Token的生成的官方描述

取自官方文档:

OAuth2TokenGenerator负责从所提供的OAuth2TokenContext中的信息生成 OAuth2Token,生成的 OAuth2Token 主要取决于在 OAuth2TokenContext 中指定的 OAuth2TokenType

OAuth2TokenTypevalue 为:

  • code,则生成 OAuth2AuthorizationCode
  • access_token,则生成 OAuth2AccessToken
  • refresh_token,则生成 OAuth2RefreshToken
  • id_token,则生成 OidcIdToken

所以我们用到的授权码,access-tokenrefresh-token,设备码等都是由OAuth2TokenGenerator的子类实现的。

1. JWT Token(透明Token

spring-security-oauth2-authorization-server默认生成的TokenJWT类型的,生成的 OAuth2AccessToken 的格式是不同的,取决于为 RegisteredClient 配置的 TokenSettings.getAccessTokenFormat()。如果格式是 OAuth2TokenFormat.SELF_CONTAINED(默认),那么就会生成一个 JWT。如果格式是 OAuth2TokenFormat.REFERENCE,那么就会生成一个 opaque不透明Token

官网上还有这么一句话:

OAuth2TokenGenerator 是一个可选的组件,默认为由 OAuth2AccessTokenGeneratorOAuth2RefreshTokenGenerator 组成的 DelegatingOAuth2TokenGenerator

如果注册了 JwtEncoder @BeanJWKSource @Bean,那么在 DelegatingOAuth2TokenGenerator 中还会额外组成一个 JwtGenerator

可以在源码找到出处。 DelegatingOAuth2TokenGenerator实例化的时候将tokenGenerators塞进去的,那么再往上找何处实例化的

img

img

因为我们注册了相应的JWKSourceJwtEncoderBean,所以Token的生成会实现在JwtGenerator上。

如果我们不注册JWT相关的Bean就是将上一篇文章中的3.3.5节的JWT相关Bean全部去掉,debug会发现还是默认加进来了JwtGenerator

img

原因应该是在我们在注册RegisteredClientRepository Bean时默认为我们指定了tokenSettings

img

所以oauth2_registered_client表的token_settings也会看到org.springframework.security.oauth2.server.authorization.settings.OAuth2TokenFormat类型为self-contained

img

2. Opaque Token(不透明Token

2.1 默认128位字符的Opaque Token

既然官网都告诉我们Token的形式是取决于为 RegisteredClient 配置的 TokenSettings.getAccessTokenFormat(),那我们改成OAuth2TokenFormat.REFERENCE 就好了。

先把上一篇文章中3.3.5节与JWT相关的Bean删掉,而后添加如下代码

.tokenSettings(TokenSettings.builder()
.accessTokenFormat(OAuth2TokenFormat.REFERENCE)
.build())
@Bean
public RegisteredClientRepository registeredClientRepository(JdbcTemplate jdbcTemplate, PasswordEncoder passwordEncoder) {
RegisteredClient registeredClient = RegisteredClient.withId(UUID.randomUUID().toString())
.clientId("oauth2-client")
.clientSecret(passwordEncoder.encode("123456"))
// 客户端认证基于请求头
.clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC)
// 配置授权的支持方式
.authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
.authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN)
.authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS)
.redirectUri("https://www.baidu.com")
.scope("user")
.scope("admin")
// 客户端设置,设置用户需要确认授权
.clientSettings(ClientSettings.builder().requireAuthorizationConsent(true).build())
// 添加tokenSettings,将accessTokenFormat改为REFERENCE即可获取Opaque Token
.tokenSettings(TokenSettings.builder().accessTokenFormat(OAuth2TokenFormat.REFERENCE).build())
.build();
JdbcRegisteredClientRepository registeredClientRepository = new JdbcRegisteredClientRepository(jdbcTemplate);
RegisteredClient repositoryByClientId = registeredClientRepository.findByClientId(registeredClient.getClientId());
if (repositoryByClientId == null) {
registeredClientRepository.save(registeredClient);
}
return registeredClientRepository;
}

img

生成Token的全类名如下: org.springframework.security.oauth2.server.authorization.token.OAuth2AccessTokenGenerator#generate

img

默认是Base64将一个96位长度的随机串编码后生成的128位字符串

img

我们发现此时的Token是一个极长的字符串,与我们在OAuth2.0生成Bearer 237d224d-1bdc-4d48-855a-f6abb37e378f相差甚远。 我们又发现accessTokenGenerator的设置是写死的,没有提供一个方法让我们重新设置,那么只能重写一个实现。

2.2 自定义Token生成器,生成一个UUID类型的OpaqueToken

官方文档说:

OAuth2TokenGenerator 提供了极大的灵活性,因为它可以为 access_tokenrefresh_token 支持任何自定义的 token 格式。

那我们就自定义一个UUIDOAuth2TokenGenerator,用UUID生成一个OpaqueToken。在此之前需要先定义一个StringKeyGenerator的实现,因为Token生成器需要用到this.accessTokenGenerator.generateKey()来生成串。

/**
* @author roshine
* @version 1.0.0
*/
public class UUIDKeyGenerator implements StringKeyGenerator {
@Override
public String generateKey() {
return UUID.randomUUID().toString().toLowerCase();
}
}

由于我们只需要将Token的生成改为UUID其他逻辑不变,所以将org.springframework.security.oauth2.server.authorization.token.OAuth2AccessTokenGenerator#generate 复刻一份,把多余的部分去掉,比如定制化器accessTokenCustomizer,如下所示:

/**
* @author roshine
* @version 1.0.0
*/
public class UUIDOAuth2TokenGenerator implements OAuth2TokenGenerator<OAuth2AccessToken> {
private final StringKeyGenerator accessTokenGenerator = new UUIDKeyGenerator();

@Override
public OAuth2AccessToken generate(OAuth2TokenContext context) {
if (!OAuth2TokenType.ACCESS_TOKEN.equals(context.getTokenType()) ||
!OAuth2TokenFormat.REFERENCE.equals(context.getRegisteredClient().getTokenSettings().getAccessTokenFormat())) {
return null;
}
String issuer = null;
if (context.getAuthorizationServerContext() != null) {
issuer = context.getAuthorizationServerContext().getIssuer();
}
RegisteredClient registeredClient = context.getRegisteredClient();

Instant issuedAt = Instant.now();
Instant expiresAt = issuedAt.plus(registeredClient.getTokenSettings().getAccessTokenTimeToLive());

// @formatter:off
OAuth2TokenClaimsSet.Builder claimsBuilder = OAuth2TokenClaimsSet.builder();
if (StringUtils.hasText(issuer)) {
claimsBuilder.issuer(issuer);
}
claimsBuilder
.subject(context.getPrincipal().getName())
.audience(Collections.singletonList(registeredClient.getClientId()))
.issuedAt(issuedAt)
.expiresAt(expiresAt)
.notBefore(issuedAt)
.id(UUID.randomUUID().toString());
if (!CollectionUtils.isEmpty(context.getAuthorizedScopes())) {
claimsBuilder.claim(OAuth2ParameterNames.SCOPE, context.getAuthorizedScopes());
}
OAuth2TokenClaimsSet accessTokenClaimsSet = claimsBuilder.build();

return new OAuth2AccessTokenClaims(OAuth2AccessToken.TokenType.BEARER,
this.accessTokenGenerator.generateKey(), accessTokenClaimsSet.getIssuedAt(), accessTokenClaimsSet.getExpiresAt(),
context.getAuthorizedScopes(), accessTokenClaimsSet.getClaims());
}

private static final class OAuth2AccessTokenClaims extends OAuth2AccessToken implements ClaimAccessor {
private final Map<String, Object> claims;

private OAuth2AccessTokenClaims(TokenType tokenType, String tokenValue,
Instant issuedAt, Instant expiresAt, Set<String> scopes, Map<String, Object> claims) {
super(tokenType, tokenValue, issuedAt, expiresAt, scopes);
this.claims = claims;
}

@Override
public Map<String, Object> getClaims() {
return this.claims;
}

}
}

然后在我们的AuthorizationServerConfig添加

    /**
* 自定义Token生成器
*
* @return OAuth2TokenGenerator
*/
@Bean
public OAuth2TokenGenerator<?> tokenGenerator() {
UUIDOAuth2TokenGenerator uuidoAuth2TokenGenerator = new UUIDOAuth2TokenGenerator();
return new DelegatingOAuth2TokenGenerator(uuidoAuth2TokenGenerator);
}

获取token时报错:

{
"error_description": "The token generator failed to generate the refresh token.",
"error": "server_error",
"error_uri": "https://datatracker.ietf.org/doc/html/rfc6749#section-5.2"
}

那我们就再定义一个UUIDOAuth2RefreshTokenGenerator来生成 refresh-token

/**
* @author roshine
* @version 1.0.0
* @date 2023-09-15 23:16
*/
public class UUIDOAuth2RefreshTokenGenerator implements OAuth2TokenGenerator<OAuth2RefreshToken> {

private final StringKeyGenerator refreshTokenGenerator = new UUIDKeyGenerator();

@Nullable
@Override
public OAuth2RefreshToken generate(OAuth2TokenContext context) {
if (!OAuth2TokenType.REFRESH_TOKEN.equals(context.getTokenType())) {
return null;
}
Instant issuedAt = Instant.now();
Instant expiresAt = issuedAt.plus(context.getRegisteredClient().getTokenSettings().getRefreshTokenTimeToLive());
return new OAuth2RefreshToken(this.refreshTokenGenerator.generateKey(), issuedAt, expiresAt);
}
}

tokenGenerator()里添加上UUIDOAuth2RefreshTokenGenerator

/**
* 自定义Token生成器
*
* @return OAuth2TokenGenerator
*/
@Bean
public OAuth2TokenGenerator<?> tokenGenerator() {
UUIDOAuth2TokenGenerator uuidoAuth2TokenGenerator = new UUIDOAuth2TokenGenerator();
UUIDOAuth2RefreshTokenGenerator refreshTokenGenerator = new UUIDOAuth2RefreshTokenGenerator();
return new DelegatingOAuth2TokenGenerator(uuidoAuth2TokenGenerator, refreshTokenGenerator);
}

img

终于对味儿了。

以上就是简单的分析了一下spring-security-oauth2-authorization-server生成Token的策略以及如何自定义Token生成器来生成我们在OAuth2.0中熟悉的形如:8f9e0b4b-6696-4424-aa2a-550398a0a685这样的Token

遗留思考的问题:

  • 生成了三张表,另外两张oauth2_authorizationoauth2_authorization_consent一直没有数据什么原因?
  • 为什么每次调用/oauth2/authorize接口都需要重新授权?
  • 如何自定义登录页面,自定义表单提交请求、自定义回调地址等。

下一篇文章会一一解答。