🍉Spring Authorization Server (9) 授权服务的授权信息存储方式扩展

什么是授权信息?

在Spring Authorization Server 中授权信息指的是客户端应用程序请求访问受保护资源时所需要的权限信息。这些信息通常包括客户端ID、客户端密钥、授权类型和范围等。

oauth2_authorization 表里面就存储的授权信息,包含有access_token、refesh_token、过期时间等关键数据字段,有兴趣的可以再去详细看看这个表的其他字段数据。

授权信息存储方式为什么要去扩展?

因为我们前面扩展的 PhoneCaptchaAuthenticationToken is not in the allowlist 序列化的时候出现了异常😂,玩什么扩展嘛,花里胡哨的,看看原因再去想怎么解决这个问题。

java 复制代码
java.lang.IllegalArgumentException: The class with com.watermelon.authorization.defaultauth.support.phone.PhoneCaptchaAuthenticationToken and name of com.watermelon\
.authorization.defaultauth.support.phone.PhoneCaptchaAuthenticationToken is not in the allowlist. If you believe this class is safe to deserialize, please provide an explicit mapping 
using Jackson annotations or by providing a Mixin. If the serialization is only done by a trusted source, you can also enable default typing. See https://github
.com/spring-projects/spring-security/issues/4370 for details

at org.springframework.security.oauth2.server.authorization.

为什么 UsernamePasswordAuthenticationToken 内置的就行 AuthenticationToken 就可以呢,自定义的PhoneCaptchaAuthenticationToken 就出现 is not in the allowlist 很疑惑啊。

再看看关键的错误信息 JdbcOAuth2AuthorizationService$OAuth2AuthorizationRowMapper.parseMap(JdbcOAuth2AuthorizationService.java:517)
JdbcOAuth2AuthorizationService 517行看看有啥

java 复制代码
private Map<String, Object> parseMap(String data) {
  try {
  	return this.objectMapper.readValue(data, new >TypeReference<Map<String, Object>>() {});
  } catch (Exception ex) {
         throw new IllegalArgumentException(ex.getMessage(), ex);
  }
}

转换出错了,那我们对比看看内置的UsernamePasswordAuthenticationTokenPhoneCaptchaAuthenticationToken 存储的数据到底有什么差异

PhoneCaptchaAuthenticationToken 时的data数据

java 复制代码
{"@class":"java.util.Collections$UnmodifiableMap","java.security.Principal":
{"@class":"com.watermelon.authorization.defaultauth.support.phone.PhoneCaptchaAuthenticationToken",
"authorities":["java.util.Collections$UnmodifiableRandomAccessList",
[{"@class":"org.springframework.security.core.authority.SimpleGrantedAuthority","authority":"/oauth2/token"},
{"@class":"org.springframework.security.core.authority.SimpleGrantedAuthority","authority":"/oauth2/authorize"},
{"@class":"org.springframework.security.core.authority.SimpleGrantedAuthority","authority":"/authorized"}]],"details":
{"@class":"org.springframework.security.web.authentication.WebAuthenticationDetails","remoteAddress":"192.168.56.1","sessionId":"AuXsyKnFsc3cyjp2Dy-k3FAIKeVZNa3-6S8WFsBf"},"authenticated":true,"principal":
{"@class":"com.watermelon.authorization.defaultauth.builtin.dto.SysUserDto","id":1,"phone":"18682678995","username":"18682678995","password":"
{bcrypt}$2a$10$2sGumFFLA./mT.d7w6awleE9Y9KsPL.CjwzvyvlHB5fblCBYCX/di",
"avatar":null,"status":1,"authorities":["java.util.ArrayList",
[{"@class":"org.springframework.security.core.authority.SimpleGrantedAuthority","authority":"/oauth2/token"},
{"@class":"org.springframework.security.core.authority.SimpleGrantedAuthority","authority":"/oauth2/authorize"},
{"@class":"org.springframework.security.core.authority.SimpleGrantedAuthority","authority":"/authorized"}]],"enabled":true,"accountNonExpired":true,"credentialsNonExpired":true,
"accountNonLocked":true},"credentials":null,"name":"18682678995"},"org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest":
{"@class":"org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest","authorizationUri":"http://192.168.56.1:9000/oauth2/authorize","authorizationGrantType":
{"value":"authorization_code"},"responseType":{"value":"code"},"clientId":"messaging-client","redirectUri":"http://127.0.0.1:8080/login/oauth2/code/messaging-client-oidc","scopes":
["java.util.Collections$UnmodifiableSet",["openid","profile"]],"state":"-Vrwm1Muxpcgwj7STwNbT9SVgv2h8XjkBAEWV5n2T3Y=","additionalParameters":
{"@class":"java.util.Collections$UnmodifiableMap","nonce":"vHmRCCodKwOltSXWCtlS_XZPQyLTTqkkLqv8Fogwcks"},"authorizationRequestUri":"http://192.168.56.1:9000/oauth2/authorize?
response_type=code&client_id=messaging-client&scope=openid%20profile&state=-Vrwm1Muxpcgwj7STwNbT9SVgv2h8XjkBAEWV5n2T3Y%3D&redirect_uri=http://127.0.0.1:8080/login/oauth2/code/messaging-client-oidc&nonce=vHmRCCodKwOltSXWCtlS_XZPQyLTTqkkLqv8Fogwcks","attributes":{"@class":"java.util.Collections$UnmodifiableMap"}}}

UsernamePasswordAuthenticationToken的data数据

java 复制代码
{"@class":"java.util.Collections$UnmodifiableMap","java.security.Principal":
{"@class":"org.springframework.security.authentication.UsernamePasswordAuthenticationToken","authorities":["java.util.Collections$UnmodifiableRandomAccessList",
[{"@class":"org.springframework.security.core.authority.SimpleGrantedAuthority","authority":"/oauth2/token"},
{"@class":"org.springframework.security.core.authority.SimpleGrantedAuthority","authority":"/oauth2/authorize"},
{"@class":"org.springframework.security.core.authority.SimpleGrantedAuthority","authority":"/authorized"}]],"details":
{"@class":"org.springframework.security.web.authentication.WebAuthenticationDetails","remoteAddress":"192.168.56.1","sessionId":"6sBN1fkBDIopKW98msokyGdYMo0FeMOzyGGE4Dx-"},"authenticated":true,"principal":
{"@class":"com.watermelon.authorization.defaultauth.builtin.dto.SysUserDto","id":1,"phone":"18682678995","username":"18682678995","password":"
{bcrypt}$2a$10$pJYa8tfSmDysF7pz5EVJ3.qg7Q8G3qNS00KSCurw5VpUfIVoksR4K","avatar":null,"status":1,"authorities":["java.util.ArrayList",
[{"@class":"org.springframework.security.core.authority.SimpleGrantedAuthority","authority":"/oauth2/token"},
{"@class":"org.springframework.security.core.authority.SimpleGrantedAuthority","authority":"/oauth2/authorize"},
{"@class":"org.springframework.security.core.authority.SimpleGrantedAuthority","authority":"/authorized"}]],"enabled":true,"accountNonExpired":true,"credentialsNonExpired":true,
"accountNonLocked":true},"credentials":null},"org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest":
{"@class":"org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest","authorizationUri":"http://192.168.56.1:9000/oauth2/authorize","authorizationGrantType":
{"value":"authorization_code"},"responseType":{"value":"code"},"clientId":"messaging-client","redirectUri":"http://127.0.0.1:8080/login/oauth2/code/messaging-client-oidc","scopes":["java.util.Collections$UnmodifiableSet",
["openid","profile"]],"state":"6o4EXBxk_kN9U8jof0rGaz5t4UjB64h5Xc076gGEORg=","additionalParameters":
{"@class":"java.util.Collections$UnmodifiableMap","nonce":"REhLXzkG6XBFP8vmQGuMkWcYiQ0vvkuJBXVakN-PfoA","continue":""},
"authorizationRequestUri":"http://192.168.56.1:9000/oauth2/authorize?response_type=code&client_id=messaging-client&scope=openid%20profile&state=6o4EXBxk_kN9U8jof0rGaz5t4UjB64h5Xc076gGEORg%3D&redirect_uri=http://127.0.0.1:8080/login/oauth2/code/messaging-client-oidc&nonce=REhLXzkG6XBFP8vmQGuMkWcYiQ0vvkuJBXVakN-PfoA&continue=","attributes":{"@class":"java.util.Collections$UnmodifiableMap"}}}

以上看没除了class不一样 结构大致都一样,就因为 PhoneCaptchaAuthenticationToken 不是亲生的,JdbcOAuth2AuthorizationService#parseMap()就不支持了😂。

解决方案

1:用UsernamePasswordAuthenticationToken 替换PhoneCaptchaAuthenticationToken

2:重新一个 JdbcOAuth2AuthorizationService 来进行存储和转换

选择第二种方案,因为后期可能还扩展其他的 AuthenticationToken ,再加上授权信息想使用redis进行存储,那就开始干吧。 JdbcOAuth2AuthorizationService 实现了 OAuth2AuthorizationService 接口,同样实现它干就完事了

RedisOAuth2AuthorizationServiceImpl

java 复制代码
@Component
public class RedisOAuth2AuthorizationServiceImpl implements OAuth2AuthorizationService {

   private final static String AUTHORIZATION_TYPE = "authorization_type";

   private final static String OAUTH2_PARAMETER_NAME_ID = "id";

   private final static Long TIMEOUT = 600L;

   @Resource
   private RedisTemplate<String, Object> redisTemplate;

   @Override
   public void save(OAuth2Authorization authorization) {
       Assert.notNull(authorization, "authorization cannot be null");
       redisTemplate.setValueSerializer(RedisSerializer.java());
       redisTemplate.opsForValue().set(buildAuthorizationKey(OAUTH2_PARAMETER_NAME_ID, authorization.getId()), authorization, TIMEOUT, TimeUnit.SECONDS);
       if (isState(authorization)) {
           String state = authorization.getAttribute(OAuth2ParameterNames.STATE);
           String isStateKey = buildAuthorizationKey(OAuth2ParameterNames.STATE, state);
           redisTemplate.setValueSerializer(RedisSerializer.java());
           redisTemplate.opsForValue().set(isStateKey, authorization, TIMEOUT, TimeUnit.SECONDS);
       }
       if (isAuthorizationCode(authorization)) {
           OAuth2Authorization.Token<OAuth2AuthorizationCode> authorizationCode =
                   authorization.getToken(OAuth2AuthorizationCode.class);
           String tokenValue = authorizationCode.getToken().getTokenValue();
           String isAuthorizationCodeKey = buildAuthorizationKey(OAuth2ParameterNames.CODE, tokenValue);
           Instant expiresAt = authorizationCode.getToken().getExpiresAt();//过期时间
           Instant issuedAt = authorizationCode.getToken().getIssuedAt();//发放token的时间
           Date expiresAtDate = Date.from(expiresAt);
           Date issuedAtDate = Date.from(issuedAt);
           redisTemplate.setValueSerializer(RedisSerializer.java());
           redisTemplate.opsForValue().set(isAuthorizationCodeKey, authorization, TIMEOUT, TimeUnit.SECONDS);
       }
       if (isAccessToken(authorization)) {
           OAuth2Authorization.Token<OAuth2AccessToken> accessToken =
                   authorization.getToken(OAuth2AccessToken.class);
           String tokenValue = accessToken.getToken().getTokenValue();
           String isAccessTokenKey = buildAuthorizationKey(OAuth2ParameterNames.ACCESS_TOKEN, tokenValue);
           Instant expiresAt = accessToken.getToken().getExpiresAt();//过期时间
           Instant issuedAt = accessToken.getToken().getIssuedAt();//发放token的时间
           Date expiresAtDate = Date.from(expiresAt);
           Date issuedAtDate = Date.from(issuedAt);
           redisTemplate.setValueSerializer(RedisSerializer.java());
           redisTemplate.opsForValue().set(isAccessTokenKey, authorization, TIMEOUT, TimeUnit.SECONDS);
       }
       if (isRefreshToken(authorization)) {
           OAuth2Authorization.Token<OAuth2RefreshToken> refreshToken =
                   authorization.getToken(OAuth2RefreshToken.class);
           String tokenValue = refreshToken.getToken().getTokenValue();
           String isRefreshTokenKey = buildAuthorizationKey(OAuth2ParameterNames.REFRESH_TOKEN, tokenValue);
           Instant expiresAt = refreshToken.getToken().getExpiresAt();//过期时间
           Instant issuedAt = refreshToken.getToken().getIssuedAt();//发放token的时间
           Date expiresAtDate = Date.from(expiresAt);
           Date issuedAtDate = Date.from(issuedAt);
           redisTemplate.setValueSerializer(RedisSerializer.java());
           redisTemplate.opsForValue().set(isRefreshTokenKey, authorization, TIMEOUT, TimeUnit.SECONDS);

       }
       if (isIdToken(authorization)) {
           OAuth2Authorization.Token<OidcIdToken> idToken =
                   authorization.getToken(OidcIdToken.class);
           String tokenValue = idToken.getToken().getTokenValue();
           String isIdTokenKey = buildAuthorizationKey(OidcParameterNames.ID_TOKEN, tokenValue);
           Instant expiresAt = idToken.getToken().getExpiresAt();//过期时间
           Instant issuedAt = idToken.getToken().getIssuedAt();//发放token的时间
           Date expiresAtDate = Date.from(expiresAt);
           Date issuedAtDate = Date.from(issuedAt);
           redisTemplate.setValueSerializer(RedisSerializer.java());
           redisTemplate.opsForValue().set(isIdTokenKey, authorization, TIMEOUT, TimeUnit.SECONDS);
       }
       if (isDeviceCode(authorization)) {
           OAuth2Authorization.Token<OAuth2DeviceCode> deviceCode =
                   authorization.getToken(OAuth2DeviceCode.class);

           String tokenValue = deviceCode.getToken().getTokenValue();
           String isDeviceCodeKey = buildAuthorizationKey(OAuth2ParameterNames.DEVICE_CODE, tokenValue);
           Instant expiresAt = deviceCode.getToken().getExpiresAt();//过期时间
           Instant issuedAt = deviceCode.getToken().getIssuedAt();//发放token的时间
           Date expiresAtDate = Date.from(expiresAt);
           Date issuedAtDate = Date.from(issuedAt);
           redisTemplate.setValueSerializer(RedisSerializer.java());
           redisTemplate.opsForValue().set(isDeviceCodeKey, authorization, TIMEOUT, TimeUnit.SECONDS);
       }
       if (isUserCode(authorization)) {
           OAuth2Authorization.Token<OAuth2UserCode> userCode =
                   authorization.getToken(OAuth2UserCode.class);
           String tokenValue = userCode.getToken().getTokenValue();
           String isUserCodeKey = buildAuthorizationKey(OAuth2ParameterNames.USER_CODE, tokenValue);
           Instant expiresAt = userCode.getToken().getExpiresAt();//过期时间
           Instant issuedAt = userCode.getToken().getIssuedAt();//发放token的时间
           Date expiresAtDate = Date.from(expiresAt);
           Date issuedAtDate = Date.from(issuedAt);
           redisTemplate.setValueSerializer(RedisSerializer.java());
           redisTemplate.opsForValue().set(isUserCodeKey, authorization, TIMEOUT, TimeUnit.SECONDS);
       }
   }

   @Override
   public void remove(OAuth2Authorization authorization) {
       List<String> keys = new ArrayList<>();
       String idKey = buildAuthorizationKey(OAUTH2_PARAMETER_NAME_ID, authorization.getId());
       keys.add(idKey);
       if (isState(authorization)) {
           String state = authorization.getAttribute(OAuth2ParameterNames.STATE);
           String isStateKey = buildAuthorizationKey(OAuth2ParameterNames.STATE, state);
           keys.add(isStateKey);
       }
       if (isAuthorizationCode(authorization)) {
           OAuth2Authorization.Token<OAuth2AuthorizationCode> authorizationCode =
                   authorization.getToken(OAuth2AuthorizationCode.class);
           String tokenValue = authorizationCode.getToken().getTokenValue();
           String isAuthorizationCodeKey = buildAuthorizationKey(OAuth2ParameterNames.CODE, tokenValue);
           keys.add(isAuthorizationCodeKey);
       }
       if (isAccessToken(authorization)) {
           OAuth2Authorization.Token<OAuth2AccessToken> accessToken =
                   authorization.getToken(OAuth2AccessToken.class);
           String tokenValue = accessToken.getToken().getTokenValue();
           String isAccessTokenKey = buildAuthorizationKey(OAuth2ParameterNames.ACCESS_TOKEN, tokenValue);
           keys.add(isAccessTokenKey);
       }
       if (isRefreshToken(authorization)) {
           OAuth2Authorization.Token<OAuth2RefreshToken> refreshToken =
                   authorization.getToken(OAuth2RefreshToken.class);
           String tokenValue = refreshToken.getToken().getTokenValue();
           String isRefreshTokenKey = buildAuthorizationKey(OAuth2ParameterNames.REFRESH_TOKEN, tokenValue);
           keys.add(isRefreshTokenKey);
       }
       if (isIdToken(authorization)) {
           OAuth2Authorization.Token<OidcIdToken> idToken =
                   authorization.getToken(OidcIdToken.class);
           String tokenValue = idToken.getToken().getTokenValue();
           String isIdTokenKey = buildAuthorizationKey(OidcParameterNames.ID_TOKEN, tokenValue);
           keys.add(isIdTokenKey);
       }
       if (isDeviceCode(authorization)) {
           OAuth2Authorization.Token<OAuth2DeviceCode> deviceCode =
                   authorization.getToken(OAuth2DeviceCode.class);

           String tokenValue = deviceCode.getToken().getTokenValue();
           String isDeviceCodeKey = buildAuthorizationKey(OAuth2ParameterNames.DEVICE_CODE, tokenValue);
           keys.add(isDeviceCodeKey);
       }
       if (isUserCode(authorization)) {
           OAuth2Authorization.Token<OAuth2UserCode> userCode =
                   authorization.getToken(OAuth2UserCode.class);
           String tokenValue = userCode.getToken().getTokenValue();
           String isUserCodeKey = buildAuthorizationKey(OAuth2ParameterNames.USER_CODE, tokenValue);
           keys.add(isUserCodeKey);
       }
       redisTemplate.delete(keys);
   }

   @Override
   public OAuth2Authorization findById(String id) {
       return (OAuth2Authorization) Optional.ofNullable(redisTemplate.opsForValue().get(buildAuthorizationKey(OAUTH2_PARAMETER_NAME_ID, id))).orElse(null);
   }

   @Override
   public OAuth2Authorization findByToken(String token, OAuth2TokenType tokenType) {
       Assert.hasText(token, "token cannot be empty");
       Assert.notNull(tokenType, "tokenType cannot be empty");
       redisTemplate.setValueSerializer(RedisSerializer.java());
       return (OAuth2Authorization) redisTemplate.opsForValue().get(buildAuthorizationKey(tokenType.getValue(), token));
   }


   private boolean isState(OAuth2Authorization authorization) {
       return Objects.nonNull(authorization.getAttribute(OAuth2ParameterNames.STATE));
   }


   private boolean isAuthorizationCode(OAuth2Authorization authorization) {
       OAuth2Authorization.Token<OAuth2AuthorizationCode> authorizationCode =
               authorization.getToken(OAuth2AuthorizationCode.class);
       return Objects.nonNull(authorizationCode);
   }

   private boolean isAccessToken(OAuth2Authorization authorization) {
       OAuth2Authorization.Token<OAuth2AccessToken> accessToken =
               authorization.getToken(OAuth2AccessToken.class);
       return Objects.nonNull(accessToken) && Objects.nonNull(accessToken.getToken().getTokenType());
   }

   private boolean isRefreshToken(OAuth2Authorization authorization) {
       OAuth2Authorization.Token<OAuth2RefreshToken> refreshToken =
               authorization.getToken(OAuth2RefreshToken.class);
       return Objects.nonNull(refreshToken) && Objects.nonNull(refreshToken.getToken().getTokenValue());
   }

   private boolean isIdToken(OAuth2Authorization authorization) {
       OAuth2Authorization.Token<OidcIdToken> idToken =
               authorization.getToken(OidcIdToken.class);
       return Objects.nonNull(idToken) && Objects.nonNull(idToken.getToken().getTokenValue());
   }

   private boolean isDeviceCode(OAuth2Authorization authorization) {
       OAuth2Authorization.Token<OAuth2DeviceCode> deviceCode =
               authorization.getToken(OAuth2DeviceCode.class);
       return Objects.nonNull(deviceCode) && Objects.nonNull(deviceCode.getToken().getTokenValue());
   }

   private boolean isUserCode(OAuth2Authorization authorization) {
       OAuth2Authorization.Token<OAuth2UserCode> userCode =
               authorization.getToken(OAuth2UserCode.class);
       return Objects.nonNull(userCode) && Objects.nonNull(userCode.getToken().getTokenValue());
   }

   /**
    * redis key 构建
    *
    * @param type  授权类型
    * @param value 授权值
    * @return
    */
   private String buildAuthorizationKey(String type, String value) {
       return AUTHORIZATION_TYPE.concat("::").concat(type).concat("::").concat(value);
   }
}

redisTemplate.setValueSerializer(RedisSerializer.java())RedisSerializer的原因是因为 OAuth2Authorization有些字段类型的原因,用其他的就会抛一些序列化异常的。

选择用 @Component 注入,之前 @Bean 注入的JdbcOAuth2AuthorizationService 就需要删除掉

java 复制代码
@Bean
public OAuth2AuthorizationService authorizationService(JdbcTemplate jdbcTemplate,
                                                       RegisteredClientRepository registeredClientRepository) {
    return new JdbcOAuth2AuthorizationService(jdbcTemplate, registeredClientRepository);
}

把它删除掉

这个问题就解决了,从开始到现在 用 spring-authorization-server 的过程很曲折😔。

相关推荐
lang201509281 小时前
Spring Boot优雅关闭全解析
java·spring boot·后端
刘一说2 小时前
Spring Boot 启动慢?启动过程深度解析与优化策略
java·spring boot·后端
lang201509283 小时前
Spring Boot缓存机制全解析
spring boot·后端·缓存
摇滚侠4 小时前
Spring Boot 3零基础教程,WEB 开发 默认页签图标 Favicon 笔记29
java·spring boot·笔记
lang201509284 小时前
Spring Boot SQL数据库全攻略
数据库·spring boot·sql
是梦终空5 小时前
计算机毕业设计241—基于Java+Springboot+vue的爱心公益服务系统(源代码+数据库+11000字文档)
java·spring boot·vue·毕业设计·课程设计·毕业论文·爱心公益系统
泉城老铁9 小时前
springboot 对接发送钉钉消息,消息内容带图片
前端·spring boot·后端
qq_12498707539 小时前
基于Spring Boot的高校实习实践管理系统(源码+论文+部署+安装)
java·spring boot·后端·毕业设计
韩宁羽9 小时前
SpringBoot开发双11商品服务系统[完结19章]
spring boot
5pace11 小时前
【JavaWeb|第二篇】SpringBoot篇
java·spring boot·后端