snackjson v4.0.58:用 TypeChecker 为 JsonPath 增加类型安全边界

2026-08-03 48 预计阅读时间: 1 分钟
来源: oschina.net AI 摘要 Original link

Disclaimer: This article is an AI-assisted summary. Read it together with the original source when precision matters. The summary may omit context, version differences, or edge cases and is not official documentation.

预计阅读时间:7 分钟

snackjson v4.0.58 已经发布。这个版本的重点不是增加一个新的查询语法,而是为 JsonPath 处理链路加入 TypeChecker 类型安全检测机制,让应用可以在运行时限制哪些 Java 类型能够参与 JSON 反序列化或相关类型转换。

对于需要处理外部 JSON、插件数据或多租户输入的服务来说,JsonPath 的便利性不能替代边界控制。TypeChecker 提供了一个可以集中配置的检查点:允许可信包中的类型继续处理,拒绝其他来源的类型。

为什么 JsonPath 需要类型检查

JsonPath 通常用于从 JSON 文档中定位节点,例如读取用户信息、订单明细或配置项。实际应用中,读取路径只是问题的一部分,框架还可能根据调用者指定的 Java 类型执行转换。

当输入来自 HTTP 请求、消息队列或第三方文件时,类型转换就应当具备明确的安全策略。应用可以根据包名、类名或更细粒度的规则判断目标类型是否可信:

  • 业务 DTO 可以放行;
  • 内部实现类、测试类或未知包可以拒绝;
  • 规则应集中配置,避免散落在每个调用点;
  • 默认策略应偏向最小权限,而不是无条件接受。

v4.0.58 提供的 TypeChecker 正适合承担这层职责。摘要中的示例通过类名判断包名前缀:com.demo 下的类型允许处理,其他类型拒绝处理。

一个可改造的配置示例

下面是一个基于发布摘要中 API 形式整理的 Java 配置示例。请根据项目中实际使用的 Options 创建方式和依赖坐标进行调整:

import com.alibaba.fastjson2.JSONReader;
import com.alibaba.fastjson2.TypeChecker;

public final class JsonSecurityConfig {
    private JsonSecurityConfig() {
    }

    public static JSONReader.Feature[] trustedTypeFeatures() {
        TypeChecker checker = clzName -> {
            if (clzName == null) {
                return TypeChecker.DENY;
            }

            // 只允许应用自己的 DTO 包参与类型处理。
            if (clzName.startsWith("com.demo.dto.")) {
                return TypeChecker.ALLOW;
            }

            return TypeChecker.DENY;
        };

        // 按当前 snackjson 版本的 Options API 注册 checker。
        Options options = new Options();
        options.addChecker(checker);

        return options.features();
    }

    public static void main(String[] args) {
        String json = "{\"name\":\"Ada\"}";
        UserDto user = JSON.parseObject(
                json,
                UserDto.class,
                trustedTypeFeatures()
        );
        System.out.println(user.name());
    }

    public record UserDto(String name) {
    }
}

示例中的 Options 只是用于表达配置位置的占位类型。如果当前项目的 snackjson 版本提供了不同的 Options 包名或初始化方式,应以项目 API 为准。关键点在于把 TypeChecker 注册到统一的解析配置中,而不是在业务代码里重复判断类名。

如果项目只允许一个明确的 DTO 包,规则应当尽量写成带结尾点号的前缀,例如 com.demo.dto.,而不是宽泛的 com.demo。这样可以避免误放行 com.demoevil 之类的相似包名。

规则设计时要注意什么

默认拒绝未知类型

对来自外部边界的数据,未知类名不应该自动获得权限。可以采用如下策略:

options.addChecker(clzName -> {
    if (clzName == null) {
        return TypeChecker.DENY;
    }
    return clzName.startsWith("com.demo.dto.")
            ? TypeChecker.ALLOW
            : TypeChecker.DENY;
});

如果业务确实需要支持多个包,建议显式列出允许范围,并为每个范围写测试,而不是直接返回 ALLOW

把检查器当作安全边界

TypeChecker 能限制类型处理范围,但它不是完整的输入校验方案。生产服务仍应同时完成:

  • JSON 字段和长度限制;
  • 业务字段校验;
  • 请求来源和权限认证;
  • 反序列化异常处理;
  • 对拒绝类型和异常输入进行日志记录,但避免记录敏感数据。

类型检查解决的是“允许处理哪些类型”,并不等价于“输入内容一定合法”。

为规则建立回归测试

至少覆盖允许类型、拒绝类型、空类名和相似包名四类场景:

import static org.junit.jupiter.api.Assertions.assertEquals;
import org.junit.jupiter.api.Test;

class TypeCheckerTest {
    @Test
    void onlyTrustedDtoPackageIsAllowed() {
        TypeChecker checker = clzName -> {
            if (clzName == null) {
                return TypeChecker.DENY;
            }
            return clzName.startsWith("com.demo.dto.")
                    ? TypeChecker.ALLOW
                    : TypeChecker.DENY;
        };

        assertEquals(TypeChecker.ALLOW,
                checker.check("com.demo.dto.UserDto"));
        assertEquals(TypeChecker.DENY,
                checker.check("com.demo.internal.UserMapper"));
        assertEquals(TypeChecker.DENY,
                checker.check("com.demoevil.dto.UserDto"));
        assertEquals(TypeChecker.DENY, checker.check(null));
    }
}

具体方法名可能因当前 API 定义而不同;测试表达的重点是规则本身,而不是把判断逻辑隐藏在框架调用中。

升级建议

升级到 v4.0.58 时,可以按这个顺序检查:

  1. 盘点项目中所有需要类型转换的 JsonPath 或 JSON 解析入口。
  2. 为可信 DTO 建立明确的包边界。
  3. 使用 TypeChecker 配置允许列表,默认拒绝未知类型。
  4. 对合法和非法类型分别编写测试。
  5. 在预发布环境观察拒绝日志和解析异常,再逐步扩大允许范围。

TypeChecker 的价值在于提供了一个集中、可审计的类型控制点。它不会替代权限、校验和异常处理,但可以让 JsonPath 的灵活性拥有更清晰的安全边界。对于处理不可信 JSON 的服务,升级时把这项能力纳入统一解析配置,通常比在各个业务方法中临时补规则更容易长期维护。


相关推荐