HarmonyOS 7 ArkWeb:文档H5跳转URL边界归一与重定向拦截【鸿蒙心迹】
一个应用把使用说明放进 ArkWeb,通常是为了保持内容可更新。但当文档里的链接越来越多,Web 的用途很容易悄悄从“阅读本应用文档”变成“任意网址浏览器”。尤其是站内重定向、用户名式 URL、相似域名和自定义 scheme 混在一起时,仅根据字符串中是否出现公司域名判断跳转,既不可靠,也不便于排查。
这篇用一个刻意缩小边界的 Demo DocRouteFence 研究导航策略:只阅读 https://docs.example.org 上固定结构的演示文章和帮助页,其他跳转由应用自行拒绝。本文里的域名属于示例域名,不是已部署并且经过域名所有权验证的服务;所有八条测试均为本地输入向量。我们核对的是 URL 策略和 ArkWeb 回调的接线方法,而不是宣称浏览器、网络或真机已经跑通。

一、真正的风险不在网页能否打开,而在导航资格如何改变
文档阅读器经常在同一个页面里包含目录链接、帮助链接、第三方下载按钮和内容站的跳转脚本。应用初次装载的 src 是可信地址,并不意味着之后每一次跳转都处于同一个可信边界。比如 https://docs.example.org.evil.test/article/108 里包含完整的站内域名字符串,可它的实际主机根本不是 docs.example.org。如果开发者采用 raw.includes('docs.example.org'),就等于把对方域名当成自己的。
另一个常见误区是“只检查 scheme 就够了”。https 代表传输协议,并不声明这个页面有权获得应用信任。https://user@docs.example.org/article/108 在 URL 语法里还引入了用户信息;https://docs.example.org:8443/article/108 使用了非预期端口;带查询参数、fragment 的链接也可能扩大应用路由约定。我们不把这些全部描述成浏览器漏洞,它们是业务策略没有明确写出来的边界。
Demo 的任务编号固定为 WEB-1010-17,页面分别叫 DocShellPage 与 LinkAuditPage,主要策略对象为 NavPolicy。主页展示的当前地址是 https://docs.example.org/article/108,对应文章“多设备文档排版”。规则修订记作 policyEpoch=4。总共八个固定样例,预期三条允许、五条拒绝,其中一条模拟了重定向到伪装域名的拒绝。因此 redirectBlocked=1 是五条拒绝中的子集,不应被重复加进拒绝总量。
产品层面更值得先定下“不做什么”。这个文档壳不承担通用浏览器能力;不需要任意 HTTP 地址、不允许 javascript:,也不承诺把被拦截的外链自动交给系统浏览器。所有跳转在经过策略检查前,都不得被当成站内文档。若未来确实需要外部帮助站点,应先做产品评审并建立另一套外跳规则,而不是把域名通配符加宽。
二、策略表先于页面实现,免得靠截图反推业务
此次固定的允许范围很窄:协议必须为 https:,hostname 必须恰好等于 docs.example.org,端口必须为空,URL 不含用户名、密码、查询或片段,路径只接收 /article/ 加三位数字,或完全等于 /help。/article/108、/article/109、/help 三个样本预期允许,其余五个被拒绝。这种策略不会自动适用于开放的新闻站点,但对单应用说明文档足够直白。
样例中的五个拒绝项也有意采用不同理由。http://docs.example.org/article/108 是协议错误;https://docs.example.org.evil.test/article/108 是主机越界;https://user@docs.example.org/article/108 是凭据结构不允许;https://docs.example.org:8443/article/108 是端口越界;javascript:alert(1) 是非 HTTPS scheme。我们把第二项标记为一次模拟重定向目标,体现“即使第一跳已经受信,重定向目标也要重新判断”。
这里应区分 URL 解析结果和原始字符串。比较 host 时使用规范化解析字段,而不是把字符串切成斜线数组;比较路径时需要完整匹配,不能使用 startsWith('/article/') 就收下所有后缀。对嵌入式 Web 而言,输出两种东西也要分开:一是业务判定 ALLOW/BLOCK_*,二是 ArkWeb 的布尔拦截结果。业务允许对应回调返回 false,业务拒绝对应 true。把这两个布尔含义颠倒,日志会看起来正确,导航却完全相反。
还要考虑缺失或不合法的 URL。没有足够信息时按拒绝处理;不要在 catch 分支默认放行。一个合理的策略模块应该有纯函数入口,能够脱离 UI 运行固定输入向量。Web 控件的回调负责调用它,而不负责临时推断域名含义。这样的设计也为未来策略变更留下回归测试位置。
三、URL 规范化必须借助解析器,不能写成包含关系
在 ArkTS 侧可以使用 @kit.ArkTS 的 url.URL.parseURL。官方 URL 模块会提供 protocol、hostname、port、username、password、pathname 等字段。代码前要解决的问题是:输入可能合法但不符合业务策略,也可能压根无法解析,两者都不应该被自动当成站内导航。
// entry/src/main/ets/model/NavPolicy.ets
import { url } from '@kit.ArkTS';
export type NavDecision = 'ALLOW' | 'BLOCK_PARSE' | 'BLOCK_SCHEME' |
'BLOCK_HOST' | 'BLOCK_CREDENTIALS' | 'BLOCK_PORT' | 'BLOCK_ROUTE';
export class NavPolicy {
classify(raw: string): NavDecision {
try {
const target = url.URL.parseURL(raw);
if (target.protocol !== 'https:') return 'BLOCK_SCHEME';
if (target.hostname !== 'docs.example.org') return 'BLOCK_HOST';
if (target.username || target.password) return 'BLOCK_CREDENTIALS';
if (target.port) return 'BLOCK_PORT';
if (target.search || target.hash) return 'BLOCK_ROUTE';
const path: string = target.pathname;
const article = /^\/article\/[0-9]{3}$/.test(path);
return article || path === '/help' ? 'ALLOW' : 'BLOCK_ROUTE';
} catch (_) {
return 'BLOCK_PARSE';
}
}
shouldBlock(raw: string): boolean {
return this.classify(raw) !== 'ALLOW';
}
}
这段代码特别没有处理“被允许的所有资源请求”。它判断的是顶层 URL 业务导航,不是 CSS、字体、图片以及脚本资源的通用许可表。把导航白名单直接放到资源请求回调里,会把合法站内网页依赖的 CDN 静态资源一起挡掉;反过来,把静态资源域名加入导航白名单,又会放大可打开页面的范围。二者需要不同的数据模型。
当前策略对带参数的文章链接也采取拒绝,这并非所有产品必需。若以后允许 ?lang=zh,应该把允许的键集合、值类型、重复键处理和规范化顺序设计成另一个严格步骤,更新策略版本和测试。先给宽松规则,再在代码里到处做特殊判断,是文档壳最难维护的路径。
正则表达式匹配只解决路径形态,没有解决网络安全的全部问题。它不检查 DNS 解析和证书信任,也无法证明服务器的最终内容是否可信;TLS 及重定向仍由系统网络栈参与处理。我们把 NavPolicy 称为应用层“导航资格门禁”,不把它包装成万能的 URL 安全系统。
四、两个拦截回调承担不同的入口,不把它们视为同义词
华为 ArkWeb 文档明确区分 onLoadIntercept 和 onOverrideUrlLoading:前者涉及程序主动 loadUrl 等加载流程,后者在其它即将导航的阶段被调用,并非所有加载路径都能触发两者。两处都接入同一策略是为了降低漏检风险,而不是认为两个回调一定会依次触发。初始 src 由可信常量指定;如果它变成服务端配置值,还要在构建 Web 组件前单独验一遍。
下面代码的职责只有“把 URL 交给策略,再把拒绝转换为 ArkWeb 布尔返回值”。它没有实现网络访问和测试数据来源,也不会在回调里随意调用 loadUrl 形成递归重定向。要注意 onLoadIntercept 的 event.data 是 WebResourceRequest 相关事件数据, onOverrideUrlLoading 的参数则是 WebResourceRequest。代码日志以 DocRouteFence 为固定标识。
// entry/src/main/ets/pages/DocShellPage.ets — 组件中的关键部分
import { webview } from '@kit.ArkWeb';
import { NavPolicy } from '../model/NavPolicy';
@Entry
@Component
struct DocShellPage {
private controller: webview.WebviewController = new webview.WebviewController();
private policy: NavPolicy = new NavPolicy();
build() {
Column() {
Web({
src: 'https://docs.example.org/article/108',
controller: this.controller
})
.onLoadIntercept((event) => {
const target = event.data.getRequestUrl();
const blocked = this.policy.shouldBlock(target);
console.info(`DocRouteFence load ${blocked ? 'BLOCK' : 'ALLOW'}`);
return blocked; // true:中止本次导航
})
.onOverrideUrlLoading((request) => {
const blocked = this.policy.shouldBlock(request.getRequestUrl());
console.info(`DocRouteFence override ${blocked ? 'BLOCK' : 'ALLOW'}`);
return blocked;
})
}.width('100%').height('100%');
}
}
这段接口接线需要用目标 API 版本的 DevEco Studio 编译核对事件签名。文中的示意图不是 IDE 运行证据,不能因图片里的语法高亮和日志就宣称回调已经触发。官方还指出 onLoadIntercept 返回 true 表示取消这次导航、false 表示继续;onOverrideUrlLoading 的触发时机不同。两个事件的覆盖边界不应由一张成功截图来推断。

图二刻意保留左工程目录、中央代码、右模拟器和底部日志四个区域,帮助对应文件职责:DocShellPage.ets 是页面装配,NavPolicy.ets 是纯规则,LinkAuditPage.ets 是诊断呈现。右侧显示 WEB-1010-17、policyEpoch=4、允许3、拦截5、重定向拦截1、NAV_GUARDED。这些都是预设模型数据,不是一次已成功加载的网页。
五、用固定样本把边界撞出来,比只写一句“加白名单”可靠
八条输入不是随机搜索来的案例,而是作为可重放的协议用例保存。这个做法的价值在于:将来某位同事把路径放宽到 /article/*,或者为了一个帮助链接加入子域名通配符,测试立刻指出原有假设发生变化。我们既关心错误的禁止,也关心错误的允许;只统计拦截数量会漏掉合法帮助页被误杀。
对这类纯数据规则,Node.js 能在开发机器上运行一组等价黄金向量。以下代码属于测试工具示例,它不调用 ArkWeb,也不能代替ArkTS目标环境的 URL 解析一致性测试。真实工程可以把八条 JSON 向量放到 test/fixtures/nav-cases.json,再由 ArkTS 侧用同一组输入确认一致性。
// test/nav-cases.mjs:本地黄金输入,非真机网络测试
const host = 'docs.example.org';
function allowed(raw) {
try {
const u = new URL(raw);
return u.protocol === 'https:' && u.hostname === host &&
u.port === '' && !u.username && !u.password &&
!u.search && !u.hash &&
(/^\/article\/[0-9]{3}$/.test(u.pathname) || u.pathname === '/help');
} catch { return false; }
}
const vectors = [
['https://docs.example.org/article/108', true],
['https://docs.example.org/article/109', true],
['https://docs.example.org/help', true],
['http://docs.example.org/article/108', false],
['https://docs.example.org.evil.test/article/108', false],
['https://user@docs.example.org/article/108', false],
['https://docs.example.org:8443/article/108', false],
['javascript:alert(1)', false]
];
let pass = 0, allow = 0;
for (const [target, expected] of vectors) {
const actual = allowed(target);
if (actual !== expected) throw Error(`Mismatch ${target}`);
if (actual) allow++;
pass++;
}
console.log(`WEB-1010-17 cases=${pass} allow=${allow} block=${pass-allow}`);
测试结果与运行状态必须分开写。 这里纯函数的预期是 cases=8 / allow=3 / block=5。而真实网络访问仍为 NOT_RUN,初始 https://docs.example.org/article/108 仅展示为方案中的模拟文档。将“八条本地规则全部符合预期”写成“HTTPS跳转已经在真实设备安全运行”,既不准确,也不利于后续验收。

主界面提供一个可对照的状态面板,固定文档“多设备文档排版”、版本4和 NAV_GUARDED。这一页不是浏览器成功截图,更像是把导航策略以产品可理解的方式呈现:允许查看什么、拒绝什么以及为什么尚未进入网络集成阶段。查看拦截诊断 进入的也是应用自己的审计页,不会因为查看日志再触发远端文档的重载。
六、一次重定向不能继承第一跳的许可,策略更新也要有代次
八条样例里,伪装域名 docs.example.org.evil.test 被标成一条“模拟重定向的目标”。这个设定用于强调:第一跳允许不意味着下一跳可以跳过检查。在真实 ArkWeb 中,是否会产生某个回调、是哪一个回调、如何处理多级响应,要按实际网络、目标 API 版本和服务端状态验证。本文只给出重定向目标同样交给规则判断的要求,不编造已经覆盖所有重定向链的结论。
另外一种更隐蔽的乱序发生在策略配置自己更新时。譬如用户已打开旧配置 policyEpoch=3,管理员刚把白名单升级为版本4,旧页面又发来一条延迟导航事件。业务层不能只看一个 allowed 布尔值,还要记录它依据的策略版本。当前 Demo 将策略版本固定为4;正式实现应在事件评价和最终消费之间带上版本,旧版本结果不得直接提升为“当前可信”。
这不是让 ArkWeb 支持一个新的“取消旧请求”API。应用层可以阻止自己用过期判断做新动作,但不能保证系统里已经开始的所有网络请求被同步回滚。因此,对于从文档页进入下载、身份认证、支付等敏感流程,强约束应放到真正执行动作的业务服务端,而不能仅依靠 Web 控件里的一个拦截回调。
审核日志同样应当最小化。记录 reason、policyEpoch 和经过脱敏的 path 通常已经足够;不要把 URL 中的 token、用户名、手机号和完整查询参数原样写进 HiLog。本文示例全部没有真实用户凭据,故意用 user@ 作为语法测试。生产中要把敏感值过滤策略置于日志适配器内,而不是依赖各个调用方主动记得删除。
七、诊断页要能回答“为何拒绝”,但不应制造成功幻觉
在 LinkAuditPage 中最应该出现的是决策表,而不只是“拦截成功”的勾。第一行到第三行 ALLOW;第四行 BLOCK_SCHEME;第五行 BLOCK_HOST;第六行 BLOCK_CREDENTIALS;第七行 BLOCK_PORT;第八行 BLOCK_SCHEME。redirectBlocked=1 对应第五行的模拟链路,仍计入五个拒绝内。日志里的文档当前地址与策略版本也要固定,否则图文很难核对。
诊断页还区分三个不同状态:NAV_GUARDED 表示本地规则准备好,不表示 Web 页面已经渲染;network=NOT_RUN 表示没有真实网络加载证据;policyEpoch=4 是规则修订号,不是HarmonyOS API版本。这种状态分层能防止产品、测试和作者对同一个绿色标签作出不同解释。若未来接上真实Web,应加入主frame进入次数、回调触发路径和服务器响应证据,并且单独建立设备结果字段。

实际验收可以准备三组设备侧观察。第一组在已授权测试域名打开 /article/108,记录 onControllerAttached、onPageBegin、onPageEnd 的顺序和页面是否可读;第二组让测试站点返回跳到未授权主机的重定向,核对 onLoadIntercept 或 onOverrideUrlLoading 的实际覆盖;第三组测试 about:blank、页面内iframe、非标准端口、断网和证书异常,确认产品面对未知结果时保持关闭。任何一组缺少证据,都不能据此声称完成系统级安全验收。
要额外区分 onInterceptRequest。它用于拦截请求并返回资源响应数据,不是本文的主流程。错误地在里面直接替换HTML,可能遮盖真正的重定向来源,甚至把跨域资源问题变成错误的业务判断。本文选择只做导航资格判断,是为了让工程问题保持聚焦;真正的内容安全策略、CSP和网络证书校验另成独立工作项。
八、工程交付时留下什么,比一张通过界面更重要
把这类Demo交给同事时,我会要求同时保留 NavPolicy.ets、八条黄金向量、策略版本、拒绝原因以及一张明示 NOT_RUN 的集成待办。规则不应散落在页面各种回调中;只有这样,回归检查才能在没有真机时先完成纯逻辑部分,在有设备和测试站点后再补上真实页面与网络链路证据。
这一方案的边界也明确:当前代码验证 URL 形态和路由范围,没有验证真实站点归属,未处理所有重定向与子资源,无法代替后端授权,不讨论支付或下载。Web 回调的精确参数仍以所用 DevEco Studio SDK 的 .d.ts 和真机行为为最终依据。若编译出现签名调整,应先依据官方文档修正适配器,而不是为了凑一张“已运行”截图删除关键检查。
从工程取舍看,最值得保留的不是那五条拒绝规则,而是把导航许可写成纯函数,把 Web 事件只当成输入通道,把设备网络验证和本地规则验证分开。这样文档H5扩大了内容范围,也不必同步扩大应用信任边界。下一步先拿到可控 HTTPS 测试域名与服务器重定向夹具,再把 NOT_RUN 一项项替换为可追溯的设备证据,才是真正意义上的闭环。
还有一个经常被低估的验收维度:用户按返回键。Web自身的历史记录和ArkUI页面路由栈不是同一套结构。在文档阅读器里,用户按返回时,应用必须先决定是回退Web历史页,还是关闭整个原生容器。无论采用哪种交互方案,不能因为回退动作“不像一个新跳转”就假设它必然在白名单内。对于返回到旧页面的情况,要记录当前可见URL和策略版本,必要时退到一个原生安全页,而不是盲目重放历史字符串。这个问题需要真机测试实际触发事件,本文不擅自宣称backward()在所有设备上都重新经过相同拦截链。
另外,错误页与登录页不能成为白名单后门。发生网络异常时,有些团队会把错误原因转成 file://、resource:// 或自定义HTML网页再塞进原有Web组件。它们的安全约束与HTTPS不同;若产品确实需要离线错误页,应当独立约定加载来源、允许资源和退出行为,不宜直接给 NavPolicy 加上一条“允许所有本地协议”。一个原生错误卡片往往更容易说明当前内容不可用,也更容易避免离线页意外继承其它Web权限。
从测试设计看,除了八条正向黄金向量,还应该建立扩展回归池:大小写混合的主机、尾点域名、编码斜杠、编码的点号、过长URL、无效百分号、Unicode域名、重复查询键和片段路由。这里不预设各类编码的最终解析结果,而是记录原始输入、解析后的关键字段、最终判定三联数据。只要开发中修改了SDK版本、URL解析模块或政策修订号,就重新执行;若某个样本在不同环境出现不同归一化结果,宁可暂时拒绝并升级规则,也不要安静地放行。
日志和告警也需要限流。恶意网页可能反复触发被拦截的导航,若每次把完整URL、堆栈和时间戳同步刷新到ArkUI状态,诊断系统本身就可能带来卡顿。可以按reason + policyEpoch聚合计数,仅保留有限长度的最近事件,并设置最大缓存条数。这样既能看到“BLOCK_HOST在一分钟内出现了多少次”,又不会使一个本应轻量的文档组件变成无界日志容器。聚合统计用于排查,不自动等价于攻击证据。
九、参考与边界
华为 ArkWeb 白屏排查与生命周期FAQ(更新时间2026-06-26):https://developer.huawei.com/consumer/cn/doc/doccenter-dev-faq/faqs-arkweb-174
华为“如何解析URL信息”(更新时间2026-06-26):https://developer.huawei.com/consumer/cn/doc/doccenter-dev-faq/faqs-arkts-159
华为 ArkWeb 折叠屏分栏及导航拦截示例(更新时间2026-06-26):https://developer.huawei.com/consumer/cn/doc/doccenter-dev-faq/faqs-arkweb-194
以上链接用于标明接口来源及能力边界。本文八条URL输入、计数、日志和文中所有生成图片均是设计演示素材,不构成真实设备运行、安全测试、实际网络访问或者发布审核通过的证明。
更多推荐




所有评论(0)