从fastjson迁移到Jackson:反序列化漏洞与Java JSON库选型思考
最近几年做 Java 后端有一件事让我越来越坚定新项目一律不用 fastjson老项目只要有排期就迁移到 Jackson。这不是跟风也不是对阿里开源组件有偏见而是从漏洞修复节奏、autoType 机制风险、团队维护成本三个维度反复权衡之后做出的技术决策。这篇就把“为什么禁用 fastjson”梳理完整。文章不涉及具体业务敏感信息只讲技术判断、漏洞背景、迁移思路和排查方法。如果你正在犹豫要不要把项目里 fastjson 换掉或者被 fastjson 反序列化漏洞折腾过可以直接用这篇文章作为参考。1. fastjson 核心能力速览先把 fastjson 本身的能力和问题放在同一张表里看方便快速评估。能力项说明项目类型Java 高性能 JSON 处理库由阿里巴巴开源主要功能JSON 序列化、反序列化、JSONPath、与 Spring Boot 集成历史定位以“速度快、API 简单”著称早期在国内 Java 项目中使用率很高核心风险autoType 反序列化机制多次爆出远程命令执行漏洞典型漏洞fastjson 1.2.24、1.2.47、1.2.68、1.2.80 等版本均出现过反序列化绕过问题版本现状1.2.84 是 1.x 系列的修复版本但社区仍不建议在核心链路使用官方态度阿里云在 2020 年曾公开发布 fastjson 漏洞风险提示建议低版本升级或迁移替代方案Jackson 是 Spring Boot 默认 JSON 库生态完整漏洞响应稳定适合场景遗留系统维护期可做升级加固新系统不建议选型 fastjson迁移成本常规 DTO 的序列化注解迁移成本不高特殊特性需要做兼容改造不用记太多版本号你只需要理解一个关键结论fastjson 的反序列化安全问题不是单次漏洞而是 autoType 机制带来的长期结构性风险。下面展开讲。2. 为什么 fastjson 反序列化漏洞一直修不完2.1 autoType 机制是问题的根源先明确一个概念JSON 反序列化本质上就是把 JSON 字符串还原成 Java 对象。正常情况下这没什么问题。但 fastjson 为了支持多态设计了一个autoType机制允许在 JSON 字符串中通过type字段指定具体的类名。{ type: com.example.Person, name: zhangsan }这样写的本意是方便反序列化时自动识别类型但问题也随之而来攻击者可以控制type指向任意类。如果目标类本身存在可利用的 getter、setter 或构造方法攻击者就可能构造恶意输入最终触发远程命令执行RCE。简单说autoType 打开了一扇门而这扇门很难保证每次都能精准拦截恶意类。2.2 绕过与修复的拉锯战fastjson 的反序列化漏洞从 1.2.24 开始陆续暴露。官方每修复一次安全研究者就继续寻找新的利用链形成一种“修复—绕过—再修复—再绕过”的循环。版本问题描述修复状态1.2.24爆出经典反序列化 RCE 漏洞升级到 1.2.251.2.47autoType 绕过影响大量线上系统紧急升级到 1.2.481.2.68又出现新的绕过利用方式升级到 1.2.691.2.80高危漏洞通报影响面广升级到 1.2.831.2.84官方继续修复并发布 1.2.84建议继续关注安全公告这里要特别说一句1.2.84 并不是终点。它只是 1.x 时代又一个新的修复版本不意味着从此以后绝对安全。任何依赖 autoType 机制的 JSON 库都要持续面对“新利用链”的挑战。这也是很多团队最终选择迁移的根本原因——不想把安全水位押在某个组件的补丁速度上。2.3 默认配置真的安全吗fastjson 从某个版本开始默认关闭了 autoType但这不代表万事大吉。很多老项目在早期接入 fastjson 时为了处理多态对象会手动打开 autoType或者使用了ParserConfig.getGlobalInstance()去添加白名单。一旦配置不当就会重新暴露风险。更隐蔽的情况是项目代码没有显式开启 autoType但引用的第三方组件内部调用了 fastjson 的特定反序列化入口攻击面依然存在。所以从依赖治理的角度看只要 fastjson 还在依赖树里风险就不会自动消失。3. 从工程角度评估禁用 fastjson 的三点原因3.1 漏洞响应压力大后端团队最怕的不是出漏洞而是出漏洞之后需要紧急排查所有服务。fastjson 每次出高危漏洞运维和开发都要联动排查哪些服务依赖了 fastjson版本是否在受影响范围内攻击者是否可能触达反序列化入口如果暂时无法升级要不要上 waf 规则这种排查一次两次还行但 fastjson 的漏洞通报频率明显高于 Jackson长期下来团队会疲于应付。3.2 与 Spring Boot 生态重合新项目只要使用 Spring Boot默认的 JSON 序列化/反序列化库就是 Jackson。也就是说你不引入 fastjson框架本身已经有一套完整且维护积极的 JSON 处理方案。额外引入 fastjson相当于给项目增加一个重复的 JSON 处理引擎同时增加一份安全维护成本。在一次技术方案评审中我给出的建议很直接如果只是为了“JSON 解析速度快一点”而引入 fastjson这个收益在绝大多数业务系统里感受不到但安全风险和依赖维护成本却是长期存在的。技术选型不能只看单点性能要看整个依赖生命周期的成本。3.3 代码可维护性fastjson 的 API 确实简单JSON.parseObject()、JSON.toJSONString()用起来很顺手。但它的很多高级特性比如JSONField的序列化顺序、jsonType的多态处理、SerializeConfig定制属于 fastjson 特有的 API。一旦后续要迁移这些特性都需要逐个改造。如果项目里到处都是com.alibaba.fastjson.JSONObject这种类型迁移到 Jackson 时不仅要改工具类还要改大量业务代码。越早意识到这个问题迁移成本越低。4. fastjson 迁移为 Jackson 的实操路径4.1 先盘点依赖位置在动手迁移之前先全局搜索代码里com.alibaba.fastjson的引用。常用的命令grep -R com.alibaba.fastjson --include*.java .同时检查 Maven 或 Gradle 依赖树确认 fastjson 是直接依赖还是传递依赖。# Maven 项目查看 fastjson 依赖来源 mvn dependency:tree -Dincludescom.alibaba:fastjson如果是传递依赖需要找到是哪个中间件引入了 fastjson再决定是排除依赖还是替换中间件。4.2 替换工具类最常用的是把 fastjson 的序列化、反序列化调用替换成 Jackson 的 ObjectMapper。下面是三组最典型的替换方式。fastjson 写法// fastjson 序列化 String json JSON.toJSONString(user); // fastjson 反序列化 User user JSON.parseObject(json, User.class); // fastjson 解析为 JSONObject JSONObject obj JSON.parseObject(json);Jackson 写法import com.fasterxml.jackson.databind.ObjectMapper; ObjectMapper objectMapper new ObjectMapper(); // Jackson 序列化 String json objectMapper.writeValueAsString(user); // Jackson 反序列化 User user objectMapper.readValue(json, User.class); // Jackson 解析为 JsonNode JsonNode node objectMapper.readTree(json);如果项目里已经注入了 Spring Boot 的 ObjectMapper建议直接用容器中的实例不要每一次都new ObjectMapper()这样能保证全局配置一致。4.3 处理 fastjson 特有注解fastjson 里常用JSONField控制属性名和序列化顺序。在 Jackson 里对应的是JsonProperty和JsonPropertyOrder。fastjson 写法public class User { JSONField(name user_name) private String username; JSONField(serialize false) private String password; JSONField(ordinal 1) private int age; }Jackson 写法public class User { JsonProperty(user_name) private String username; JsonIgnore private String password; JsonPropertyOrder private int age; }需要注意的是JSONField里serialize false只是不序列化反序列化时仍可能接收该字段JsonIgnore则是序列化和反序列化都忽略。如果业务要求不完全一致迁移时要逐个核对字段行为。4.4 多态类型处理fastjson 依赖type实现多态Jackson 对应的机制是JsonTypeInfo。虽然效果类似但 JSON 格式会有差异如果接口是对外提供的需要考虑兼容问题。Jackson 多态示例JsonTypeInfo(use JsonTypeInfo.Id.NAME, include JsonTypeInfo.As.PROPERTY, property type) JsonSubTypes({ JsonSubTypes.Type(value Cat.class, name cat), JsonSubTypes.Type(value Dog.class, name dog) }) public abstract class Animal { private String name; } public class Cat extends Animal { private Integer catchRate; }迁移多态逻辑最好单独立个任务不要和普通 DTO 替换混在一起改否则出了问题也不好定位。5. 接口 API 与批量任务场景替代很多系统里 fastjson 不只用于普通的 JSON 解析还承担了批量数据解析和接口调用中的自定义格式处理。这类场景迁移时要额外关注。5.1 批量 JSON 行解析日志清洗和数据同步任务经常遇到“每行一个 JSON 对象”的输入文件。fastjson 处理方式可以直接按行 parse而 Jackson 没有专门的readLine方法需要手动按行读取再反序列化。这里给一个通用示例。fastjson 写法try (BufferedReader reader new BufferedReader(new FileReader(file))) { String line; while ((line reader.readLine()) ! null) { Order order JSON.parseObject(line, Order.class); processOrder(order); } }Jackson 写法ObjectMapper objectMapper new ObjectMapper(); try (BufferedReader reader new BufferedReader(new FileReader(file))) { String line; while ((line reader.readLine()) ! null) { Order order objectMapper.readValue(line, Order.class); processOrder(order); } }如果文件很大可以考虑分批提交、失败重试、记录行号断点续跑。迁移时保留同样的批量任务语义即可。5.2 统一封装 API 响应解析如果项目里用 fastjson 解析第三方接口返回的 JSON迁移时建议封装一个统一的外部接口响应解析类。这样既能统一异常处理也方便后续替换底层 JSON 库。Component public class JsonParseService { private final ObjectMapper objectMapper; public JsonParseService(ObjectMapper objectMapper) { this.objectMapper objectMapper; } public T T parseObject(String json, ClassT clazz) { try { return objectMapper.readValue(json, clazz); } catch (JsonProcessingException e) { throw new BusinessException(JSON 解析失败, e); } } public JsonNode parseTree(String json) { try { return objectMapper.readTree(json); } catch (JsonProcessingException e) { throw new BusinessException(JSON 解析失败, e); } } }封装之后即使后续再换 JSON 库业务侧代码也不需要大动。6. 资源占用与性能观察很多人一开始舍不得换 fastjson核心原因是“性能”。这里客观说一句fastjson 的基准性能在部分场景确实有优势但差距并不像宣传中那么大尤其在真实业务请求中序列化/反序列化很少是系统瓶颈。如果项目里确实有大数据量解析的需求迁移后建议做一轮基础观察重点看下面几个维度观察点方法关注指标单次解析耗时JUnit 基准测试1000 次、10000 次解析平均耗时GC 压力压测时开启 GC 日志Full GC 频率CPU 消耗压测环境 top 命令服务 CPU 占用率内存占用压测后观察堆内存大 JSON 解析时是否有明显内存增长一个客观经验是大多数业务接口的 JSON 体量在几十 KB 以内Jackson 的性能完全够用。除非是超大规模 JSON 流式解析而且压测证明 fastjson 有显著收益否则不建议为了微小的性能差异承担安全风险。如果数据量真的很大更合理的做法是使用 Jackson 的流式 APIJsonParser或者增加 DTO 字段裁剪而不是依赖 JSON 库的单点性能。7. 常见问题与排查方法迁移 fastjson 到 Jackson 的过程中最容易遇到下面这些问题。问题现象可能原因排查方式解决方案反序列化后字段为 nullfastjson 的JSONField与 Jackson 的JsonProperty名称没对齐对比新旧字段映射统一使用JsonProperty核对 name 值接口输出 JSON 字段顺序变了fastjson 默认按类字段声明顺序输出Jackson 默认按字母序对比迁移前后接口响应使用JsonPropertyOrder或配置PropertyNamingStrategyLocalDateTime 格式化异常fastjson 支持自定义日期格式Jackson 需要额外模块查看异常堆栈没有 JavaTimeModule引入 jackson-datatype-jsr310 并注册模块多态反序列化失败fastjson 使用typeJackson 需要显式配置JsonTypeInfo查看反序列化异常提示增加JsonTypeInfo和JsonSubTypes循环引用的对象序列化报错fastjson 默认处理循环引用Jackson需要手动配置报错信息包含Infinite recursion配置SerializationFeature.FAIL_ON_SELF_REFERENCES或使用JsonManagedReference字段为 null 时被过滤fastjson 默认不输出 null 字段Jackson 默认输出对比新旧输出结果配置setSerializationInclusion(JsonInclude.Include.NON_NULL)这里特别强调第三个问题Spring Boot 项目中如果直接使用手动创建的new ObjectMapper()不会自动注册 JavaTimeModule所以 LocalDateTime 序列化会报错。推荐使用 Spring Boot 容器中的ObjectMapper它已经自动配置好常见模块。8. 不适合单方面禁用 fastjson 的场景写这篇文章必须客观。并不是所有情况都适合立刻禁用 fastjson。如果遇到下面这些场景建议先做评估再动工。8.1 深度使用 fastjson 特有的 JSONPath 表达式fastjson 的JSONPath支持通过路径表达式快速读取嵌套 JSON 值比如$.store.book[0].titleJackson 也提供了JsonNode.at()方法但语法不完全一致。如果项目里大量使用了复杂的 JSONPath 表达式迁移成本会明显提升。这种情况建议先梳理 JSONPath 的使用范围再决定是否整体迁移。8.2 老旧系统缺乏自动化测试覆盖如果一个服务已经运行了好几年既没有接口测试也没有核心链路的自动化回归那么大规模替换 JSON 库可能带来隐性故障。更稳妥的做法是先在非核心服务做迁移试点稳定运行一两个迭代后再推动其他服务。8.3 第三方中间件强绑定 fastjson有些中间件 SDK 内部硬编码了 fastjson这时候项目代码里即使没有直接引用也会因为传递依赖存在 fastjson。强行排除依赖可能导致中间件运行时异常。这种情况需要优先推动中间件升级而不是在项目里强行剔除。9. 最佳实践与团队规范建议最后这部分写给技术负责人和正在做技术规范的开发者。禁用 fastjson 不是目的建立一套健康的依赖治理机制才是目的。9.1 新增项目默认使用 Jackson技术文档里可以直接写一条规范新项目默认依赖 Jackson禁止新增 fastjson 依赖。如果确实有特殊需求需要提交技术评审说明理由和风险缓解方案。9.2 存量项目建立迁移排期存量项目按风险等级分批迁移优先级项目类型处理方案P0直接对外提供接口、有公网入口的服务尽快评估迁移优先处理P1内部系统 API 或核心数据链路服务纳入近期迭代排期P2离线任务、内部工具、无公网暴露观察期后统一处理9.3 定期扫描依赖安全漏洞可以把 fastjson 或其他高危组件的版本检查接入 CI 流程在合并代码前自动检查依赖中是否存在已知漏洞。工具层面可以用 OWASP Dependency-Check 或 Snyk也可以使用公司内部已有的依赖扫描平台。# 检查 Maven 依赖漏洞示例 mvn org.owasp:dependency-check-maven:check9.4 序列化代码统一收口无论是 fastjson 还是 Jackson业务代码里直接散落大量 JSON 解析调用都是不健康的。建议把序列化/反序列化能力封装在统一的基础组件中底层切换 JSON 库时业务层无感。这也是迁移成本最低的一种组织方式。9.5 涉及数据安全与合法合规的提醒如果系统涉及用户个人信息、人脸信息、声音数据或版权素材请务必在代码层面和制度层面同时做好授权核验。JSON 解析库本身是中立的但反序列化漏洞一旦被利用可能造成敏感数据泄露引用合规审计要求的技术团队应当足够重视。10. 总结对 fastjson 的技术决策建议回到标题的问题我为什么禁用 fastjson。总结起来答案并不复杂fastjson 的 autoType 反序列化机制存在长期结构性风险漏洞修复呈现“追赶模式”而 Java 生态里已经有足够成熟的 Jackson 可以替代。对于绝大多数以 Spring Boot 为核心的业务系统放弃 fastjson 不会损失核心能力反而能降低安全运维负担。如果你正在评估是否迁移建议先从这几个问题入手项目里 fastjson 的直接依赖是什么版本是否低于 1.2.84代码中是否出现过ParserConfig、type、autoType相关配置项目是否对外暴露了接受 JSON 参数的接口依赖树中 fastjson 是直接依赖还是传递依赖如果答案里有任何一项风险倾向建议尽早安排迁移。先把 Jackson 跑通一个非核心接口对比任务输出无误后再扩大范围。记住一点迁移 JSON 库不是功能开发它是一次安全加固投资。在漏洞面前越早动手越主动。