HarmonyOS 安全跳转治理实战:App Linking、Want、白名单与参数校验
HarmonyOS 安全跳转治理实战:App Linking、Want、白名单与参数校验
外部跳转是增长入口,也是安全入口。活动页、短信链接、浏览器打开、其他应用拉起,都可能通过链接进入你的应用。如果直接把外部参数塞进页面路由,就会出现错误页面、越权打开、脏参数导致崩溃、伪造链接绕过登录等问题。安全跳转治理的目标是把所有外部入口收敛到一个网关:先确认来源,再解析参数,最后分发到受控页面。

这篇文章解决四个问题:
- App Linking 和 Want 进入应用后如何统一接管。
- 域名、路径、参数、目标页四层如何逐级放行。
- 外部链接失败时如何回到可解释页面。
- 上线前如何覆盖伪造链接、脏参数和登录态缺失。
1. 外部入口必须收敛
不要在每个页面分别处理外部链接。所有来自浏览器、短信、其他应用、广告落地页的跳转,都应该先进入 LinkReceiver,再交给策略层处理。

| 阶段 | 处理内容 | 拦截原因 |
|---|---|---|
| 接收 | 读取 Want、URI、来源标记 | 没有 URI 或来源不明 |
| 域名 | 校验可信域名和 Scheme | 防止伪造站点 |
| 参数 | 转换类型、限制长度、过滤空值 | 防止页面异常 |
| 分发 | 只允许白名单页面 | 防止打开内部页面 |
外部入口的原则是“默认拒绝,明确放行”。只要没有命中配置,就进入兜底页,而不是猜测用户想打开什么。
2. App Linking 资料边界和工程目录
HarmonyOS 提供 App Linking 能力,可通过经验证的链接拉起应用;AbilityKit 的 Want 负责承载启动目标和参数。工程上要把官方能力接入和业务路由拆开。
| 资料入口 | 工程落点 |
|---|---|
| App Linking 概述 | 配置可验证域名和应用拉起关系 |
| 配置 App Linking | 域名校验、路径规则和发布配置 |
| Want 概述 | 接收外部拉起参数 |
| 启动 Ability | 目标页面分发和异常处理 |
目录建议:
entry/src/main/ets/
common/linking/LinkReceiver.ets
common/linking/DomainPolicy.ets
common/linking/ParamParser.ets
common/linking/RouteDispatcher.ets
common/linking/LinkFallback.ets
pages/common/LinkErrorPage.ets
这套目录不是为了复杂化,而是为了让所有外部跳转有统一入口和统一日志。
3. LinkReceiver 只做原始输入归一化
接收层不要做业务判断。它只负责把 Want、URI、来源 App、时间戳归一化成一条外部请求。
export interface ExternalLinkInput {
rawUri: string
sourceBundle?: string
receivedAt: number
}
export class LinkReceiver {
fromWant(parameters: Record<string, Object | undefined>): ExternalLinkInput | undefined {
const uri = parameters['uri']
if (typeof uri !== 'string' || uri.length === 0) {
return undefined
}
const source = parameters['sourceBundle']
return {
rawUri: uri,
sourceBundle: typeof source === 'string' ? source : undefined,
receivedAt: Date.now()
}
}
}
这段代码不打开页面,也不信任参数。它只是把外部输入放进治理链路,后面每一步都可以记录和回放。
4. DomainPolicy 校验可信域名
域名策略必须明确。不要用 contains('example.com') 这种写法,因为 evil-example.com 也可能命中。应该解析 URL 后做精确域名和协议判断。
export interface LinkPolicyResult {
allowed: boolean
reason?: string
url?: URL
}
export class DomainPolicy {
private readonly domains = ['app.example.com', 'www.example.com']
verify(rawUri: string): LinkPolicyResult {
try {
const url = new URL(rawUri)
if (url.protocol !== 'https:') {
return { allowed: false, reason: '只允许 HTTPS 链接' }
}
if (!this.domains.includes(url.hostname)) {
return { allowed: false, reason: '域名不在可信范围内' }
}
return { allowed: true, url }
} catch (_) {
return { allowed: false, reason: '链接格式不正确' }
}
}
}
域名校验是第一道门。只要域名不可信,后面的路径和参数都不应该继续处理。
5. ParamParser 做类型和范围转换
外部链接里的参数全部是字符串。页面需要数字、枚举、布尔值时,必须经过转换和范围限制。
export type LinkTarget = 'article' | 'order' | 'member'
export interface ParsedLink {
target: LinkTarget
id: string
from: string
}
export class ParamParser {
parse(url: URL): ParsedLink | undefined {
const target = url.searchParams.get('target') as LinkTarget | null
const id = url.searchParams.get('id')
const from = url.searchParams.get('from') ?? 'external'
if (!target || !['article', 'order', 'member'].includes(target)) {
return undefined
}
if (!id || !/^[a-zA-Z0-9_-]{1,64}$/.test(id)) {
return undefined
}
return { target, id, from: from.slice(0, 32) }
}
}
这里把 ID 长度、字符集和目标页都限制住。这样页面拿到的是干净参数,而不是原始 URL。
6. RouteDispatcher 只允许白名单页面
分发层要把业务目标转换成内部路由。外部链接不能直接传内部页面路径,否则很容易打开调试页、管理页或未登录页面。

export interface SafeRoute {
page: string
params: Record<string, string>
loginRequired: boolean
}
export class RouteDispatcher {
dispatch(link: ParsedLink): SafeRoute {
const map: Record<LinkTarget, SafeRoute> = {
article: { page: 'pages/content/ArticlePage', params: { id: link.id }, loginRequired: false },
order: { page: 'pages/order/OrderDetailPage', params: { orderNo: link.id }, loginRequired: true },
member: { page: 'pages/member/MemberCenterPage', params: { source: link.from }, loginRequired: true }
}
return map[link.target]
}
}
路由表要白名单化。以后新增外部入口时,只能在这里显式增加,而不是让 URL 自己决定页面路径。
7. 登录态缺失要有中转页
如果链接目标需要登录,不要直接丢弃。可以进入登录中转页,登录完成后再恢复安全路由。
export interface LoginRedirectPlan {
needLogin: boolean
route: SafeRoute
reason?: string
}
export function buildRedirectPlan(route: SafeRoute, isLogin: boolean): LoginRedirectPlan {
if (route.loginRequired && !isLogin) {
return {
needLogin: true,
route,
reason: '该页面需要登录后访问'
}
}
return { needLogin: false, route }
}
这样用户从订单短信进入应用时,不会因为未登录直接看到错误页。登录成功后仍能回到原订单详情。
8. 失败兜底要保存排查信息
外部链接失败时,页面应该告诉用户原因,同时把原始 URI、来源、失败阶段记录下来,方便运营排查投放链接是否配置错误。
export interface LinkFailure {
rawUri: string
stage: 'receive' | 'domain' | 'param' | 'route'
reason: string
occurredAt: number
}
export class LinkFallback {
buildFailure(rawUri: string, stage: LinkFailure['stage'], reason: string): LinkFailure {
return { rawUri: rawUri.slice(0, 300), stage, reason, occurredAt: Date.now() }
}
}
失败记录不要包含敏感字段。截断原始链接、过滤 Token 类参数,是外部跳转日志的基本要求。
9. 外部拉起验收动作
| 场景 | 操作 | 预期结果 |
|---|---|---|
| 可信链接 | 打开 https://app.example.com/open?target=article&id=1001 |
进入文章页 |
| 非可信域名 | 打开伪造域名链接 | 进入兜底页并提示域名不可信 |
| 脏参数 | id 超长或包含特殊字符 |
链接被拦截,不进入业务页 |
| 需要登录 | 未登录打开订单链接 | 进入登录中转,登录后恢复订单页 |
| 未知目标 | target=admin |
不打开内部页面 |
可以用一个轻量函数保证外部路由不越界:
export function assertSafeRoute(route: SafeRoute): void {
if (!route.page.startsWith('pages/')) {
throw new Error('外部跳转只能分发到应用页面目录')
}
if (route.page.includes('Debug') || route.page.includes('Admin')) {
throw new Error('外部跳转禁止打开内部管理页面')
}
}
这个断言适合放在路由配置用例里,防止后续新增入口时误放行内部页面。
10. 跳转异常排查表
| 现象 | 优先查看 | 处理建议 |
|---|---|---|
| 浏览器不能拉起应用 | App Linking 配置、域名验证、安装包版本 | 先确认域名和包名配置一致 |
| 进入错误页面 | target 映射表 |
不允许 URL 直接传内部路径 |
| 页面参数异常 | ParamParser 转换结果 |
所有页面只接收解析后的参数 |
| 投放链接大量失败 | 失败阶段日志 | 按阶段区分域名、参数、登录态问题 |
| 未登录用户流失 | 登录中转页 | 登录后恢复原安全路由 |
安全跳转复现场景:给读者一组可执行核验
App Linking 和 Want 跳转都要验证白名单、参数和兜底页。能跳转不代表安全,非法链接必须被拒绝。
| 核验维度 | 读者需要准备的证据 |
|---|---|
| 输入 | 页面入口、用户动作、关键参数 |
| 过程 | 日志、状态变化、异常分支 |
| 输出 | UI 表现、回调结果、持久化结果 |
| 回归 | 同场景重复执行后的结果 |
interface SafeLinkReplayCase {
uri: any
matchedRule: any
fallbackPath: any
allowed: any
}
const replay77: SafeLinkReplayCase = {
uri: 'sample',
matchedRule: 'sample',
fallbackPath: 'sample',
allowed: 'sample',
}
function assertReplay77(item: SafeLinkReplayCase): void {
if (!item.allowed && item.fallbackPath.length === 0) throw new Error('非法链接缺少兜底页')
}
这组核验把外部链接的白名单和兜底页一起验证,避免非法参数直接进入业务页面。
安全跳转攻击回放表:把文章方法变成可复现动作
安全跳转文章需要给读者一个攻击视角。建议构造未知 scheme、合法域名缺参数、伪造业务 id、重复跳转四种链接,观察是否进入兜底页。
| 回放动作 | 核验方式 |
|---|---|
| 未知 scheme 拒绝 | 准备输入、执行操作、记录结果、给出结论 |
| 缺参数兜底 | 准备输入、执行操作、记录结果、给出结论 |
| 伪造 id 不进入详情 | 准备输入、执行操作、记录结果、给出结论 |
| 重复跳转被限流 | 准备输入、执行操作、记录结果、给出结论 |
安全跳转的回放要故意输入异常链接。读者可以准备未知 scheme、合法域名但缺参数、伪造业务 id、重复打开四类链接,分别确认白名单、参数校验、兜底页和限流是否生效。只有非法输入被稳定拒绝,App Linking 和 Want 跳转才适合放到外部入口使用。
11. 小结:外部链接要先进安全网关
安全跳转治理的关键不是写更多路由,而是减少入口。所有外部链接先经过接收、域名、参数、路由四层,再决定是否进入页面。这样活动投放、短信唤起、App Linking、其他应用拉起都能走同一套规则,既保护内部页面,也让异常链接有据可查。
更多推荐



所有评论(0)