什么是授权信息?
在Spring Authorization Server 中授权信息指的是客户端应用程序请求访问受保护资源时所需要的权限信息。这些信息通常包括客户端ID、客户端密钥、授权类型和范围等。
oauth2_authorization 表里面就存储的授权信息,包含有access_token、refesh_token、过期时间等关键数据字段,有兴趣的可以再去详细看看这个表的其他字段数据。
授权信息存储方式为什么要去扩展?
因为我们前面扩展的 PhoneCaptchaAuthenticationToken
is not in the allowlist 序列化的时候出现了异常😂,玩什么扩展嘛,花里胡哨的,看看原因再去想怎么解决这个问题。
javajava.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行看看有啥
javaprivate 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); } }
转换出错了,那我们对比看看内置的UsernamePasswordAuthenticationToken
与PhoneCaptchaAuthenticationToken
存储的数据到底有什么差异
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 的过程很曲折😔。