🥯2025 年终极避坑指南:Spring Boot 2.7 + 3.2 混合集群的 Redis + OAuth2 序列化血泪史

有没有遇到过这种事:认证中心不敢动(怕升级炸),业务服务却全升到 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 不就完了?" 结果......一个月后我才爬出来😂

我到底发现了哪些鬼畜问题?

  1. 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,最隐晦的坑!)
  2. NPE 与 null 狂魔

    • tokens 为 null → getAccessToken() NPE
    • attributes.putAll(null) → HashMap NPE
    • GenericJackson2JsonRedisSerializer 加 "value" 包装,导致测试和生产行为不一致
  3. 刷新令牌异常

    • Type id handling not implemented for java.lang.Object
    • Unrecognized field "settings"
    • Problem deserializing 'setterless' property
    • OAuth2AuthenticationException: The token generator failed
  4. 终极 Boss:脏数据

    • Redis 里混进 Boot 3.2 写的数据(attributes 有 "java.security.Principal" key)
    • Boot 2.7 读到直接吐血

我是怎么一步步爬出来的?(解决过程超搞笑)

  1. Jackson 补丁大战 加了 SecurityJackson2Module、OAuth2LegacyJacksonModule 处理特殊类。 写了自定义 Deserializer,手动填充 tokens(因为老版本没有 setter)。 visibility 配置 FIELD ANY 让 Jackson 看到 private 字段。

  2. NPE 猎杀时刻 attributes 判 null,authorizationGrantType 判 null。 发现 GenericJackson2JsonRedisSerializer 会加 "value" 包装 → 测试要用 serializer.serialize() 存数据。

  3. 自定义 Provider 后,移除默认的Provider

  4. 脏数据大清洗

    一条命令解决 90% 问题:

    Bash

    css 复制代码
     redis-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

      swift 复制代码
       private 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 → 业务爆炸

    完整代码

    ini 复制代码
     import 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

    arduino 复制代码
     new SimpleGrantedAuthority("ROLE_ADMIN")

    这个类长这样(简化版):

    Java

    arduino 复制代码
     public 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 → 业务全炸!

    完整的代码

    scala 复制代码
     import 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)里,几个关键类序列化格式超级"任性":

    1. 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)
    2. AuthorizationGrantType

      • 同上,字符串 vs {"value": "..."} → 炸!
    3. ClientSettings / TokenSettings

      • Boot 3.2 是 record(无默认构造函数)
      • Boot 2.7 读到 → no Creators, like default constructor, exist
    4. 老版本遗留字段 "settings"

      • 早期版本有 "settings" 字段,后来拆成 clientSettings + tokenSettings
      • 新代码读老数据 → Unrecognized field "settings"

    结果:登录 → 登出 → 再登录,必炸!(因为登出清 Session,重新读 Redis 里的 RegisteredClient/OAuth2Authorization)

    scala 复制代码
     import 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

    ini 复制代码
     import 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

    scss 复制代码
     import 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的刷新功能

    less 复制代码
     http.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,赶紧收藏这篇! 踩坑一个月,我哭了,你别哭了~,另外我的代码不一定跟您的一模一样,所以,仅供参考!

相关推荐
兔丝2 小时前
ThinkPHP8 常见并发场景解决方案文档
redis·后端
陌路物是人非2 小时前
记一个 @Resource BUG
java·开发语言·bug
晴天飛 雪2 小时前
Spring Boot 上传shp压缩包解析多少地块
java·spring boot
superman超哥2 小时前
Rust 闭包的定义与捕获:所有权系统下的函数式编程
开发语言·后端·rust·函数式编程·rust闭包·闭包的定义与捕获
曹牧2 小时前
Java:Math.abs()‌
java·开发语言·算法
期待のcode2 小时前
Java的泛型
java·开发语言
FPGAI2 小时前
Java学习之计算机存储规则、数据类型、标识符、键盘录入、IDEA
java·学习
AC赳赳老秦2 小时前
pbootcms模板后台版权如何修改
java·开发语言·spring boot·postgresql·测试用例·pbootcms·建站
落枫592 小时前
如何快速搭建一个JAVA持续交付环境
后端·github