先看结论与判断条件
- 按语法解析、字段映射、规范化、输入约束、业务不变量和远端授权六层拆分,不按 Parser 或 Validator 类名划线。
- 通用解析器、生成序列化器、反射入口和错误定位默认保持在 VMP 外,确保格式失败可观察、可分类并先于副作用。
- 规范化必须在签名、缓存键和业务比较前完成,使用显式版本与确定性字节规则,不能顺便填充授权字段。
- 只把稳定、低频、纯函数、失败关闭且可用固定向量回归的业务判断纳入最小保护范围。
- JWT 字段与客户端 verdict 不能替代资源服务器授权;长期 API Key 和服务密钥不得写入 APK。
- 发布门禁比较四类结果、最终方法清单、服务端调用决策和设备语义,静态通过不外推兼容或攻击阻断结论。
直接答案:让解析层保持可诊断,只保护稳定且不可替代的业务裁决
数据从网络或文件进入应用后,先经过字节解码、格式解析、字段映射、规范化、输入约束、业务不变量和授权裁决。VMP 不应把这条链整体包住。格式解析和错误定位默认保持透明,适合保护的是经过规范化后的少量稳定业务判断,例如状态转换、额度组合、幂等选择和敏感动作前置条件。
分界不能按包名或类名决定。一个名为 Validator 的对象可能只检查字符串长度,一个名为 Parser 的适配器也可能偷偷选择业务状态。评审必须记录每个方法的输入、输出、副作用、失败枚举、是否依赖反射或生成代码、服务端能否最终裁决,以及规则变化频率,再按职责选择最小方法集合。
客户端保护只增加本地规则被阅读和修改的成本,不能把设备变成可信授权端。价格、权益、身份、对象归属和高风险动作必须由服务端重新验证。没有同一候选包、同一协议样本和设备回归时,文章只能给出范围方法,不能声称某个解析链已兼容或某种攻击已被阻断。
| 层级 | 主要职责 | 默认建议 | 发布证据 |
|---|---|---|---|
| 字节与语法解析 | 把输入变成结构 | 保持在外 | 错误位置与类型 |
| 字段映射 | 绑定 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、异常和线程范围保持可控。
| 判断维度 | 适合 | 不适合 | 处理 |
|---|---|---|---|
| 决策价值 | 关键状态或额度 | 展示格式 | 保护关键函数 |
| 变化频率 | 低频版本化 | 远端动态策略 | 服务端配置 |
| 输入稳定性 | 领域值对象 | 框架回调对象 | 先做投影 |
| 副作用 | 纯判断 | 直接写库或联网 | 拆开执行 |
| 失败方式 | 明确关闭 | 异常即允许 | 重构 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 文案或函数返回不崩溃,无法证明分层语义没有改变。
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 命中表、服务端回执和设备矩阵证据。