有没有遇到过这种事:认证中心不敢动(怕升级炸),业务服务却全升到 Boot 3.2,结果 Redis 共享数据成了"战场"? 我踩了一个月的坑,从 Jackson 异常狂轰滥炸,到 Redis 密码特殊符号闹剧,再到脏数据暗杀......今天把全过程写成这篇博客,轻松活泼版,保证你看完笑着就把坑避开!
为什么我会掉进这个大坑?
简单说:公司认证中心用 Boot 2.7 + 老 Authorization Server(0.2.3/0.4.x),稳如老狗,维护到 2028 年。 业务微服务全升 Boot 3.2,享受 VirtualThread、新特性。 共享 Redis 存 Session + OAuth2Authorization → 序列化不兼容 → 全线爆炸!
我当时想:"不就个序列化吗,加几个 Jackson Module 不就完了?" 结果......一个月后我才爬出来😂
我到底发现了哪些鬼畜问题?
-
Jackson 异常连环杀
- Cannot construct instance of SimpleGrantedAuthority(GrantedAuthority 变 record 了)
- Unrecognized field "settings"(老版本遗留字段)
- Cannot construct instance of ClientSettings/TokenSettings(无默认构造函数)
- The token generator failed to generate the access token(settings 为 null,最隐晦的坑!)
-
NPE 与 null 狂魔
- tokens 为 null → getAccessToken() NPE
- attributes.putAll(null) → HashMap NPE
- GenericJackson2JsonRedisSerializer 加 "value" 包装,导致测试和生产行为不一致
-
刷新令牌异常
- Type id handling not implemented for java.lang.Object
- Unrecognized field "settings"
- Problem deserializing 'setterless' property
- OAuth2AuthenticationException: The token generator failed
-
终极 Boss:脏数据
- Redis 里混进 Boot 3.2 写的数据(attributes 有 "java.security.Principal" key)
- Boot 2.7 读到直接吐血
我是怎么一步步爬出来的?(解决过程超搞笑)
-
Jackson 补丁大战 加了 SecurityJackson2Module、OAuth2LegacyJacksonModule 处理特殊类。 写了自定义 Deserializer,手动填充 tokens(因为老版本没有 setter)。 visibility 配置 FIELD ANY 让 Jackson 看到 private 字段。
-
NPE 猎杀时刻 attributes 判 null,authorizationGrantType 判 null。 发现 GenericJackson2JsonRedisSerializer 会加 "value" 包装 → 测试要用 serializer.serialize() 存数据。
-
自定义 Provider 后,移除默认的Provider
-
脏数据大清洗
一条命令解决 90% 问题:
Bash
cssredis-cli --scan --pattern "*:token::*" | xargs redis-cli del清理完重新登录,世界安静了😂
终极无敌配置(复制即用,2025 年老项目救星)
- PublicJsonRedisSerializer:springboot3与springboot2都使用这个类,Security公共 JSON 序列化器
arduino
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.databind.jsontype.BasicPolymorphicTypeValidator;
import com.fasterxml.jackson.databind.module.SimpleModule;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.security.oauth2.server.authorization.OAuth2Authorization;
@Configuration
public class PublicJsonRedisSerializer {
/**
* 公共 JSON 序列化器(我们打磨万年的终极版)
* 名字叫 publicJsonRedisSerializer,所有地方统一用这个
*/
@Bean
public GenericJackson2JsonRedisSerializer publicJsonRedisSerializer() {
ObjectMapper objectMapper = new ObjectMapper();
// Java8 时间支持
objectMapper.registerModule(new JavaTimeModule());
objectMapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
SimpleModule module = new SimpleModule("OAuth2AuthorizationModule");
module.addDeserializer(OAuth2Authorization.class, new OAuth2AuthorizationDeserializer());
objectMapper.registerModule(module);
// 全局忽略未知字段(治愈历史脏数据)
objectMapper.configure(com.fasterxml.jackson.databind.DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
objectMapper.configure(DeserializationFeature.READ_UNKNOWN_ENUM_VALUES_AS_NULL, true);
objectMapper.registerModule(new SecurityJackson2Module());
objectMapper.registerModule(new OAuth2LegacyJacksonModule());
// 安全的多态支持(Boot 3 推荐写法)
objectMapper.activateDefaultTyping(
BasicPolymorphicTypeValidator.builder()
.allowIfBaseType(Object.class)
.build(),
ObjectMapper.DefaultTyping.NON_FINAL,
JsonTypeInfo.As.PROPERTY
);
return new GenericJackson2JsonRedisSerializer(objectMapper);
}
}
RedisTemplate 强制用它,OAuth2AuthorizationService 也强制用它。
-
OAuth2AuthorizationDeserializer
因为 spring-security-oauth2-authorization-server 0.2.3 版本的反序列化设计太"反人类"了,Jackson 默认方式根本读不出来完整的 OAuth2Authorization 对象,尤其是 accessToken、refreshToken 等关键 token 会丢失,导致 getAccessToken() 返回 null,进而引发 NPE 或 introspect 失败。
核心问题(老版本 0.2.3 的"坑王设计")
-
JSON 格式是独立的字段
perl{ "accessToken": { "@class": "OAuth2Authorization$Token", "token": { ... } }, "refreshToken": { ... } }
-
类结构却是 private final tokens Map
swiftprivate final Map<Class<? extends OAuth2Token>, Token<?>> tokens = new HashMap<>(); // 没有 public setAccessToken() / setRefreshToken() // 唯一合法填充方式是 Builder.token(...) → build()
-
Jackson 默认行为
- 看到 "accessToken" 字段,但类里没有对应的 setter → 忽略!
- tokens 是 private final → 默认不填充
- 结果:反序列化后 tokens 为空 → getAccessToken()/getRefreshToken() 全是 null → 业务爆炸
完整代码
iniimport com.fasterxml.jackson.core.JsonParser; import com.fasterxml.jackson.databind.DeserializationContext; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.deser.std.StdDeserializer; import org.springframework.security.oauth2.core.AuthorizationGrantType; import org.springframework.security.oauth2.core.ClientAuthenticationMethod; import org.springframework.security.oauth2.core.OAuth2AccessToken; import org.springframework.security.oauth2.core.OAuth2RefreshToken; import org.springframework.security.oauth2.server.authorization.OAuth2Authorization; import org.springframework.security.oauth2.server.authorization.client.RegisteredClient; import java.io.IOException; import java.time.Instant; import java.util.HashMap; import java.util.HashSet; import java.util.Map; import java.util.Set; public class OAuth2AuthorizationDeserializer extends StdDeserializer<OAuth2Authorization> { public OAuth2AuthorizationDeserializer() { super(OAuth2Authorization.class); } @Override public OAuth2Authorization deserialize(JsonParser p, DeserializationContext ctxt) throws IOException { JsonNode root = p.getCodec().readTree(p); ObjectMapper mapper = (ObjectMapper) p.getCodec(); // 1. 创建一个最小的合法 dummy RegisteredClient(只为通过 Builder 校验) String registeredClientId = root.get("registeredClientId").asText("dummy"); RegisteredClient dummyClient = RegisteredClient.withId(registeredClientId) .clientId(registeredClientId) .clientSecret("{noop}secret") .clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC) .authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS) .scope("dummy") .build(); // 2. 开始构建 OAuth2Authorization.Builder builder = OAuth2Authorization.withRegisteredClient(dummyClient) .id(root.get("id").asText()) .principalName(root.get("principalName").asText()); // authorizationGrantType JsonNode grantNode = root.get("authorizationGrantType"); String grantTypeValue = grantNode.has("value") ? grantNode.get("value").asText() : "client_credentials"; builder.authorizationGrantType(new AuthorizationGrantType(grantTypeValue)); // authorizedScopes if (root.has("authorizedScopes")) { JsonNode scopesArray = root.get("authorizedScopes"); if (scopesArray.isArray() && scopesArray.size() > 1) { Set<String> scopes = new HashSet<>(); JsonNode actualScopes = scopesArray.get(1); for (JsonNode s : actualScopes) { scopes.add(s.asText()); } builder.authorizedScopes(scopes); } } // attributes(空就空,没事) if (root.has("attributes")) { Map<String, Object> attributes = mapper.treeToValue(root.get("attributes"), Map.class); builder.attributes(attrs -> attrs.putAll(attributes)); } // ========== 关键:手动解析 Token 节点,自己 new Token<>() ========== if (root.has("accessToken")) { JsonNode accessTokenNode = root.get("accessToken"); // 解析 OAuth2AccessToken JsonNode tokenNode = accessTokenNode.get("token"); String tokenValue = tokenNode.get("tokenValue").asText(); Instant issuedAt = tokenNode.has("issuedAt") ? Instant.parse(tokenNode.get("issuedAt").asText()) : null; Instant expiresAt = tokenNode.has("expiresAt") ? Instant.parse(tokenNode.get("expiresAt").asText()) : null; JsonNode tokenTypeNode = tokenNode.get("tokenType"); OAuth2AccessToken.TokenType tokenType = OAuth2AccessToken.TokenType.BEARER; Set<String> scopes = new HashSet<>(); if (tokenNode.has("scopes")) { JsonNode scopesWrapper = tokenNode.get("scopes"); if (scopesWrapper.isArray() && scopesWrapper.size() > 1) { JsonNode scopesArray = scopesWrapper.get(1); for (JsonNode s : scopesArray) { scopes.add(s.asText()); } } } OAuth2AccessToken accessToken = new OAuth2AccessToken(tokenType, tokenValue, issuedAt, expiresAt, scopes); // OAuth2AccessToken accessToken = mapper.treeToValue(tokenNode, OAuth2AccessToken.class); // 解析 metadata(如果有) Map<String, Object> metadata = new HashMap<>(); if (accessTokenNode.has("metadata")) { metadata = mapper.treeToValue(accessTokenNode.get("metadata"), Map.class); } // 解析 claims(如果有) Map<String, Object> claims = null; if (accessTokenNode.has("claims")) { claims = mapper.treeToValue(accessTokenNode.get("claims"), Map.class); } else if (metadata.containsKey(OAuth2Authorization.Token.CLAIMS_METADATA_NAME)) { claims = (Map<String, Object>) metadata.get(OAuth2Authorization.Token.CLAIMS_METADATA_NAME); } // 关键:正确调用 token 方法 if (claims != null) { Map<String, Object> finalClaims = claims; Map<String, Object> finalMetadata = metadata; builder.token(accessToken, meta -> { meta.putAll(finalMetadata); // 先放原有 metadata meta.put(OAuth2Authorization.Token.CLAIMS_METADATA_NAME, finalClaims); }); } else { Map<String, Object> finalMetadata = metadata; builder.token(accessToken, meta -> meta.putAll(finalMetadata)); } } if (root.has("refreshToken")) { JsonNode refreshTokenNode = root.get("refreshToken"); JsonNode tokenNode = refreshTokenNode.get("token"); String tokenValue = tokenNode.get("tokenValue").asText(); Instant issuedAt = tokenNode.has("issuedAt") ? Instant.parse(tokenNode.get("issuedAt").asText()) : null; Instant expiresAt = tokenNode.has("expiresAt") ? Instant.parse(tokenNode.get("expiresAt").asText()) : null; OAuth2RefreshToken refreshToken = new OAuth2RefreshToken(tokenValue, issuedAt, expiresAt); Map<String, Object> metadata = new HashMap<>(); if (refreshTokenNode.has("metadata")) { metadata = mapper.treeToValue(refreshTokenNode.get("metadata"), Map.class); } Map<String, Object> finalMetadata = metadata; builder.token(refreshToken, meta -> meta.putAll(finalMetadata)); } return builder.build(); } } -
-
SecurityJackson2Module
在 Spring Security(包括 Boot 2.7 和 3.x)里,用户权限是用 SimpleGrantedAuthority 表示的:
Java
arduinonew SimpleGrantedAuthority("ROLE_ADMIN")这个类长这样(简化版):
Java
arduinopublic class SimpleGrantedAuthority implements GrantedAuthority { private final String authority; // 只有这个字段,没有 setter! public SimpleGrantedAuthority(String authority) { this.authority = authority; } public String getAuthority() { return authority; } }关键点:
- 没有无参构造函数
- 没有 setter
- 只有 getter
当你用 Redis + JSON 序列化存用户权限(Session 或 OAuth2Authorization)时,JSON 会变成:
JSON
less{ "@class": "org.springframework.security.core.authority.SimpleGrantedAuthority", "authority": "ROLE_ADMIN" }Jackson 反序列化时傻眼了:
- 找不到无参构造函数 → 报 no Creators, like default constructor, exist
- 找不到 setter → 不知道怎么设置 authority 字段
结果:权限读不出来 → 用户登录后权限为空 → 接口 403 → 业务全炸!
完整的代码:
scalaimport com.fasterxml.jackson.core.JsonParser; import com.fasterxml.jackson.core.JsonToken; import com.fasterxml.jackson.databind.DeserializationContext; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.deser.std.StdDeserializer; import com.fasterxml.jackson.databind.module.SimpleModule; import org.springframework.security.core.authority.SimpleGrantedAuthority; import java.io.IOException; public class SecurityJackson2Module extends SimpleModule { public SecurityJackson2Module() { super(); // 同时兼容 Spring Boot 2.7 和 3.x 的 SimpleGrantedAuthority 反序列化 addDeserializer(SimpleGrantedAuthority.class, new SimpleGrantedAuthorityDeserializer()); } static class SimpleGrantedAuthorityDeserializer extends StdDeserializer<SimpleGrantedAuthority> { public SimpleGrantedAuthorityDeserializer() { super(SimpleGrantedAuthority.class); } @Override public SimpleGrantedAuthority deserialize(JsonParser p, DeserializationContext ctxt) throws IOException { // 支持两种格式:{"authority":"ROLE_ADMIN"} 或 直接字符串 "ROLE_ADMIN" if (p.currentToken() == JsonToken.VALUE_STRING) { return new SimpleGrantedAuthority(p.getText()); } JsonNode node = p.getCodec().readTree(p); String authority = null; if (node.isTextual()) { authority = node.asText(); } else if (node.has("authority")) { authority = node.get("authority").asText(); } return authority != null ? new SimpleGrantedAuthority(authority) : null; } } }
-
OAuth2LegacyJacksonModule
这个 OAuth2LegacyJacksonModule 才是我整个序列化大战的"救命稻草"!没有它,Boot 2.7 认证中心读 Redis 里的 OAuth2 数据,直接原地爆炸😂
核心问题(老 Authorization Server 的"遗产坑")
在老版本 spring-security-oauth2-authorization-server(0.2.x/0.4.x)里,几个关键类序列化格式超级"任性":
-
ClientAuthenticationMethod
- Boot 2.7 存的是字符串 "client_secret_basic"
- Boot 3.2 存的是对象 {"value": "client_secret_basic"}
- Boot 2.7 读 Boot 3.2 的数据 → Cannot construct instance (no delegate- or property-based Creator)
-
AuthorizationGrantType
- 同上,字符串 vs {"value": "..."} → 炸!
-
ClientSettings / TokenSettings
- Boot 3.2 是 record(无默认构造函数)
- Boot 2.7 读到 → no Creators, like default constructor, exist
-
老版本遗留字段 "settings"
- 早期版本有 "settings" 字段,后来拆成 clientSettings + tokenSettings
- 新代码读老数据 → Unrecognized field "settings"
结果:登录 → 登出 → 再登录,必炸!(因为登出清 Session,重新读 Redis 里的 RegisteredClient/OAuth2Authorization)
scalaimport com.fasterxml.jackson.core.JsonParser; import com.fasterxml.jackson.databind.DeserializationContext; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.deser.std.StdDeserializer; import com.fasterxml.jackson.databind.module.SimpleModule; import org.springframework.security.oauth2.core.AuthorizationGrantType; import org.springframework.security.oauth2.core.ClientAuthenticationMethod; import org.springframework.security.oauth2.server.authorization.settings.ClientSettings; import org.springframework.security.oauth2.server.authorization.settings.OAuth2TokenFormat; import org.springframework.security.oauth2.server.authorization.settings.TokenSettings; import java.io.IOException; import java.time.Duration; public class OAuth2LegacyJacksonModule extends SimpleModule { public OAuth2LegacyJacksonModule() { addDeserializer(ClientAuthenticationMethod.class, new ClientAuthenticationMethodDeserializer()); addDeserializer(AuthorizationGrantType.class, new AuthorizationGrantTypeDeserializer()); // 新增这两行,专治 ClientSettings 和 TokenSettings(Boot3 record 格式) addDeserializer(ClientSettings.class, new ClientSettingsDeserializer()); addDeserializer(TokenSettings.class, new TokenSettingsDeserializer()); } // ... 你之前已有的两个 Deserializer 不变 ... // 新增:兼容 Boot3 存的 ClientSettings static class ClientSettingsDeserializer extends StdDeserializer<ClientSettings> { public ClientSettingsDeserializer() { super(ClientSettings.class); } @Override public ClientSettings deserialize(JsonParser p, DeserializationContext ctxt) throws IOException { JsonNode node = p.getCodec().readTree(p); // Boot3 默认值:requireProofKey=false, requireAuthorizationConsent=true boolean requireProofKey = node.has("requireProofKey") && node.get("requireProofKey").asBoolean(); boolean requireAuthorizationConsent = node.has("requireAuthorizationConsent") ? node.get("requireAuthorizationConsent").asBoolean() : true; return ClientSettings.builder() .requireProofKey(requireProofKey) .requireAuthorizationConsent(requireAuthorizationConsent) .build(); } @Override public ClientSettings getNullValue(DeserializationContext ctxt) { return ClientSettings.builder() .requireAuthorizationConsent(true) // 你项目大多数情况都是 true .requireProofKey(false) .build(); } } // 新增:兼容 Boot3 存的 TokenSettings static class TokenSettingsDeserializer extends StdDeserializer<TokenSettings> { public TokenSettingsDeserializer() { super(TokenSettings.class); } @Override public TokenSettings deserialize(JsonParser p, DeserializationContext ctxt) throws IOException { JsonNode node = p.getCodec().readTree(p); TokenSettings.Builder builder = TokenSettings.builder(); // 1. accessTokenFormat:你代码里设的是 REFERENCE(不透明令牌) // Boot3 默认是 SELF_CONTAINED(JWT),所以必须显式处理 if (node != null && node.has("accessTokenFormat")) { JsonNode formatNode = node.get("accessTokenFormat"); if (formatNode.has("value")) { String value = formatNode.get("value").asText(); if ("reference".equalsIgnoreCase(value)) { builder.accessTokenFormat(OAuth2TokenFormat.REFERENCE); } else { builder.accessTokenFormat(OAuth2TokenFormat.SELF_CONTAINED); } } } else { // 默认跟你代码保持一致:REFERENCE(大部分资源服务器 + Redis 存储都用这个) builder.accessTokenFormat(OAuth2TokenFormat.REFERENCE); } // 2. accessTokenTimeToLive if (node != null && node.has("accessTokenTimeToLive")) { long seconds = node.get("accessTokenTimeToLive").asLong(); builder.accessTokenTimeToLive(Duration.ofSeconds(seconds)); } else { // 给一个兜底默认值(比如 1 小时),防止 null builder.accessTokenTimeToLive(Duration.ofHours(1)); } // 3. refreshTokenTimeToLive if (node != null && node.has("refreshTokenTimeToLive")) { long seconds = node.get("refreshTokenTimeToLive").asLong(); builder.refreshTokenTimeToLive(Duration.ofSeconds(seconds)); } else { builder.refreshTokenTimeToLive(Duration.ofDays(30)); // 常见默认值 } // 你如果还设置了 reuseRefreshTokens、idTokenSignatureAlgorithm 等,继续加即可 return builder.build(); } // 关键:即使 Redis 里完全没有 tokenSettings 字段,也要返回一个有效的默认实例! @Override public TokenSettings getNullValue(DeserializationContext ctxt) { return TokenSettings.builder() .accessTokenFormat(OAuth2TokenFormat.REFERENCE) // 跟你代码保持一致 .accessTokenTimeToLive(Duration.ofHours(1)) .refreshTokenTimeToLive(Duration.ofDays(30)) .build(); } } // 你之前已有的两个保持不变 static class ClientAuthenticationMethodDeserializer extends StdDeserializer<ClientAuthenticationMethod> { public ClientAuthenticationMethodDeserializer() { super(ClientAuthenticationMethod.class); } @Override public ClientAuthenticationMethod deserialize(JsonParser p, DeserializationContext ctxt) throws IOException { JsonNode node = p.getCodec().readTree(p); String value = node.isTextual() ? node.asText() : node.get("value").asText(); return new ClientAuthenticationMethod(value); } } static class AuthorizationGrantTypeDeserializer extends StdDeserializer<AuthorizationGrantType> { public AuthorizationGrantTypeDeserializer() { super(AuthorizationGrantType.class); } @Override public AuthorizationGrantType deserialize(JsonParser p, DeserializationContext ctxt) throws IOException { JsonNode node = p.getCodec().readTree(p); String value = node.isTextual() ? node.asText() : node.get("value").asText(); return new AuthorizationGrantType(value); } } } -
-
oAuth2AuthorizationRedisTemplate
为了不影响其他的业务信息,创建一个认证用的RedisTemplate,与认证相关的都用这个,原来的那些业务不变,当然如果你想要都使用json来保存业务数据也是可以的,按需来操作!
typescript/** * 主要用于存储 OAuth2Authorization,对授权的json序列化 * @param publicJsonRedisSerializer * @param redisConnectionFactory * @return */ @Bean public RedisTemplate<String, Object> oAuth2AuthorizationRedisTemplate(RedisSerializer<Object> publicJsonRedisSerializer, RedisConnectionFactory redisConnectionFactory) { RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>(); redisTemplate.setKeySerializer(new StringRedisSerializer()); redisTemplate.setHashKeySerializer(new StringRedisSerializer()); redisTemplate.setValueSerializer(publicJsonRedisSerializer); redisTemplate.setHashValueSerializer(publicJsonRedisSerializer); redisTemplate.setConnectionFactory(redisConnectionFactory); return redisTemplate; }重要,这边使用setValueSerializer与setHashValueSerializer都使用publicJsonRedisSerializer。
-
RedisOAuth2AuthorizationService
修改自己的redis认证服务类,我的可能跟大家的不一样,按照自己的来!下面仅供参考!
-
自定义Provider
iniimport org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.springframework.security.authentication.AuthenticationProvider; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.Authentication; import org.springframework.security.core.AuthenticationException; import org.springframework.security.oauth2.core.*; import org.springframework.security.oauth2.core.oidc.OidcIdToken; import org.springframework.security.oauth2.core.oidc.OidcScopes; import org.springframework.security.oauth2.core.oidc.endpoint.OidcParameterNames; import org.springframework.security.oauth2.jwt.Jwt; import org.springframework.security.oauth2.server.authorization.OAuth2Authorization; import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService; import org.springframework.security.oauth2.server.authorization.OAuth2TokenType; import org.springframework.security.oauth2.server.authorization.authentication.OAuth2AccessTokenAuthenticationToken; import org.springframework.security.oauth2.server.authorization.authentication.OAuth2ClientAuthenticationToken; import org.springframework.security.oauth2.server.authorization.authentication.OAuth2RefreshTokenAuthenticationToken; import org.springframework.security.oauth2.server.authorization.client.RegisteredClient; import org.springframework.security.oauth2.server.authorization.context.AuthorizationServerContextHolder; import org.springframework.security.oauth2.server.authorization.token.DefaultOAuth2TokenContext; import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenContext; import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenGenerator; import org.springframework.util.Assert; import java.util.Collections; import java.util.HashMap; import java.util.Map; import java.util.Set; public class OAuth2RefreshTokenAuthenticationProvider implements AuthenticationProvider { private static final String ERROR_URI = "https://datatracker.ietf.org/doc/html/rfc6749#section-5.2"; private static final OAuth2TokenType ID_TOKEN_TOKEN_TYPE = new OAuth2TokenType(OidcParameterNames.ID_TOKEN); private final Log logger = LogFactory.getLog(getClass()); private final OAuth2AuthorizationService authorizationService; private final OAuth2TokenGenerator<? extends OAuth2Token> tokenGenerator; private final UserDetailsService userDetailsService; /** * Constructs an {@code OAuth2RefreshTokenAuthenticationProvider} using the provided parameters. * * @param authorizationService the authorization service * @param tokenGenerator the token generator * @since 0.2.3 */ public OAuth2RefreshTokenAuthenticationProvider(OAuth2AuthorizationService authorizationService, OAuth2TokenGenerator<? extends OAuth2Token> tokenGenerator, UserDetailsService userDetailsService) { Assert.notNull(authorizationService, "authorizationService cannot be null"); Assert.notNull(tokenGenerator, "tokenGenerator cannot be null"); this.authorizationService = authorizationService; this.tokenGenerator = tokenGenerator; this.userDetailsService = userDetailsService; } @Override public Authentication authenticate(Authentication authentication) throws AuthenticationException { OAuth2RefreshTokenAuthenticationToken refreshTokenAuthentication = (OAuth2RefreshTokenAuthenticationToken) authentication; OAuth2ClientAuthenticationToken clientPrincipal = OAuth2AuthenticationProviderUtils.getAuthenticatedClientElseThrowInvalidClient(refreshTokenAuthentication); RegisteredClient registeredClient = clientPrincipal.getRegisteredClient(); if (this.logger.isTraceEnabled()) { this.logger.trace("Retrieved registered client"); } OAuth2Authorization authorization = this.authorizationService.findByToken( refreshTokenAuthentication.getRefreshToken(), OAuth2TokenType.REFRESH_TOKEN); if (authorization == null) { throw new OAuth2AuthenticationException(OAuth2ErrorCodes.INVALID_GRANT); } if (this.logger.isTraceEnabled()) { this.logger.trace("Retrieved authorization with refresh token"); } if (!registeredClient.getId().equals(authorization.getRegisteredClientId())) { throw new OAuth2AuthenticationException(OAuth2ErrorCodes.INVALID_CLIENT); } if (!registeredClient.getAuthorizationGrantTypes().contains(AuthorizationGrantType.REFRESH_TOKEN)) { throw new OAuth2AuthenticationException(OAuth2ErrorCodes.UNAUTHORIZED_CLIENT); } OAuth2Authorization.Token<OAuth2RefreshToken> refreshToken = authorization.getRefreshToken(); if (!refreshToken.isActive()) { // As per https://tools.ietf.org/html/rfc6749#section-5.2 // invalid_grant: The provided authorization grant (e.g., authorization code, // resource owner credentials) or refresh token is invalid, expired, revoked [...]. throw new OAuth2AuthenticationException(OAuth2ErrorCodes.INVALID_GRANT); } // As per https://tools.ietf.org/html/rfc6749#section-6 // The requested scope MUST NOT include any scope not originally granted by the resource owner, // and if omitted is treated as equal to the scope originally granted by the resource owner. Set<String> scopes = refreshTokenAuthentication.getScopes(); Set<String> authorizedScopes = authorization.getAuthorizedScopes(); if (!authorizedScopes.containsAll(scopes)) { throw new OAuth2AuthenticationException(OAuth2ErrorCodes.INVALID_SCOPE); } if (this.logger.isTraceEnabled()) { this.logger.trace("Validated token request parameters"); } if (scopes.isEmpty()) { scopes = authorizedScopes; } // AuthenticationManager authenticationManager = http.getSharedObject(AuthenticationManager.class); // Authentication usernamePasswordAuthentication = authenticationManager // .authenticate(new UsernamePasswordAuthenticationToken(authorization.getPrincipalName(),null)); //需要直接获取到登录用户信息 String principalName = authorization.getPrincipalName(); User user = (User) userDetailsService.loadUserByUsername(principalName); Authentication principalAuth = new UsernamePasswordAuthenticationToken(user, null, user.getAuthorities()); // @formatter:off DefaultOAuth2TokenContext.Builder tokenContextBuilder = DefaultOAuth2TokenContext.builder() .registeredClient(registeredClient) .principal(principalAuth) .authorizationServerContext(AuthorizationServerContextHolder.getContext()) .authorization(authorization) .authorizedScopes(scopes) .authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN) .authorizationGrant(refreshTokenAuthentication); // @formatter:on OAuth2Authorization.Builder authorizationBuilder = OAuth2Authorization.from(authorization); // ----- Access token ----- OAuth2TokenContext tokenContext = tokenContextBuilder.tokenType(OAuth2TokenType.ACCESS_TOKEN).build(); OAuth2Token generatedAccessToken = this.tokenGenerator.generate(tokenContext); if (generatedAccessToken == null) { OAuth2Error error = new OAuth2Error(OAuth2ErrorCodes.SERVER_ERROR, "The token generator failed to generate the access token.", ERROR_URI); throw new OAuth2AuthenticationException(error); } if (this.logger.isTraceEnabled()) { this.logger.trace("Generated access token"); } OAuth2AccessToken accessToken = new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER, generatedAccessToken.getTokenValue(), generatedAccessToken.getIssuedAt(), generatedAccessToken.getExpiresAt(), tokenContext.getAuthorizedScopes()); if (generatedAccessToken instanceof ClaimAccessor) { authorizationBuilder.token(accessToken, (metadata) -> { metadata.put(OAuth2Authorization.Token.CLAIMS_METADATA_NAME, ((ClaimAccessor) generatedAccessToken).getClaims()); metadata.put(OAuth2Authorization.Token.INVALIDATED_METADATA_NAME, false); }); } else { authorizationBuilder.accessToken(accessToken); } // ----- Refresh token ----- OAuth2RefreshToken currentRefreshToken = refreshToken.getToken(); if (!registeredClient.getTokenSettings().isReuseRefreshTokens()) { tokenContext = tokenContextBuilder.tokenType(OAuth2TokenType.REFRESH_TOKEN).build(); OAuth2Token generatedRefreshToken = this.tokenGenerator.generate(tokenContext); if (!(generatedRefreshToken instanceof OAuth2RefreshToken)) { OAuth2Error error = new OAuth2Error(OAuth2ErrorCodes.SERVER_ERROR, "The token generator failed to generate the refresh token.", ERROR_URI); throw new OAuth2AuthenticationException(error); } if (this.logger.isTraceEnabled()) { this.logger.trace("Generated refresh token"); } currentRefreshToken = (OAuth2RefreshToken) generatedRefreshToken; authorizationBuilder.refreshToken(currentRefreshToken); } // ----- ID token ----- OidcIdToken idToken; if (authorizedScopes.contains(OidcScopes.OPENID)) { // @formatter:off tokenContext = tokenContextBuilder .tokenType(ID_TOKEN_TOKEN_TYPE) .authorization(authorizationBuilder.build()) // ID token customizer may need access to the access token and/or refresh token .build(); // @formatter:on OAuth2Token generatedIdToken = this.tokenGenerator.generate(tokenContext); if (!(generatedIdToken instanceof Jwt)) { OAuth2Error error = new OAuth2Error(OAuth2ErrorCodes.SERVER_ERROR, "The token generator failed to generate the ID token.", ERROR_URI); throw new OAuth2AuthenticationException(error); } if (this.logger.isTraceEnabled()) { this.logger.trace("Generated id token"); } idToken = new OidcIdToken(generatedIdToken.getTokenValue(), generatedIdToken.getIssuedAt(), generatedIdToken.getExpiresAt(), ((Jwt) generatedIdToken).getClaims()); authorizationBuilder.token(idToken, (metadata) -> metadata.put(OAuth2Authorization.Token.CLAIMS_METADATA_NAME, idToken.getClaims())); } else { idToken = null; } authorization = authorizationBuilder.build(); this.authorizationService.save(authorization); if (this.logger.isTraceEnabled()) { this.logger.trace("Saved authorization"); } Map<String, Object> additionalParameters = Collections.emptyMap(); if (idToken != null) { additionalParameters = new HashMap<>(); additionalParameters.put(OidcParameterNames.ID_TOKEN, idToken.getTokenValue()); } if (this.logger.isTraceEnabled()) { this.logger.trace("Authenticated token request"); } return new OAuth2AccessTokenAuthenticationToken( registeredClient, clientPrincipal, accessToken, currentRefreshToken, additionalParameters); } @Override public boolean supports(Class<?> authentication) { return OAuth2RefreshTokenAuthenticationToken.class.isAssignableFrom(authentication); } } -
OAuth2AuthenticationProviderUtils
scssimport org.springframework.security.core.Authentication; import org.springframework.security.oauth2.core.OAuth2AuthenticationException; import org.springframework.security.oauth2.core.OAuth2ErrorCodes; import org.springframework.security.oauth2.core.OAuth2RefreshToken; import org.springframework.security.oauth2.core.OAuth2Token; import org.springframework.security.oauth2.server.authorization.OAuth2Authorization; import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationCode; import org.springframework.security.oauth2.server.authorization.authentication.OAuth2ClientAuthenticationToken; public class OAuth2AuthenticationProviderUtils { private OAuth2AuthenticationProviderUtils() { } static OAuth2ClientAuthenticationToken getAuthenticatedClientElseThrowInvalidClient(Authentication authentication) { OAuth2ClientAuthenticationToken clientPrincipal = null; if (OAuth2ClientAuthenticationToken.class.isAssignableFrom(authentication.getPrincipal().getClass())) { clientPrincipal = (OAuth2ClientAuthenticationToken) authentication.getPrincipal(); } if (clientPrincipal != null && clientPrincipal.isAuthenticated()) { return clientPrincipal; } throw new OAuth2AuthenticationException(OAuth2ErrorCodes.INVALID_CLIENT); } static <T extends OAuth2Token> OAuth2Authorization invalidate( OAuth2Authorization authorization, T token) { // @formatter:off OAuth2Authorization.Builder authorizationBuilder = OAuth2Authorization.from(authorization) .token(token, (metadata) -> metadata.put(OAuth2Authorization.Token.INVALIDATED_METADATA_NAME, true)); if (OAuth2RefreshToken.class.isAssignableFrom(token.getClass())) { authorizationBuilder.token( authorization.getAccessToken().getToken(), (metadata) -> metadata.put(OAuth2Authorization.Token.INVALIDATED_METADATA_NAME, true)); OAuth2Authorization.Token<OAuth2AuthorizationCode> authorizationCode = authorization.getToken(OAuth2AuthorizationCode.class); if (authorizationCode != null && !authorizationCode.isInvalidated()) { authorizationBuilder.token( authorizationCode.getToken(), (metadata) -> metadata.put(OAuth2Authorization.Token.INVALIDATED_METADATA_NAME, true)); } } // @formatter:on return authorizationBuilder.build(); } } -
AuthorizationServerConfiguration,任务服务的配置项,这个很重要,影响到token的刷新功能
lesshttp.apply(authorizationServerConfigurer.tokenEndpoint((tokenEndpoint) -> {// 个性化认证授权端点 //处理 OAuth2RefreshTokenAuthenticationToken 刷新令牌 OAuth2RefreshTokenAuthenticationProvider oAuth2RefreshTokenAuthenticationProvider = new OAuth2RefreshTokenAuthenticationProvider( authorizationService, oAuth2TokenGenerator(),slkjUserDetailsService); tokenEndpoint.accessTokenRequestConverter(accessTokenRequestConverter()) // 注入自定义的授权认证Converter .authenticationProviders(providers -> { // 移除默认的 OAuth2RefreshTokenAuthenticationProvider providers.removeIf(p -> p instanceof OAuth2RefreshTokenAuthenticationProvider); }) .authenticationProvider(slkjOAuth2RefreshTokenAuthenticationProvider) .accessTokenResponseHandler(successEventHandler) // 登录成功处理器 .errorResponseHandler(failureEventHandler);// 登录失败处理器 }).clientAuthentication(oAuth2ClientAuthenticationConfigurer -> // 个性化客户端认证需要再这边添加OAuth2RefreshTokenAuthenticationProvider,然后移除默认的 OAuth2RefreshTokenAuthenticationProvider
写在最后:给还在坑里的兄弟们
- 统一 JSON 序列化,JDK 序列化跨版本必炸!
- 清理脏数据,这是 90% 异常的元凶!
- 别加太多自定义 Module,容易把自己玩死😂
- 测试用 serializer.serialize() 存数据,别手写 JSON!
- 自定义 Provider 后,移除默认的Provider!
如果你也在混用 Boot 2.7 和 3.2,赶紧收藏这篇! 踩坑一个月,我哭了,你别哭了~,另外我的代码不一定跟您的一模一样,所以,仅供参考!