likes
comments
collection
share

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

作者站长头像
站长
· 阅读数 11

架构版本 Spring Boot 3.1 Spring Authorization Server 1.1.1 spring-cloud 2022.0.3 spring-cloud-alibaba 2022.0.0.0 完整代码👉watermelon-cloud

什么是授权信息?

在Spring Authorization Server 中授权信息指的是客户端应用程序请求访问受保护资源时所需要的权限信息。这些信息通常包括客户端ID、客户端密钥、授权类型和范围等。 oauth2_authorization 表里面就存储的授权信息,包含有access_token、refesh_token、过期时间等关键数据字段,有兴趣的可以再去详细看看这个表的其他字段数据。

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

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

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

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

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

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数据

{"@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数据

{"@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 来进行存储和转换

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

RedisOAuth2AuthorizationServiceImpl

@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 就需要删除掉

@Bean
public OAuth2AuthorizationService authorizationService(JdbcTemplate jdbcTemplate,
                                                       RegisteredClientRepository registeredClientRepository) {
    return new JdbcOAuth2AuthorizationService(jdbcTemplate, registeredClientRepository);
}

把它删除掉

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