com.xxx.UserInfo not in the allowlist问题解决


一次由 Redis + Spring Security + Jackson 引发的"看不见的坑"

------ 从一个诡异错误到系统性解决的完整复盘


一、问题背景(发生了什么)

我们项目使用的是一套很常见的技术组合

  • Spring Boot
  • Spring Security
  • Spring Session + Redis
  • Jackson(JSON 序列化)

系统中有一个自定义的用户对象:

java 复制代码
UserInfo implements UserDetails

👉 这是 Spring Security 官方推荐的做法

现象一:系统突然启动报错

没有修改 UserInfo 类结构的情况下,系统启动或访问接口时报错:

复制代码
com.xxx.UserInfo not in the allowlist

错误发生在 Spring Security 从 Redis 反序列化 Session 的过程中。


二、第一次"误判"(为什么一开始很迷惑)

1️⃣ 我没改 UserInfo,为什么会报错?

直觉判断是:

  • ❌ Redis 有问题?
  • ❌ 类加载问题?
  • ❌ Spring Bug?

2️⃣ 神奇现象:执行 FLUSHDB 后,错误消失了

bash 复制代码
redis-cli
FLUSHDB

系统立刻恢复正常。

👉 这让人产生了一个非常危险的错觉

"那是不是 Redis 脏数据?

清一下就好了?"

这是第一个关键误区。


三、真正的原因(第一层真相)

1️⃣ Redis 里存的不是"数据",而是"对象快照"

Spring Session 会把**登录状态(SecurityContext)**序列化进 Redis。

Redis 里存的是类似这样的 JSON:

json 复制代码
{
  "principal": {
    "@class": "com.xxx.UserInfo",
    "username": "admin",
    ...
  }
}

2️⃣ Spring Security 做了一件"安全升级"

为了防止反序列化漏洞(反序列化 RCE),

Spring Security 只允许反序列化白名单中的类(allowlist)

👉 问题来了:

  • Spring Security 不信任你的自定义 UserInfo
  • 即使它实现了 UserDetails
  • 即使类真实存在

只要不在 allowlist,就拒绝反序列化


四、为什么 FLUSHDB 能"治好"?

因为:

  • FLUSHDB 清掉了 旧 Session
  • 系统不再需要反序列化旧的 UserInfo
  • 错误就"暂时消失"了

⚠️ 但这不是修复,是"把炸弹扔了"

一旦:

  • 服务重启
  • 多实例部署
  • Redis Session 再次被读取

👉 错误必然复发


五、第一次正式修复方案(方案二)

修复思路

告诉 Jackson:
"UserInfo 是安全的,可以反序列化"

技术手段:Jackson Mixin

java 复制代码
@JsonTypeInfo(
    use = JsonTypeInfo.Id.CLASS,
    include = JsonTypeInfo.As.PROPERTY,
    property = "@class"
)
public abstract class UserInfoMixin {}

并注册到 ObjectMapper


六、第二次错误(更诡异,也更隐蔽)

修完之后,登录没问题了。

普通接口突然开始报错

text 复制代码
JSON parse error:
Unexpected token (START_OBJECT),
expected VALUE_STRING:
need JSON String that contains type id
(for subtype of java.util.List)

错误出现在:

java 复制代码
@RequestBody List<FormDataVO>

👉 这和 Security、UserInfo 看起来完全没关系!


七、第二层真相(真正的根因)

1️⃣ 我们"误伤"了整个系统的 JSON 规则

问题出在这里:

java 复制代码
@Bean
public ObjectMapper objectMapper() {
    ObjectMapper mapper = new ObjectMapper();
    mapper.registerModules(SecurityJackson2Modules.getModules(...));
    mapper.addMixIn(UserInfo.class, UserInfoMixin.class);
    return mapper;
}

Spring Boot 的默认行为是:

只要你定义了一个 ObjectMapper Bean

👉 Spring MVC、@RequestBody、@ResponseBody

👉 全部都会用它


2️⃣ SecurityJackson2Modules 是"手术刀"

它的作用是:

  • 启用多态反序列化
  • 要求 JSON 中带 @class

这对 Redis Session 是必须的

但对 HTTP 请求 JSON 是灾难性的

于是:

json 复制代码
[
  { "field": "a", "value": "b" }
]

在 Jackson 看来变成了:

❌ "你这是 List?

那 @class 在哪?"


八、最终正确解法(生产级)

核心原则(请记住)

Security 的 ObjectMapper
≠ MVC 的 ObjectMapper


正确做法:只给 Redis 用专用 ObjectMapper

java 复制代码
@Configuration
public class RedisSessionConfig {

    @Bean
    public RedisSerializer<Object> springSessionRedisSerializer() {
        ObjectMapper mapper = new ObjectMapper();

        // 只用于 Spring Session / Security
        mapper.registerModules(
            SecurityJackson2Modules.getModules(
                getClass().getClassLoader()
            )
        );

        mapper.addMixIn(UserInfo.class, UserInfoMixin.class);

        return new GenericJackson2JsonRedisSerializer(mapper);
    }
}

✅ 不定义全局 ObjectMapper

✅ 不影响 Spring MVC

✅ Security 问题彻底解决


九、完整"踩坑 → 修坑"路线总结

阶段 做了什么 结果
初始 Redis 存 UserInfo 升级后反序列化失败
FLUSHDB 清 Redis 暂时不报错(假象)
方案二 放行 UserInfo Security 正常
误操作 ObjectMapper 设为全局 MVC JSON 崩溃
最终方案 Redis 专用 ObjectMapper 全系统稳定

十、给初级开发者的三条铁律

✅ 铁律一

FLUSHDB 不是修复,是掩盖问题


✅ 铁律二

Security 的序列化配置,永远不要直接影响 MVC


✅ 铁律三

看到以下关键词同时出现,立刻警惕:

  • Redis Session
  • Spring Security
  • Jackson
  • allowlist / type id

👉 99% 是序列化边界问题


抽象类UserInfoMixin的作用是什么?


一、一句话先给结论(先记住)

java 复制代码
@JsonTypeInfo(
    use = JsonTypeInfo.Id.CLASS,
    include = JsonTypeInfo.As.PROPERTY,
    property = "@class"
)

作用只有一个:

在 JSON 里明确写清楚"这个对象原本是哪一个 Java 类",
让 Jackson 在反序列化时知道"该 new 谁"。


二、为什么 Jackson "不知道该 new 谁"?

先看一个很普通的 Java 场景

java 复制代码
UserDetails user = new UserInfo();

在 Java 运行时:

  • 变量类型:UserDetails
  • 实际对象:UserInfo

👉 这是多态


但是 JSON 是"无类型语言"

如果你把 user 转成 JSON:

json 复制代码
{
  "username": "admin",
  "roles": ["ADMIN"]
}

你现在问 Jackson:

"请你把这段 JSON 再变回 UserDetails"

Jackson 会懵:

❓ 是 UserInfo

❓ 还是 User

❓ 还是别的实现类?

👉 JSON 里没有答案


三、@JsonTypeInfo 是怎么解决这个问题的?

它的作用是:

把"真实 Java 类型"直接写进 JSON


加上这个注解后,JSON 会变成这样

json 复制代码
{
  "@class": "com.hectdi.springsecurity.vo.UserInfo",
  "username": "admin",
  "roles": ["ADMIN"]
}

现在 Jackson 再反序列化时:

  1. 看到 @class
  2. 读到 "com.hectdi.springsecurity.vo.UserInfo"
  3. 用反射 new UserInfo()
  4. 填充字段

👉 精准无误


四、逐个解释这几个参数(非常重要)

java 复制代码
@JsonTypeInfo(
    use = JsonTypeInfo.Id.CLASS,
    include = JsonTypeInfo.As.PROPERTY,
    property = "@class"
)

1️⃣ use = JsonTypeInfo.Id.CLASS

意思:

类型信息用 "完整类名" 表示

json 复制代码
"@class": "com.hectdi.springsecurity.vo.UserInfo"

(而不是用别名)


2️⃣ include = JsonTypeInfo.As.PROPERTY

意思:

把类型信息作为 普通 JSON 字段 放进去

json 复制代码
{
  "@class": "...",
  "username": "admin"
}

而不是:

json 复制代码
["com.xxx.UserInfo", { ... }]

3️⃣ property = "@class"

意思:

这个字段名就叫 @class


五、那为什么不用直接在 UserInfo 上加这个注解?

你用了 Mixin

java 复制代码
public abstract class UserInfoMixin {}

Mixin 的作用是:

"不改原类,也能给它'临时贴注解'"

原因通常有三种:

  1. UserInfo 来自公共模块,不方便改
  2. 不想污染领域模型
  3. 只在某些序列化场景需要

👉 这在框架级开发中是标准做法


六、结合你这次问题,UserInfoMixin 的真实作用

回到你的错误场景

  • Redis 里存的是 Authentication.principal
  • 类型是 UserDetails
  • 实际对象是 UserInfo

反序列化时:

  • Spring Security 说:
    👉 "我需要知道你到底是哪一个具体类"
  • Jackson 说:
    👉 "JSON 里没写类型,我不敢 new"

于是炸了 💥


加了 UserInfoMixin 之后

  1. Redis 里的 JSON 变成:
json 复制代码
"principal": {
  "@class": "com.hectdi.springsecurity.vo.UserInfo",
  ...
}
  1. Spring Security allowlist 校验通过
  2. Jackson 成功反序列化
  3. Session 恢复正常

七、一个非常重要的⚠️警告(你已经踩过)

@JsonTypeInfo 是"核武器"级别的注解

为什么?

因为:

  • 它会改变 JSON 的结构
  • 会影响反序列化规则
  • 一旦用在 MVC ObjectMapper
  • 所有请求 JSON 都可能炸

👉 所以你最后的做法是对的:

只给 Redis Session 用,不给 MVC 用


八、给初级开发者的记忆口诀

Java 有多态,JSON 没有;
要想反序列化,类型必须写。
安全模块能信谁,
必须白名单 + 明示类。


相关推荐
IT 行者4 天前
Spring Security 6.x CSRF Token增强:从XorCsrfTokenRequestAttributeHandler到安全实践
安全·spring·spring security·csrf
zs宝来了5 天前
Spring Security源码深度解析:从FilterChainProxy到SecurityContext的认证流程
spring security·源码解析·java后端·安全框架·认证流程·filterchainproxy·securitycontext
阿拉斯攀登15 天前
设计模式:责任链模式(Spring Security)
设计模式·spring security·责任链模式
青鱼入云23 天前
@JsonValue和@JsonCreator介绍
json·jackson
xiegwei1 个月前
spring security 方法安全@PreAuthorize实现从方法参数中获取数据并判断权限
spring security
TracyCoder1231 个月前
SpringSecurity 技术原理深度解析:认证、授权与 Filter 链机制
spring security
xiegwei1 个月前
spring security oauth2 集成异常处理
数据库·spring·spring security
豆奶特浓61 个月前
Java面试生死局:谢飞机遭遇在线教育场景,从JVM、Spring Security到AI Agent,他能飞吗?
java·jvm·微服务·ai·面试·spring security·分布式事务
努力发光的程序员1 个月前
互联网大厂Java面试:从Spring Boot到微服务架构
spring boot·缓存·微服务·消息队列·rabbitmq·spring security·安全框架