spring authorization server默认使用的是jwt令牌,但由于jwt令牌的缺点:
- 服务器不保存会话状态,一旦JWT被颁发,除非更改密钥或等待令牌过期,否则无法在有效期内撤销
- 尽管JWT包含了签名,但如果密钥管理不当,例如密钥泄露或密钥强度不足,令牌仍可能被伪造
- JWT本身包含认证信息,因此一旦信息泄露,任何人都可以获得令牌的所有权限
- WT令牌的长度可能较长,占用较多的存储空间,这可能导致每个请求的额外网络开销,尤其是在移动设备或低带宽环境下
因此,由于上述相关的缺点,现将令牌改为不透明令牌模式。
1、启用不透明令牌配置
将RegisteredClient
的accessTokenFormat设置为OAuth2TokenFormat.REFERENCE
即可启用不透明令牌。

配置完成后,默认生成的key的长度为96位,为了节省存储空间和传输带宽,现将位数进行缩短。
2、配置TokenGenerator(非必须)
创建token生成类,实现OAuth2TokenGenerator接口,来生成AccessToken和RefreshToken。
如果需要更多的信息,可以对OAuth2Token进行重写,添加更多的属性。
@Component
public class RedisTokenGenerator implements OAuth2TokenGenerator<OAuth2Token> {
@Override
public OAuth2Token generate(OAuth2TokenContext context) {
// accessToken和refreshToken都会通过该方法进行生成,需要独立进行判断
OAuth2TokenType tokenType = context.getTokenType();
if (OAuth2TokenType.ACCESS_TOKEN.equals(context.getTokenType()) &&
OAuth2TokenFormat.REFERENCE.equals(context.getRegisteredClient().getTokenSettings().getAccessTokenFormat())) {
// accessToken 生成
return buildAccessToken(context);
} else if (OAuth2TokenType.REFRESH_TOKEN.equals(context.getTokenType()) && !isPublicClientForAuthorizationCodeGrant(context)) {
// refreshToken 生成
return buildRefreshToken(context);
}
return null;
}
/**
* 判断是不是public client
* @param context
* @return
*/
private boolean isPublicClientForAuthorizationCodeGrant(OAuth2TokenContext context) {
if (AuthorizationGrantType.AUTHORIZATION_CODE.equals(context.getAuthorizationGrantType()) &&
(context.getAuthorizationGrant().getPrincipal() instanceof OAuth2ClientAuthenticationToken clientPrincipal)) {
return clientPrincipal.getClientAuthenticationMethod().equals(ClientAuthenticationMethod.NONE);
}
return false;
}
/**
* 构建access token {@link OAuth2AccessTokenGenerator}
* @param context
*/
private OAuth2Token buildAccessToken(OAuth2TokenContext context) {
RegisteredClient registeredClient = context.getRegisteredClient();
Instant issuedAt = Instant.now();
Instant expiresAt = issuedAt.plus(registeredClient.getTokenSettings().getAccessTokenTimeToLive());
return new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER, buildUUID(), issuedAt, expiresAt, context.getAuthorizedScopes());
}
/**
* 构建刷新密钥 {@link OAuth2RefreshTokenGenerator}
* @param context
*/
private OAuth2RefreshToken buildRefreshToken(OAuth2TokenContext context) {
Instant issuedAt = Instant.now();
Instant expiresAt = issuedAt.plus(context.getRegisteredClient().getTokenSettings().getRefreshTokenTimeToLive());
return new OAuth2RefreshToken(buildUUID(), issuedAt, expiresAt);
}
private String buildUUID() {
return UUID.randomUUID().toString().replace("-", "");
}
}
使用了@Component注解后,spring security会自动使用在容器内的OAuth2TokenGenerator类,不会再创建默认配置(DelegatingOAuth2TokenGenerator)。也可以在SecurityConfig中通过OAuth2AuthorizationServerConfigurer.tokenGenerator()
方法配置对应的OAuth2TokenGenerator类。
3、使用redis保存令牌信息
为了使用redis来保存令牌信息,我们需要实现OAuth2AuthorizationService接口。
3.1 管理token类型
使用枚举的方式来管理spring authorization server中所有的token类型,防止遗漏。
@Getter
public enum OAuth2AuthorizationTokenType {
STATE(OAuth2ParameterNames.STATE, null) {
@Override
public String getTokenValue(OAuth2Authorization authorization) {
String authorizationState = authorization.getAttribute(OAuth2ParameterNames.STATE);
if (StringUtils.hasText(authorizationState)) {
return authorizationState;
}
return null;
}
@Override
public OAuth2Token getOAuth2Token(OAuth2AuthorizationTokenValue tokenValue) {
return null;
}
},
AUTHORIZATION_CODE(OAuth2ParameterNames.CODE, OAuth2AuthorizationCode.class) {
@Override
public OAuth2Token getOAuth2Token(OAuth2AuthorizationTokenValue tokenValue) {
return new OAuth2AuthorizationCode(tokenValue.getValue(), tokenValue.getIssuedAt(), tokenValue.getExpiresAt());
}
},
ACCESS_TOKEN(OAuth2TokenType.ACCESS_TOKEN.getValue(), OAuth2AccessToken.class) {
@Override
public OAuth2AuthorizationTokenValue getTokenValue(OAuth2Authorization.Token<? extends OAuth2Token> token) {
OAuth2AuthorizationTokenValue auth2AuthorizationTokenValue = super.getTokenValue(token);
OAuth2AccessToken oAuth2AccessToken = (OAuth2AccessToken) token.getToken();
auth2AuthorizationTokenValue.setTokenType(oAuth2AccessToken.getTokenType().getValue());
auth2AuthorizationTokenValue.setScopes(oAuth2AccessToken.getScopes());
return auth2AuthorizationTokenValue;
}
@Override
public OAuth2Token getOAuth2Token(OAuth2AuthorizationTokenValue tokenValue) {
OAuth2AccessToken.TokenType tokenType = null;
if (OAuth2AccessToken.TokenType.BEARER.getValue().equalsIgnoreCase(tokenValue.getTokenType())) {
tokenType = OAuth2AccessToken.TokenType.BEARER;
}
return new OAuth2AccessToken(tokenType, tokenValue.getValue(), tokenValue.getIssuedAt(), tokenValue.getExpiresAt(), tokenValue.getScopes());
}
},
ID_TOKEN(OidcParameterNames.ID_TOKEN, OidcIdToken.class) {
@Override
public OAuth2Token getOAuth2Token(OAuth2AuthorizationTokenValue tokenValue) {
return new OidcIdToken(tokenValue.getValue(),
tokenValue.getIssuedAt(),
tokenValue.getExpiresAt(),
(Map<String, Object>) tokenValue.getMetaData().get(OAuth2Authorization.Token.CLAIMS_METADATA_NAME)
);
}
},
REFRESH_TOKEN(OAuth2TokenType.REFRESH_TOKEN.getValue(), OAuth2RefreshToken.class) {
@Override
public OAuth2Token getOAuth2Token(OAuth2AuthorizationTokenValue tokenValue) {
return new OAuth2RefreshToken(tokenValue.getValue(), tokenValue.getIssuedAt(), tokenValue.getExpiresAt());
}
},
DEVICE_CODE(OAuth2ParameterNames.DEVICE_CODE, OAuth2DeviceCode.class) {
@Override
public OAuth2Token getOAuth2Token(OAuth2AuthorizationTokenValue tokenValue) {
return new OAuth2DeviceCode(tokenValue.getValue(), tokenValue.getIssuedAt(), tokenValue.getExpiresAt());
}
},
USER_CODE(OAuth2ParameterNames.USER_CODE, OAuth2UserCode.class) {
@Override
public OAuth2Token getOAuth2Token(OAuth2AuthorizationTokenValue tokenValue) {
return new OAuth2UserCode(tokenValue.getValue(), tokenValue.getIssuedAt(), tokenValue.getExpiresAt());
}
}
;
/**
* 类型
*/
String tokenType;
/**
* 对应的对象类型
*/
Class<? extends OAuth2Token> oAuth2Token;
OAuth2AuthorizationTokenType(String tokenType, Class<? extends OAuth2Token> oAuth2Token) {
this.tokenType = tokenType;
this.oAuth2Token = oAuth2Token;
}
/**
* 获取
* @param authorization
*/
public OAuth2Authorization.Token<? extends OAuth2Token> getToken(OAuth2Authorization authorization) {
if (oAuth2Token == null) {
return null;
}
return authorization.getToken(oAuth2Token);
}
/**
* 获取token value对象
* @return
*/
public OAuth2AuthorizationTokenValue getTokenValue(OAuth2Authorization.Token<? extends OAuth2Token> token) {
OAuth2AuthorizationTokenValue oAuth2AuthorizationTokenValue = new OAuth2AuthorizationTokenValue();
oAuth2AuthorizationTokenValue.setValue(token.getToken().getTokenValue());
oAuth2AuthorizationTokenValue.setExpiresAt(token.getToken().getExpiresAt());
oAuth2AuthorizationTokenValue.setIssuedAt(token.getToken().getIssuedAt());
oAuth2AuthorizationTokenValue.setMetaData(token.getMetadata());
return oAuth2AuthorizationTokenValue;
}
/**
* 获取token的值
* @param authorization
* @return
*/
public String getTokenValue(OAuth2Authorization authorization) {
OAuth2Authorization.Token<? extends OAuth2Token> token = getToken(authorization);
if (token != null) {
return token.getToken().getTokenValue();
}
return null;
}
/**
* 反序列化构建对象
* @param tokenValue
* @return
*/
public abstract OAuth2Token getOAuth2Token(OAuth2AuthorizationTokenValue tokenValue);
public static final Map<String, OAuth2AuthorizationTokenType> INSTANCE = new HashMap<>();
static {
for (OAuth2AuthorizationTokenType item : OAuth2AuthorizationTokenType.values()) {
INSTANCE.put(item.getTokenType(), item);
}
}
public static OAuth2AuthorizationTokenType getInstance(String type) {
return INSTANCE.get(type);
}
}
3.2 Oauth2Authorization管理
Oauth2Authorization内部有很多的类没有默认构造函数,我们这里使用的是jackson的方式存储到redis,当反序列化时,没有默认构造函数的类就会反序列化错误,因此,这里我们定义自己的类来管理令牌的信息。
@Setter
@Getter
@NoArgsConstructor
public class RedisOAuth2Authorization {
private String id;
/**
* 客户端key
*/
private String registeredClientId;
/**
* 对象名称
*/
private String principalName;
/**
* 授权类型
*/
private String authorizationGrantType;
/**
* 授权范围
*/
private Set<String> authorizedScopes;
/**
* 参数
*/
private Map<String, Object> attributes;
/**
* 状态
*/
private String state;
/**
* 最小过期时间
*/
private Instant minExpiresAt;
/**
*
*/
private Map<OAuth2AuthorizationTokenType, OAuth2AuthorizationTokenValue> tokenValuesMap = new HashMap<>();
public RedisOAuth2Authorization(OAuth2Authorization authorization) {
this.id = authorization.getId();
this.registeredClientId = authorization.getRegisteredClientId();
this.principalName = authorization.getPrincipalName();
this.authorizationGrantType = authorization.getAuthorizationGrantType().getValue();
this.authorizedScopes = authorization.getAuthorizedScopes();
this.attributes = authorization.getAttributes();
this.state = OAuth2AuthorizationTokenType.STATE.getTokenValue(authorization);
Instant minExpiresAt = null;
for (OAuth2AuthorizationTokenType tokenType : OAuth2AuthorizationTokenType.values()) {
OAuth2Authorization.Token<? extends OAuth2Token> token = tokenType.getToken(authorization);
if (token != null) {
tokenValuesMap.put(tokenType, tokenType.getTokenValue(token));
minExpiresAt = getMinExpiresAt(minExpiresAt, token.getToken().getExpiresAt());
}
}
this.minExpiresAt = minExpiresAt;
}
/**
* 获取最小的到期时间
* @param lastExpiresAt
* @param currentExpiresAt
*/
private Instant getMinExpiresAt(Instant lastExpiresAt, Instant currentExpiresAt) {
if (currentExpiresAt == null) {
return lastExpiresAt;
}
if (lastExpiresAt == null || currentExpiresAt.isBefore(lastExpiresAt)) {
return currentExpiresAt;
}
return lastExpiresAt;
}
}
3.3 存储令牌到redis内
创建类,实现OAuth2AuthorizationService接口相关方法即可。
@Component
@AllArgsConstructor
public class RedisOAuth2AuthorizationService implements OAuth2AuthorizationService {
public static final String KEY = "authorization" + VariableConstant.SEPARATOR;
private RedisTemplate redisTemplate;
private RegisteredClientRepository registeredClientRepository;
@Override
public void save(OAuth2Authorization authorization) {
/**
* 通过id存储实际的内容,
* 然后其他的token对应存储id,后续查找时通过二次查询获取
*/
RedisOAuth2Authorization redisOAuth2Authorization = new RedisOAuth2Authorization(authorization);
// 存储到redis中
Duration cacheExpireTime = Duration.ofSeconds(redisOAuth2Authorization.getMinExpiresAt() == null ? 60*30L :
redisOAuth2Authorization.getMinExpiresAt().getEpochSecond() - Instant.now().getEpochSecond());
redisTemplate.executePipelined(new SessionCallback<Object>() {
@Override
public Object execute(RedisOperations operations) throws DataAccessException {
/**
* 查询token时,会通过token的类型和token的值的方式来查询
*/
if (!redisOAuth2Authorization.getTokenValuesMap().isEmpty()) {
// token类型的对应
redisOAuth2Authorization.getTokenValuesMap().forEach((key, value) -> {
operations.opsForValue().set(KEY + key.name() + VariableConstant.SEPARATOR + value.getValue(),
redisOAuth2Authorization.getId(),
cacheExpireTime);
});
}
/**
* state类型也是TokenType的一种,也需要存储
*/
if (StringUtils.hasText(redisOAuth2Authorization.getState())) {
operations.opsForValue().set(KEY + OAuth2AuthorizationTokenType.STATE.name() + VariableConstant.SEPARATOR + redisOAuth2Authorization.getState(),
redisOAuth2Authorization.getId(),
cacheExpireTime);
}
// 具体信息
operations.opsForValue().set(KEY + VariableConstant.ID + VariableConstant.SEPARATOR + redisOAuth2Authorization.getId(),
redisOAuth2Authorization,
cacheExpireTime);
return null;
}
});
}
@Override
public void remove(OAuth2Authorization authorization) {
List<String> tokens = new ArrayList<>();
for (OAuth2AuthorizationTokenType tokenType : OAuth2AuthorizationTokenType.values()) {
String token = tokenType.getTokenValue(authorization);
if (!StringUtils.hasText(token)) {
continue;
}
tokens.add(KEY + tokenType.name() + VariableConstant.SEPARATOR + token);
}
// id的映射
tokens.add(KEY + VariableConstant.ID + VariableConstant.SEPARATOR + authorization.getId());
redisTemplate.delete(tokens);
}
@Override
public OAuth2Authorization findById(String id) {
Assert.hasText(id, "id 不能为空");
return getOAuth2Authorization((RedisOAuth2Authorization) redisTemplate.opsForValue().get(KEY + VariableConstant.ID + VariableConstant.SEPARATOR + id));
}
@Override
public OAuth2Authorization findByToken(String token, @Nullable OAuth2TokenType tokenType) {
Assert.hasText(token, "token 不能为空");
OAuth2AuthorizationTokenType oAuth2AuthorizationTokenType = OAuth2AuthorizationTokenType.getInstance(tokenType.getValue());
Assert.isTrue(oAuth2AuthorizationTokenType != null, "token类型不支持");
String oAuth2AuthorizationId = (String) redisTemplate.opsForValue().get(KEY + oAuth2AuthorizationTokenType.name() + VariableConstant.SEPARATOR + token);
if (StringUtils.hasText(oAuth2AuthorizationId)) {
return getOAuth2Authorization((RedisOAuth2Authorization) redisTemplate.opsForValue().get(KEY + VariableConstant.ID + VariableConstant.SEPARATOR + oAuth2AuthorizationId));
}
return null;
}
/**
* 转成OAuth2Authorization对象
* @param authorization redis中存储的对象
*/
private OAuth2Authorization getOAuth2Authorization(RedisOAuth2Authorization authorization) {
if (authorization == null) {
return null;
}
// 使用构造器生成OAuth2Authorization
OAuth2Authorization.Builder builder = OAuth2Authorization.withRegisteredClient(RegisteredClient.withId(authorization.getRegisteredClientId()).build());
builder.id(authorization.getId())
.principalName(authorization.getPrincipalName())
.authorizationGrantType(new AuthorizationGrantType(authorization.getAuthorizationGrantType()))
.authorizedScopes(authorization.getAuthorizedScopes())
.attributes((attrs) -> attrs.putAll(authorization.getAttributes()));
if (StringUtils.hasText(authorization.getState())) {
builder.attribute(OAuth2ParameterNames.STATE, authorization.getState());
}
// token
authorization.getTokenValuesMap().forEach((key, value) -> {
builder.token(key.getOAuth2Token(value), (metadata) -> metadata.putAll(value.getMetaData()));
});
return builder.build();
}
}
4、解析token
当接口请求时,对token进行解析成为用户信息。
4.1 自定义用户类信息
自定义用户类信息,实现OAuth2AuthenticatedPrincipal 接口。可自行进行扩充。
public record OAuth2IntrospectionAuthenticatedPrincipal (String name,
String clientId,
Map<String, Object> attributes,
Collection<GrantedAuthority> authorities,
Principal principal) implements OAuth2AuthenticatedPrincipal {
@Override
public Map<String, Object> getAttributes() {
return attributes;
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return null;
}
@Override
public String getName() {
return name;
}
}
4.2 将token解析成用户类
添加类,实现OpaqueTokenIntrospector 接口,在接口中根据token转换成用户类信息。
@Component
public class OpaqueTokenIntrospectorHandler implements OpaqueTokenIntrospector {
private OAuth2AuthorizationService oAuth2AuthorizationService;
public OpaqueTokenIntrospectorHandler(OAuth2AuthorizationService oAuth2AuthorizationService) {
this.oAuth2AuthorizationService = oAuth2AuthorizationService;
}
@Override
public OAuth2AuthenticatedPrincipal introspect(String token) {
OAuth2Authorization oAuth2Authorization = oAuth2AuthorizationService.findByToken(token, OAuth2TokenType.ACCESS_TOKEN);
if (oAuth2Authorization == null) {
throw new BadOpaqueTokenException("授权已经过期,请重新进行登录");
}
return new OAuth2IntrospectionAuthenticatedPrincipal(oAuth2Authorization.getPrincipalName(),
oAuth2Authorization.getRegisteredClientId(),
new HashMap<>(),
new ArrayList<>(),
oAuth2Authorization.getAttribute(Principal.class.getName()));
}
}
4.3 将解析类添加到配置
在SecurityConfig中,添加oauth2ResourceServer
配置信息。

Comments NOTHING