🍉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 的过程很曲折😔。

相关推荐
后端小张18 小时前
基于飞算AI的图书管理系统设计与实现
spring boot
咖啡Beans20 小时前
使用OpenFeign实现微服务间通信
java·spring cloud
考虑考虑2 天前
Jpa使用union all
java·spring boot·后端
咖啡Beans2 天前
SpringCloud网关Gateway功能实现
java·spring cloud
阿杆2 天前
同事嫌参数校验太丑,我直接掏出了更优雅的 SpEL Validator
java·spring boot·后端
昵称为空C3 天前
SpringBoot3 http接口调用新方式RestClient + @HttpExchange像使用Feign一样调用
spring boot·后端
麦兜*3 天前
MongoDB Atlas 云数据库实战:从零搭建全球多节点集群
java·数据库·spring boot·mongodb·spring·spring cloud
麦兜*3 天前
MongoDB 在物联网(IoT)中的应用:海量时序数据处理方案
java·数据库·spring boot·物联网·mongodb·spring
汤姆yu3 天前
基于springboot的毕业旅游一站式定制系统
spring boot·后端·旅游
计算机毕业设计木哥3 天前
计算机毕设选题推荐:基于Java+SpringBoot物品租赁管理系统【源码+文档+调试】
java·vue.js·spring boot·mysql·spark·毕业设计·课程设计