先看结论与判断条件
- PoC 的最小单位是具有业务入口、状态变化和可判定结果的闭环路径,不是单个页面、按钮或测试人员的一串点击记录。
- 优先级应由失败损失、被保护资产、状态不可逆性、触发频率、平台依赖和诊断难度共同决定,收入不是唯一排序标准。
- 启动、登录、授权、支付前后状态和高风险 SDK 要覆盖允许、拒绝、取消、重试、恢复与进程重建,不能只跑最顺畅的正向分支。
- 同一代码仓库的 build type、product flavor、资源、applicationId、签名配置和 SDK 组合可能不同,PoC 结论必须绑定精确候选。
- 依赖 Android 运行时、组件和系统 API 的语义应在设备端验证;云矩阵用于扩展覆盖,但不能替代真实渠道和项目状态。
- PoC 通过只能支持已执行候选、路径、状态和环境内的结论,不能外推全部功能、全部设备、线上性能或防护强度。
先把 PoC 定义成最小业务闭环,而不是页面巡检
加固 PoC 的直接目标,是确认精确候选经过保护处理后,关键业务仍能在受控条件下完成,并且失败时能够定位和回退。一个合格路径至少包含入口、前置状态、用户动作、系统依赖、状态变化、成功标准和失败标准。只有能回答这些字段,测试结果才可复核;只写“首页正常”“支付可用”或“功能无异常”,无法说明执行了什么。
页面数量不能代表风险覆盖。登录页可能展示正常,但令牌刷新在进程重建后失败;支付页可能顺利拉起,取消支付后订单却停留在处理中;授权页面可能允许合法用户进入,却没有验证失效订阅和越权账号。PoC 应围绕资产和状态机组织,而不是从菜单第一项机械点到最后一项。
工程上可以把 pathId 作为证据主键,并绑定候选 SHA-256、variantId、设备环境、测试数据类别和执行回执。基线包与加固候选必须使用同一前置状态和判定口径。若基线已经失败,应先处理基线或记录既有缺陷,不能把它算作加固回归,也不能删除失败路径后宣称整体通过。
| 字段 | 要回答的问题 | 合格证据 | 常见误判 |
|---|---|---|---|
| 入口 | 从哪里进入业务 | Activity、深链或受控入口 | 只写页面名称 |
| 前置状态 | 账号、订单和权限处于什么状态 | 可重复夹具与状态摘要 | 依赖人工临时准备 |
| 状态变化 | 执行前后什么被改变 | before 与 after 契约 | 只看界面提示 |
| 成功标准 | 什么结果算完成 | 业务断言与设备回执 | 以没有闪退代替 |
| 失败标准 | 什么现象立即阻断 | 明确错误码、状态或超时条件 | 测试后口头解释 |
| 回退 | 失败后怎样恢复环境 | 幂等清理或受控重置 | 继续复用污染状态 |
用业务损失和技术暴露面决定先后顺序
第一层排序看业务损失。会阻止用户启动应用、登录账号、获得已购权益、完成付款或恢复交易的路径,通常应进入首轮。这里的“损失”要写成具体后果,例如用户无法到达服务、合法授权被拒、重复提交、订单状态悬空或本地记录与服务端不一致,而不是笼统写成“影响体验”。
第二层排序看被保护资产与技术暴露面。包含本地授权判断、Native 库加载、动态模块、WebView 桥、第三方支付 SDK、账号 SDK、推送入口或深链解析的路径,更容易受到类加载、反射、资源、签名、组件和进程边界变化影响。OWASP MASVS-RESILIENCE 把抗篡改与抗逆向放在纵深防御语境中;因此 PoC 既要检查业务连续性,也不能把一次功能通过写成防护强度证明。
第三层排序看可诊断性与不可逆性。一次失败如果会写入订单、消耗兑换码、改变订阅、覆盖存档或使测试账号进入不可恢复状态,就必须先设计隔离数据、幂等键、查询接口和回退步骤。高频但易恢复的路径与低频却不可逆的路径不能只按访问量比较,应由产品、研发、测试和安全共同确认优先级。
| 维度 | 高优先信号 | 需要的项目证据 | 不能直接推断 |
|---|---|---|---|
| 收入 | 付款、续费、恢复购买或权益到账 | 订单状态机和测试环境 | 真实支付金额与成功率 |
| 授权 | 本地或离线决定高价值能力 | 允许与拒绝账号夹具 | 服务端授权已安全 |
| 可达性 | 启动、首屏或初始化链阻断全应用 | 冷启动回执与退出信息 | 线上启动分位数 |
| 账号 | 登录、刷新、退出或换账号改变会话 | 令牌状态契约 | 生产身份系统无风险 |
| 数据完整性 | 操作可能重复、丢失或悬空 | 前后状态与幂等键 | 所有异常均已恢复 |
| 高风险依赖 | SDK、Native、WebView 或动态模块参与 | 精确版本与调用边界 | 依赖不存在未知缺陷 |
启动与登录要覆盖进程重建和会话失效
启动路径不是看到首屏就结束。应区分冷启动、温启动、后台返回、进程被系统回收后的恢复以及从通知或深链进入。Android app startup time 区分 TTID 与 TTFD:初始画面出现和应用真正可交互、数据完整展示不是同一时刻。PoC 要保存测量条件和关键事件,但单次耗时只用于回归诊断,不能写成线上分位数或加固导致的因果结论。
登录路径至少要有未登录成功、密码或验证码拒绝、已有会话刷新、令牌失效、主动退出和账号切换。加固可能触及 Application 初始化、序列化、反射、网络组件、Keystore 引用或 Native 依赖;因此测试既要观察界面,也要断言账号身份、令牌生命周期、本地数据隔离和服务端状态没有串用。
Android instrumented tests 在 Android 设备环境中运行,可访问组件和平台 API,适合验证依赖真实运行时的启动与登录语义。设备测试仍需使用受控测试账号和非生产密钥,日志应删除令牌、手机号和用户数据。单台设备通过只证明该候选在该环境下执行了指定断言,不能代表所有 API、ABI、厂商系统或账号策略。
| 路径 | 前置状态 | 关键断言 | 阻断示例 |
|---|---|---|---|
| 冷启动 | 进程不存在、缓存状态固定 | 入口可达且数据完成展示 | 只出现启动画面或初始化退出 |
| 进程重建 | 已登录后回收进程 | 身份与导航按契约恢复 | 恢复到错误账号或空白页 |
| 登录允许 | 有效测试凭据 | 会话建立且业务身份一致 | 界面成功但令牌未保存 |
| 登录拒绝 | 无效或过期凭据 | 拒绝且不污染旧会话 | 错误账号继承旧权益 |
| 令牌刷新 | 访问令牌失效、刷新条件有效 | 刷新一次且请求可恢复 | 循环刷新或重复提交 |
| 退出换号 | 账号甲已登录 | 数据清理后进入账号乙 | 本地缓存跨账号泄漏 |
授权和支付必须验证前后状态,而非只看拉起成功
授权路径要同时验证合法允许和明确拒绝。测试夹具可覆盖有效授权、过期授权、未购买、离线宽限、设备变更和服务端撤销,但是否存在这些状态取决于项目规则。断言应落在实际能力是否开放、服务端记录是否一致和本地缓存是否按策略更新,不能只依据按钮颜色、弹窗文案或本地布尔值。
支付路径至少拆成创建订单、拉起渠道、用户取消、渠道失败、回调确认、权益到账和恢复查询。PoC 不必执行真实生产交易,可以使用官方沙箱或项目受控测试环境;但每一步都要保存 orderFixture、beforeState、afterState 和 failureCriterion。若没有可回滚环境,应先把路径标为阻塞,而不是改成只测支付页面能否打开。
状态转换比最终页面更重要。用户取消后订单应进入项目定义的可恢复状态,重复回调不得重复发放权益,进程在支付返回前被回收后仍要能够查询最终结果。涉及收入或授权的用例必须包含至少一个拒绝或中断分支,防止正向分支成功掩盖状态机错误。任何项目尚未提供的状态规则,都应保留为待确认事实。
| 场景 | before | 事件 | after 与失败判据 |
|---|---|---|---|
| 合法授权 | 授权有效且身份匹配 | 进入受保护能力 | 能力开放;误拒绝即失败 |
| 过期授权 | 授权超过项目有效期 | 尝试进入能力 | 按规则拒绝;继续开放即失败 |
| 支付取消 | 订单已创建未支付 | 用户取消渠道流程 | 状态可查询可恢复;权益到账即失败 |
| 支付成功 | 订单待确认 | 沙箱返回并完成确认 | 订单与权益一致;只改本地显示即失败 |
| 重复回调 | 订单已经确认 | 重复提交同一回执 | 结果幂等;重复发放即失败 |
| 进程中断 | 外部支付页已打开 | 应用进程被回收后返回 | 主动查询收敛;永久悬空即失败 |
高风险 SDK 要按调用边界纳入路径,不做孤立冒烟
高风险依赖不是把所有第三方 SDK 各点一次,而是识别它们位于哪条核心路径、跨越哪些边界以及失败会留下什么状态。支付、账号、地图、推送、风控、广告、WebView 桥和 Native SDK 的初始化时机、组件声明、反射入口、资源、ABI 与回调线程不同。清单应记录精确版本、调用入口、结果回调和可替代降级路径。
若 SDK 仅在特定渠道、地区、product flavor 或动态模块中存在,代表样本必须真正包含该依赖。Android build variants 文档说明 build type、product flavor、source set、applicationId 和签名配置会组合成不同变体;所以“同一个仓库已经测过”不能证明另一变体具有相同资源、证书、SDK 或运行行为。
PoC 先选择能够代表最高业务损失和最高技术差异的候选,再把其他变体列为后续接受范围。具体样本方法可参考[多渠道多 Flavor App 的 PoC 样本选择](/zh-cn/articles/choose-representative-poc-variant/)。该内链处理变体选择,本篇只决定业务路径;两者都不能替代最终设备矩阵和发布渠道回执。
| 字段 | 记录内容 | 验证动作 | 边界 |
|---|---|---|---|
| dependencyId | 依赖名称与精确版本 | 核对候选实际包含项 | 不评价未测版本 |
| variantIds | 出现该依赖的变体 | 逐项绑定候选摘要 | 同仓库不能代替 |
| entryPoint | 初始化或调用入口 | 设备端执行真实调用 | 不包含秘密参数 |
| callback | 成功、拒绝和异常结果 | 断言线程与状态转换 | 不把拉起当完成 |
| fallback | 依赖不可用时的产品策略 | 验证降级或明确阻断 | 没有策略则待确认 |
| dataClass | 路径处理的数据类别 | 最小化测试数据和日志 | 不保存真实用户数据 |
用路径清单生成正向、拒绝与恢复用例
手工表格最容易遗漏失败判据,下面的 Python 脚本读取 JSON 路径清单,校验 pathId 唯一、影响类别有效、前置条件与回退不为空,并把每个 scenario 展开为独立用例。输入中的 before、action、expected 和 failureCriterion 都必须存在;收入或授权路径还必须同时包含 allow 与 deny 场景,避免只有正向证明。
脚本只生成公开安全的用例骨架,不调用设备、不访问账号、不执行支付,也不包含攻击或绕过逻辑。生成结果可交给 instrumentation、云真机或现有测试框架执行。任何输入不完整都会以非零状态退出,使缺少状态契约的问题在设备资源被占用前暴露。
路径清单应由业务和研发共同填写,测试负责把可判定字段变成断言,安全人员检查加固影响面。候选摘要、签名、真实账号夹具和执行回执应在受控系统另行管理,不能塞进公开示例。脚本输出不是通过证据;只有同一候选的实际执行记录才能支持 PoC 结论。
- 每个 pathId 在清单内唯一
- 业务影响类别属于受控枚举
- 入口、回退和代表变体不为空
- 每个场景都有 before、action、expected 和 failureCriterion
- 收入与授权路径同时包含 allow 和 deny
- 输出只生成用例,不冒充设备执行回执
- 候选、账号和签名材料留在受控系统
#!/usr/bin/env python3
import json
import sys
from pathlib import Path
IMPACTS = {"revenue", "authorization", "availability", "account", "data-integrity", "high-risk-dependency"}
REQUIRED_SCENARIO_FIELDS = ("name", "kind", "before", "action", "expected", "failureCriterion")
def require_text(value, label):
if not isinstance(value, str) or not value.strip():
raise ValueError(f"missing non-empty {label}")
return value.strip()
def load_inventory(path):
data = json.loads(Path(path).read_text(encoding="utf-8"))
if not isinstance(data, list) or not data:
raise ValueError("inventory must be a non-empty array")
return data
def build_cases(paths):
seen = set()
cases = []
for path in paths:
path_id = require_text(path.get("pathId"), "pathId")
if path_id in seen:
raise ValueError(f"duplicate pathId: {path_id}")
seen.add(path_id)
impact = require_text(path.get("businessImpact"), f"{path_id}.businessImpact")
if impact not in IMPACTS:
raise ValueError(f"unsupported impact for {path_id}")
require_text(path.get("entry"), f"{path_id}.entry")
require_text(path.get("rollback"), f"{path_id}.rollback")
variants = path.get("variantIds")
if not isinstance(variants, list) or not all(isinstance(v, str) and v.strip() for v in variants):
raise ValueError(f"invalid variantIds for {path_id}")
scenarios = path.get("scenarios")
if not isinstance(scenarios, list) or len(scenarios) < 2:
raise ValueError(f"at least two scenarios required for {path_id}")
kinds = set()
for index, scenario in enumerate(scenarios, start=1):
normalized = {field: require_text(scenario.get(field), f"{path_id}.{field}") for field in REQUIRED_SCENARIO_FIELDS}
kinds.add(normalized["kind"])
cases.append({"caseId": f"{path_id}-{index:02d}", "pathId": path_id, "impact": impact, "variantIds": variants, **normalized})
if impact in {"revenue", "authorization"} and not {"allow", "deny"}.issubset(kinds):
raise ValueError(f"allow and deny scenarios required for {path_id}")
return cases
if len(sys.argv) != 2:
raise SystemExit(2)
try:
result = build_cases(load_inventory(sys.argv[1]))
except (OSError, json.JSONDecodeError, ValueError) as error:
print(str(error), file=sys.stderr)
raise SystemExit(3)
print(json.dumps({"caseCount": len(result), "cases": result}, ensure_ascii=False, indent=2))把路径放进设备矩阵,并保持候选与环境可追溯
业务路径选好后才进入环境覆盖。Firebase Test Lab Android matrices 将测试组合定义为设备型号、操作系统版本、方向和 locale 等维度,矩阵中一个执行失败会影响整体判断。设备选择应来自应用最低支持范围、真实设备分布和高风险差异,而不是挑一台方便的最新设备,也不能用大量重复设备制造覆盖感。
每次执行至少绑定 candidateDigest、variantId、pathId、caseId、设备型号、系统版本、ABI、locale、安装来源和测试数据版本。Android vitals 可以提供崩溃、ANR、启动和设备分布等线上质量信号,可用于发现值得加入 PoC 的高风险环境;其样本受到安装来源、用户同意和统计口径限制,不能代替候选级设备断言。
矩阵结果要按失败保守收敛。某个设备的登录恢复失败,不能被其他设备通过“平均掉”;应先判定是候选、环境、测试数据还是既有基线问题,再决定修复、缩小结论或记录项目限制。云设备也未必复现全部商店签名、厂商账号、推送网络和支付渠道,因此关键发布路径仍需要相应渠道的真实受控回执。
| 对象 | 固定标识 | 为什么需要 | 缺失后的处理 |
|---|---|---|---|
| 候选 | 文件 SHA-256 与签名阶段 | 防止替换文件后沿用结论 | 阻断登记 |
| 变体 | variantId 与依赖摘要 | 限定 flavor、资源和 SDK | 缩小结论 |
| 用例 | pathId 与 caseId | 对应业务状态和判据 | 结果不可复核 |
| 设备 | 型号、系统、ABI、locale | 解释运行环境差异 | 不外推矩阵 |
| 数据 | fixtureVersion 与数据类别 | 复现允许、拒绝和中断 | 重新准备状态 |
| 回执 | 时间、结果和原始证据摘要 | 连接执行与结论 | 保持未验证 |
用分层准入结论结束 PoC,不把局部通过扩大成产品承诺
PoC 结束时应逐路径给出 passed、failed、blocked 或 not-run,而不是只给一个总百分比。passed 表示精确候选在记录环境和状态下满足既定判据;failed 表示出现可复现偏差;blocked 表示缺少账号、沙箱、设备、签名或业务规则;not-run 表示不在本轮范围。阻塞项不能改名为通过,也不能从报告中删除。
结论还要分层:静态身份检查、设备安装与启动、核心路径状态契约、代表矩阵、渠道回执分别支持不同范围。OWASP 控制目录、平台文档和脚本校验提供方法依据,不证明某个项目已获得抗逆向强度、性能改善或攻击阻断。没有线上样本时,也不能根据 PoC 单次启动记录宣称生产分位数改善。
准备软件加固 PoC 时,可整理精确候选、代表变体、核心路径清单、允许与拒绝夹具、测试渠道、设备范围、失败判据和回退责任,再通过御盾中央平台提交申请。申请材料越接近可执行状态,双方越容易尽早发现缺失的沙箱、签名或业务规则;最终能力和适用方案仍以实际评估与项目回执为准。
| 层级 | 所需证据 | 可以说明 | 不能说明 |
|---|---|---|---|
| 候选身份 | 摘要、variant 与签名阶段 | 测试对象被锁定 | 功能或保护已通过 |
| 安装启动 | 设备安装、冷启动和退出记录 | 指定环境可进入应用 | 核心业务完整 |
| 路径契约 | 允许、拒绝、中断与恢复断言 | 指定路径状态正确 | 未执行功能正确 |
| 代表矩阵 | 选定设备组合逐项回执 | 结论覆盖记录矩阵 | 全部用户设备正确 |
| 渠道验证 | 对应商店或分发链回执 | 该渠道样本可交付 | 其他渠道等价 |
| 生产观察 | 合规线上指标与版本身份 | 发现真实质量信号 | 单次 PoC 的因果外推 |
事实依据与适用边界
以下内容区分官方事实、本文工程判断和不能外推的范围,避免把设计建议写成未经验证的产品结论。
| 本文判断 | 事实或工程依据 | 适用限制 |
|---|---|---|
| 依赖真实 Android 运行时、组件和系统 API 的语义可以通过设备端 instrumented test 验证。 | Android instrumented tests 说明 instrumented tests 在 Android 设备环境中运行并可访问平台 API。 | 单台设备通过不能代表完整 API、ABI、系统版本、厂商与渠道矩阵。 |
| Android 云测试矩阵可以按设备型号、OS、方向和 locale 等维度组织执行。 | Firebase Test Lab Android matrices 的入门资料说明矩阵配置和各执行结果。 | 云矩阵仍需依据真实用户与最低支持范围选择,也不能自动复现全部账号和分发环境。 |
| 移动端抗篡改与抗逆向控制属于纵深防御的一部分。 | OWASP MASVS-RESILIENCE 列出与平台完整性、抗篡改和抗动态分析相关的韧性控制。 | 控制目录不证明某个候选已经达到任何保护强度,也不替代服务端授权和发布链。 |
| Android vitals 可提供崩溃、ANR、启动与设备分布等线上质量信号。 | Android vitals 文档说明其核心质量指标及可用于观察的应用质量信息。 | 指标受 Play 安装来源、用户同意和统计口径限制,不能代替候选级 PoC 回执。 |
| Android 启动分析应区分 TTID 与 TTFD,并记录启动状态和测量条件。 | Android app startup time 文档定义 TTID、TTFD 及启动性能分析语境。 | 单次耗时不能代表线上分位数,也不能单独证明加固与性能变化存在因果关系。 |
| build type、product flavor、source set、applicationId 与签名配置会共同形成不同构建变体。 | Android build variants 文档描述构建类型、产品风味、源集和变体配置。 | 同一仓库不能证明不同 variant 具有相同 SDK、资源、证书和运行行为。 |
| PoC 核心路径应按具体业务损失、状态不可逆性和技术暴露面排序,而不是按页面数量排序。 | 工程判断:页面巡检不能覆盖令牌刷新、订单状态、授权拒绝、进程重建和依赖回调等跨页面状态。 | 实际优先级必须由项目资产、业务规则、真实流量与失败成本确认。 |
| 收入与授权路径至少需要允许和拒绝分支,并保存前后状态与失败判据。 | 工程判断:仅执行成功分支无法发现越权开放、失效授权沿用、重复发放和取消后状态悬空。 | 具体状态名、沙箱能力、回退方案和接受条件取决于项目实现。 |
工程常见问题
加固 PoC 是不是把所有页面点一遍就够了?
不够。应选择具有业务入口、前置状态、状态变化和明确结果的核心闭环,重点覆盖启动、登录、授权、支付与高风险依赖的正向和失败分支。
核心路径只能按收入高低排序吗?
不能。还要考虑全局可达性、授权资产、数据不可逆性、技术依赖、触发频率和失败后的诊断成本,低频但不可恢复的路径也可能优先。
支付 PoC 只要能拉起渠道页面是否算通过?
不算。还要验证取消、失败、确认、重复回调、权益到账、进程中断和恢复查询,并使本地与服务端状态按项目契约收敛。
一个 flavor 通过能否代表同仓库的其他渠道包?
不能直接代表。不同变体可能组合不同 applicationId、资源、签名、SDK 和源集,结论必须绑定精确候选,其他变体需按差异另行接受。
云真机矩阵通过能否证明全部用户设备没有问题?
不能。矩阵只覆盖选定型号、系统、方向和 locale,且未必复现真实商店签名、厂商账号、网络和历史数据,结论应限制在已执行范围。
申请软件加固 PoC 前应准备哪些材料?
准备候选文件及摘要、代表变体、核心路径清单、允许与拒绝测试状态、沙箱渠道、设备范围、成功与失败判据、回退责任,再通过御盾中央平台提交申请。