先看结论与判断条件

  • 按语法解析、字段映射、规范化、输入约束、业务不变量和远端授权六层拆分,不按 Parser 或 Validator 类名划线。
  • 通用解析器、生成序列化器、反射入口和错误定位默认保持在 VMP 外,确保格式失败可观察、可分类并先于副作用。
  • 规范化必须在签名、缓存键和业务比较前完成,使用显式版本与确定性字节规则,不能顺便填充授权字段。
  • 只把稳定、低频、纯函数、失败关闭且可用固定向量回归的业务判断纳入最小保护范围。
  • JWT 字段与客户端 verdict 不能替代资源服务器授权;长期 API Key 和服务密钥不得写入 APK。
  • 发布门禁比较四类结果、最终方法清单、服务端调用决策和设备语义,静态通过不外推兼容或攻击阻断结论。

直接答案:让解析层保持可诊断,只保护稳定且不可替代的业务裁决

数据从网络或文件进入应用后,先经过字节解码、格式解析、字段映射、规范化、输入约束、业务不变量和授权裁决。VMP 不应把这条链整体包住。格式解析和错误定位默认保持透明,适合保护的是经过规范化后的少量稳定业务判断,例如状态转换、额度组合、幂等选择和敏感动作前置条件。

分界不能按包名或类名决定。一个名为 Validator 的对象可能只检查字符串长度,一个名为 Parser 的适配器也可能偷偷选择业务状态。评审必须记录每个方法的输入、输出、副作用、失败枚举、是否依赖反射或生成代码、服务端能否最终裁决,以及规则变化频率,再按职责选择最小方法集合。

客户端保护只增加本地规则被阅读和修改的成本,不能把设备变成可信授权端。价格、权益、身份、对象归属和高风险动作必须由服务端重新验证。没有同一候选包、同一协议样本和设备回归时,文章只能给出范围方法,不能声称某个解析链已兼容或某种攻击已被阻断。

数据处理各层的默认 VMP 处置
层级主要职责默认建议发布证据
字节与语法解析把输入变成结构保持在外错误位置与类型
字段映射绑定 DTO 字段通常在外缺失与未知字段
规范化统一可比较表示精确评审前后字节与值
输入约束长度、范围、枚举通常在外边界样本矩阵
业务不变量状态与组合规则优先候选允许与拒绝分支
远端授权主体与对象裁决留在服务端服务端 verdict

解析层的目标是确定性和可诊断,不是隐藏格式细节

解析器回答的是输入能否按某个格式解释,而不是用户是否有权执行动作。UTF-8 解码失败、JSON 语法错误、字段类型错误和枚举未知值应当产生稳定、可分类的失败;若把通用解析器整体虚拟化,异常栈、源位置和库版本兼容面都会扩大,排障信息反而更难保持。

Kotlin serialization 会根据可序列化声明生成序列化器,并依赖描述符、字段名、默认值和 Json 配置。ignoreUnknownKeys、explicitNulls、coerceInputValues 等选项都会改变接受集合。门禁应冻结真实 release 配置和样本,而不是拿开发环境的默认 Json 实例推断发布行为。

解析失败必须在任何业务副作用之前结束。不得为了提高成功率把数字解析失败默认为零、把未知枚举映射为最高权限或把缺失签名字段当成空串继续。日志只记录错误枚举、字段路径和候选版本,不能写入完整令牌、个人数据或原始请求体。

格式失败与业务失败要分开
输入现象归属层推荐结果禁止回退
非法 JSON语法解析FORMAT_ERROR返回空对象
字段类型错误字段映射TYPE_ERROR强制转零
超长字符串输入约束CONSTRAINT_ERROR截断后继续
状态不允许业务不变量BUSINESS_REJECT改成默认允许
需服务端确认授权边界REMOTE_REVIEW本地自批
网络不可用传输边界明确失败或排队离线授权

字段规范化要在签名、比较和业务校验之前固定

同一个语义值可能有多种文本表示:Unicode 组合形式、大小写、前后空白、数字指数、小数位和 JSON 属性顺序都可能不同。若签名输入、缓存键和业务校验各自规范化,客户端与服务端会对不同字节做摘要,出现误拒绝,或让等价输入绕过重复检测。

规范化函数宜保持纯函数、显式版本和有限职责。先检查原始长度与编码,再做允许的 Unicode、时间、数字或标识符规范化,输出不可变对象,随后由业务校验消费。不得在规范化阶段填充授权字段、调用网络或写数据库,否则失败类别和重试边界会混在一起。

当 JSON 参与签名或摘要时,RFC 8785 的确定性属性排序与字符串、数字序列化规则可以作为互操作依据,但它不提供认证、重放控制或业务授权。项目仍要定义协议版本、允许字段、重复请求标识和服务端重算规则,并用固定测试向量比较最终字节。

规范化对象的版本化检查
对象允许转换必须保留失败条件
用户标识明确大小写策略原始审计摘要非法字符
金额字符串固定精度解析币种与原值溢出或多余精度
时间戳转 UTC instant原时区语义越界或歧义
JSON 签名字节JCS 规则协议版本不支持的数值
枚举显式映射未知值原码未声明值
幂等键格式验证客户端提供值空值或超长

输入约束、业务不变量和授权裁决要使用不同错误类型

长度、范围、正则和枚举成员属于输入约束,它们通常公开且变化可控。库存状态、订单转换、许可证组合和离线额度属于业务不变量,可能包含更敏感的决策。主体是否拥有对象、令牌是否具备 scope、价格是否有效则属于授权,最终必须由服务端可信状态裁决。

四类结果至少应区分 FORMAT_ERROR、CONSTRAINT_ERROR、BUSINESS_REJECT 和 REMOTE_REVIEW。调用方据此决定提示、修正、停止还是向服务端提交,不能把所有失败压成 false。统一布尔值会让网络故障被当成业务拒绝,也可能让异常分支错误地改用本地默认允许。

错误对象应包含稳定 code、阶段、公开字段路径和是否可重试,不携带秘密值。外层 UI 可以把多个技术错误映射成用户能理解的提示,但遥测仍保留分类。保护前后比较的重点是 code、阶段、副作用次数和远端调用决策,而不是易受混淆影响的异常消息。

优先保护业务不变量,但先把高频变化与平台胶水剥离

业务不变量适合成为候选,是因为它把多个已解析字段组合成不可替代的决策,例如只有草稿状态可编辑、离线凭据必须同时满足类型与时间窗口、同一幂等键不能对应不同请求摘要。规则应重构为小型显式函数,输入是不可变领域对象,输出是类型化 verdict。

并非所有业务规则都值得进入 VMP。频繁受运营配置影响、依赖服务端实时数据或需要热修的规则应留在可更新的策略层,并由服务端最终裁决。把高频变化规则虚拟化会增加发布频率和兼容回归,不会自动提高边界可信度。候选应同时满足价值、稳定、可测试和失败关闭四个条件。

受保护方法不应直接持有 Activity、数据库游标、网络回调或序列化框架对象。先在外层解析和规范化,再把最小字段投影传入。方法只返回 Allow、Reject 或 RemoteReview,不负责展示提示和执行动作,使 ABI、异常和线程范围保持可控。

业务规则是否适合纳入 VMP
判断维度适合不适合处理
决策价值关键状态或额度展示格式保护关键函数
变化频率低频版本化远端动态策略服务端配置
输入稳定性领域值对象框架回调对象先做投影
副作用纯判断直接写库或联网拆开执行
失败方式明确关闭异常即允许重构 verdict
可回归性固定向量齐全依赖人工操作先补测试

服务端授权必须独立存在,客户端 VMP 只做纵深防御

JWT access token 可以携带 issuer、audience、expiry、scope 和 client 标识供资源服务器验证,但客户端解析到这些字段并不等于授权已经完成。服务端还要验证签名、时间、撤销或会话状态、主体与对象关系以及动作对应的 scope,任何客户端布尔值都不能替代这一步。

长期 API Key、服务密钥和第三方高权限凭据不能编进 APK。Android 不安全 API 使用风险指南明确指出静态密钥可被逆向或拦截;正确做法是把敏感服务调用放在服务端,客户端使用短期、限权且可撤销的会话凭据。VMP 不能把硬编码秘密变成安全存储。

离线业务若必须先行,应使用明确限权、短有效期、可审计的离线授权材料,并在恢复联网后复核。失败策略不能因为网络不可达就自动允许高风险动作。文章只描述架构边界,不声称御盾已为某个项目提供服务端授权、令牌签发或离线凭据能力。

生成序列化器与反射入口保持透明,规则绑定稳定领域方法

Kotlin serialization 生成代码和描述符会随着编译器、插件、字段声明及优化变化。把生成的 serializer、descriptor 构造或 Json 编解码路径整体纳入 VMP,会把大量平台与工具链细节带进保护面。默认仅保留精确 keep 与兼容测试,让它们负责得到领域输入。

Kotlin 反射依赖运行时可发现的类、成员、注解和元数据。若框架按名称或签名寻找构造器、属性或适配器,混淆与方法变换都可能影响结果。评审要从最终 APK 枚举反射入口,记录 mapping 后身份和调用样本;不能因为源码注解存在就假设候选中仍可发现。

业务校验器应通过普通显式接口接收已规范化对象,避免由反射扫描自动注册。若必须反射调用,先用精确规则固定入口,并把反射查找、错误类型和业务 verdict 分开测试。保护清单绑定领域方法的最终 owner、name、descriptor,不使用整个 parser 包的通配。

用类型化结果演示解析、约束、业务拒绝和远端复核

下面的 Kotlin 示例接收公开的 JSON 文本,只演示四层结果,不包含令牌、客户字段或真实接口。Json.parseToJsonElement 负责语法解析,字段读取与长度范围负责约束,状态组合负责业务拒绝,需要主体和对象关系确认时返回 RemoteReview。任何格式或约束错误都不会执行后续动作。

示例故意不在客户端批准最终授权。即使本地状态满足,也只生成包含公开字段的 RemoteReview 请求摘要,由调用方交给服务端。未知字段是否接受、金额精度和状态枚举都应由协议版本规定;代码中的限制只是示例,不应复制为某个真实业务策略。

门禁至少用空输入、非法 JSON、错误类型、超长标识、负数、未知状态、禁止状态和正常待复核八类样本。保护前后比较 Result 类型、code、字段路径和是否产生 RemoteReview;只比较 UI 文案或函数返回不崩溃,无法证明分层语义没有改变。

四层校验结果的 Kotlin 示例
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.doubleOrNull
import kotlinx.serialization.json.jsonPrimitive

sealed interface CheckResult {
    data class FormatError(val code: String) : CheckResult
    data class ConstraintError(val field: String, val code: String) : CheckResult
    data class BusinessReject(val code: String) : CheckResult
    data class RemoteReview(val request: ReviewRequest) : CheckResult
}

data class ReviewRequest(
    val orderId: String,
    val amountMinor: Long,
    val expectedState: String
)

private val json = Json {
    ignoreUnknownKeys = false
    isLenient = false
}

fun classify(raw: String): CheckResult {\n    if (raw.length > 65_536) {\n        error("input exceeds 65536 characters")\n    }
    if (raw.isBlank()) {
        return CheckResult.FormatError("EMPTY_INPUT")
    }
    val root = try {
        json.parseToJsonElement(raw)
    } catch (_: Exception) {
        return CheckResult.FormatError("INVALID_JSON")
    }
    val obj = root as? JsonObject
        ?: return CheckResult.FormatError("ROOT_NOT_OBJECT")
    val orderId = obj["orderId"]?.jsonPrimitive?.content
        ?: return CheckResult.ConstraintError("orderId", "MISSING")
    if (orderId.length !in 8..64) {
        return CheckResult.ConstraintError("orderId", "LENGTH")
    }
    if (!orderId.all { it.isLetterOrDigit() || it == '-' }) {
        return CheckResult.ConstraintError("orderId", "CHARACTER_SET")
    }
    val amount = obj["amountMinor"]?.jsonPrimitive?.doubleOrNull
        ?: return CheckResult.ConstraintError("amountMinor", "TYPE")
    if (!amount.isFinite() || amount < 0.0 || amount > 10_000_000.0) {
        return CheckResult.ConstraintError("amountMinor", "RANGE")
    }
    if (amount % 1.0 != 0.0) {
        return CheckResult.ConstraintError("amountMinor", "NOT_INTEGER")
    }
    val state = obj["state"]?.jsonPrimitive?.content
        ?: return CheckResult.ConstraintError("state", "MISSING")
    val supported = setOf("DRAFT", "LOCKED", "CANCELLED")
    if (state !in supported) {
        return CheckResult.ConstraintError("state", "UNKNOWN_ENUM")
    }
    if (state == "CANCELLED") {
        return CheckResult.BusinessReject("ORDER_CANCELLED")
    }
    if (state == "LOCKED" && amount > 0.0) {
        return CheckResult.BusinessReject("LOCKED_ORDER_CANNOT_CHANGE")
    }
    return CheckResult.RemoteReview(
        ReviewRequest(
            orderId = orderId,
            amountMinor = amount.toLong(),
            expectedState = state
        )
    )
}

发布门禁绑定候选哈希、协议样本和设备回归

发布前先冻结协议版本、release 候选哈希、序列化配置、mapping、VMP 方法清单和测试向量。静态检查确认解析器与生成代码不在宽泛保护范围,业务方法命中数量符合评审,所有远端授权动作仍指向服务端。任何依赖、编译器或字段变化都让旧回执失效。

自动测试逐层断言格式、约束、业务拒绝和远端复核,设备测试再覆盖反射、混淆、线程、进程重启和真实 Android API。对失败关闭路径要验证没有写库、发起受保护动作或泄露原始输入。单一设备通过不代表全部 API、ABI 和厂商矩阵,报告必须写明覆盖边界。

可先阅读本站的第三方 SDK 回调边界说明,避免把解析责任误并入框架回调;需要评审具体 App 的保护范围时,使用御盾中央平台提交候选、协议边界和回归要求。提交前应移除密钥与客户数据,只提供可审计样本和构建标识。

解析与业务校验发布清单
门禁项通过条件阻断条件保存证据
候选身份APK 与配置哈希一致产物变化摘要清单
解析配置release Json 选项冻结默认值漂移配置快照
错误分类四类结果稳定布尔压平样本矩阵
VMP 命中仅稳定业务方法parser 通配最终方法表
服务端授权关键动作远端裁决本地最终允许verdict 回执
设备回归范围内路径通过反射或语义差异设备与日志
  • 冻结协议、候选、配置、mapping 与规则摘要。
  • 用异常输入确认解析失败先于业务副作用。
  • 比较保护前后四类结果和远端调用决策。
  • 确认长期密钥不进入 APK、日志或公开样本。
  • 按真实反射入口与设备矩阵执行回归。
  • 任何输入或候选变化后重新生成回执。
  • 内链:/zh-cn/articles/third-party-sdk-callback-vmp-boundary/
  • 行动入口:https://www.leonadev.com/console/

事实依据与适用边界

以下内容区分官方事实、本文工程判断和不能外推的范围,避免把设计建议写成未经验证的产品结论。

本文判断事实或工程依据适用限制
Kotlin 序列化边界依赖生成序列化器、描述符、字段名和格式配置。Kotlin serialization 说明编译插件与运行库的序列化器生成和格式用法,支持冻结 release 配置与样本。该资料不证明第三方序列化库使用相同生成方式,也不证明某个候选经过 VMP 后兼容。
按运行时名称、成员和元数据查找的入口需要独立验证。Kotlin reflection 说明 Kotlin 反射依赖运行时可发现的类、成员与元数据,支持候选级查找回归。反射文档不覆盖每个框架的扫描、缓存、代码生成和降级行为。
参与签名或摘要的 JSON 需要固定可重复的字节表示。RFC 8785 JSON Canonicalization Scheme 规定确定性属性排序、字符串与数字序列化,可用于跨端测试向量。JCS 不提供认证、重放防护、完整性结论或业务授权,仍需协议与服务端验证。
JWT access token 中的声明应由资源服务器验证并绑定预期受众和权限。RFC 9068 JWT access token profile 描述 issuer、audience、expiry、scope 与 client 等声明的资源服务器处理语义。JWT 格式不能保证签发策略、令牌时长、撤销与对象级授权正确。
移动客户端不适合保存长期高权限 API Key。Android insecure API usage 指出静态 API Key 可被逆向或拦截,并建议敏感服务把长期凭据保留在服务端。服务端代理仍需用户认证、限权、审计和滥用控制,不能仅隐藏客户端调用。
抗篡改与抗逆向属于纵深防御,不能替代服务端授权。OWASP MASVS-RESILIENCE 把韧性控制放在移动安全纵深防御中,支持把 VMP 与授权、发布链分开陈述。控制目录不证明御盾或任何具体候选已经达到某种防护强度。
格式、约束、业务拒绝和远端复核应使用不同结果类型。工程判断:分层 verdict 让调用方、遥测和门禁能区分可修正输入、稳定业务拒绝与必须服务端裁决的动作。具体 code、字段和重试策略由项目协议定义,示例不能直接成为生产授权规则。
优先保护低频稳定的纯业务判断,而不是整条解析调用链。工程判断:最小领域函数拥有更小 ABI、异常和工具链耦合面,也能用固定输入向量独立回归。是否适合仍需根据最终候选、方法命中、性能与设备证据逐项确认。
解析失败必须在业务副作用之前结束且不得默认允许。工程判断:格式或类型未知时继续写库、提交或授权会把不可解释输入变成业务状态,门禁应验证副作用次数为零。低风险容错字段可以由版本化协议明确默认值,但不能由客户端临时猜测。
本文没有提供任何项目兼容率、性能、攻击阻断或客户结果。项目证据尚未接入:缺少目标候选、协议样本、服务端 verdict 和设备矩阵回执。正文只给出可复核的分层、代码示例和发布检查方法。

工程常见问题

JSON 解析器是否应该整体纳入 VMP?

通常不应该。通用解析器与生成序列化器耦合格式、异常和工具链,保持透明更易诊断。先解析成不可变领域对象,再只保护稳定的业务判断。

类名叫 Validator 就一定适合保护吗?

不一定。它可能只做公开长度或范围检查,也可能混入网络和写库。应按输入、输出、副作用、变化频率和服务端可替代性评审具体方法。

规范化为什么必须先于签名和业务比较?

同一语义值可能有多种文本或 JSON 表示。若两端规范化顺序不同,会摘要不一致或绕过重复判断,因此协议要固定版本和最终字节。

客户端解析 JWT 后可以直接决定高风险动作吗?

不可以。资源服务器仍要验证签名、issuer、audience、expiry、scope、会话和对象关系,客户端结果只能用于预检与体验。

VMP 能保护硬编码在 APK 中的 API Key 吗?

不能把长期秘密变成安全存储。高权限凭据应留在服务端,客户端只使用短期、限权、可撤销且与用户会话绑定的材料。

业务校验器返回 Boolean 是否足够?

通常不足。至少区分格式错误、约束错误、业务拒绝和远端复核,否则调用方可能把网络故障或未知输入误当成允许。

怎样验证保护前后的解析语义没有变化?

冻结候选、配置和样本,比较结果类型、错误 code、字段路径、副作用次数与远端调用决策,再在真实 Android 运行时覆盖反射和混淆路径。

代码示例通过能否证明真实项目已完成验证?

不能。示例只演示分层方法;真实结论还需要目标 APK、协议版本、序列化配置、VMP 命中表、服务端回执和设备矩阵证据。

想用自己的 App 验证?

提交候选包、目标系统和关键业务路径,申请御盾 PoC 与兼容性评估。

继续阅读: VMP 加固是什么,如何选择保护范围