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

相关推荐
2401_8576100311 分钟前
Spring Boot框架:电商系统的技术优势
java·spring boot·后端
yaosheng_VALVE31 分钟前
稀硫酸介质中 V 型球阀的材质选择与选型要点-耀圣
运维·spring cloud·自动化·intellij-idea·材质·1024程序员节
java—大象1 小时前
基于java+springboot+layui的流浪动物交流信息平台设计实现
java·开发语言·spring boot·layui·课程设计
ApiHug2 小时前
ApiSmart x Qwen2.5-Coder 开源旗舰编程模型媲美 GPT-4o, ApiSmart 实测!
人工智能·spring boot·spring·ai编程·apihug
魔道不误砍柴功2 小时前
探秘Spring Boot中的@Conditional注解
数据库·spring boot·oracle
杨哥带你写代码2 小时前
网上商城系统:Spring Boot框架的实现
java·spring boot·后端
camellias_2 小时前
SpringBoot(二十一)SpringBoot自定义CURL请求类
java·spring boot·后端
晴天飛 雪3 小时前
Spring Boot MySQL 分库分表
spring boot·后端·mysql
weixin_537590453 小时前
《Spring boot从入门到实战》第七章习题答案
数据库·spring boot·后端
二十雨辰3 小时前
[Java]微服务治理
java·spring cloud