ArkWeb 开发手记 10|安全加固、权限最小化、漏洞防护、审计日志与应用市场上架规范
前置阅读:01~09 篇
本篇目标:补齐 ArkWeb 混合应用所有安全短板,满足鸿蒙应用市场安全检测要求,规避 JS 注入、越权、敏感信息泄露等线上高危漏洞。
前面 9 篇,我们把 ArkWeb 容器从基础搭建、JSBridge 双向通信、SPA 路由、请求拦截、离线包、文件上传、性能内存优化全部实现。功能全部跑通,但是功能可用 ≠ 安全可用。 很多混合项目,开发调试一切正常,提交鸿蒙应用市场审核直接驳回;或者上线后存在高危漏洞,存在信息泄露、JS 注入风险。 这一篇就是整套连载的收尾终章,专门讲 ArkWeb 安全加固、风险点、最小权限原则、审计日志、防调试、防篡改,把容器打磨到符合企业安全规范、应用商店上架标准。

一、ArkWeb 混合开发最常见的几类高危安全漏洞
先盘点实际项目中最容易踩的安全坑,很多开发者完全没有意识到风险:
- JS 注入漏洞:H5 页面可通过 runJavaScript 执行任意原生代码,窃取 APP 本地存储、设备敏感信息
- JSBridge 无校验:任意 H5 页面都可以调用所有原生接口,不受域名白名单限制,加载恶意 H5 就会越权
- 权限过度申请:一次性申请相机、位置、文件读写全部权限,应用市场审核直接打回
- 敏感信息裸传:Token、用户凭证直接注入 Web 页面,H5 可直接拿到凭证
- 允许访问 file 协议:恶意 H5 读取 APP 沙箱本地文件,泄露本地数据
- 调试模式未关闭:正式包保留 Web 调试开关,外部可通过 Chrome 远程调试页面
- 未做证书校验:HTTPS 页面不校验证书,中间人劫持,替换 H5 页面
- 无操作审计:JSBridge 调用、文件上传、接口拦截没有日志,出现安全问题无法溯源
核心安全原则:不信任任何 H5 页面。哪怕是自己业务 H5,也要做严格限制,假设 H5 已经被篡改。
二、基础安全配置:Web 组件开关管控,关闭不必要能力
ArkWeb 很多能力默认开启,上线前必须按需关闭,减少攻击面。
Web({
src: this.currentUrl,
controller: this.webController
})
.javaScriptAccess(true) // 业务需要JS则开启,纯静态页面可以直接关闭
.domStorageAccess(true) // 按需开启localStorage
.databaseAccess(false) // 不使用Web SQL数据库,直接关闭
.fileAccess(false) // 禁止file协议访问本地文件,高危!关闭
.imageAccess(true)
// 关闭Web调试,生产环境必关
.webDebuggingAccess(false)
重点说明:
.webDebuggingAccess(false):关闭远程调试。打包 release 包必须关闭,开启会被安全扫描判定为高危风险。开发阶段临时打开调试,上线代码务必移除。.fileAccess(false):禁止 H5 访问本地文件,防止恶意页面读取沙箱数据。- JavaScriptAccess:如果你的 H5 页面完全静态、不需要 JS 交互,直接设置 false,从根源杜绝 JS 注入。

三、最重要:JSBridge 域名白名单 + 接口权限隔离
这是混合应用安全的重中之重。 默认情况下,只要加载了 H5,H5 就能调用所有注册到 nativeBridge 的方法。如果你的页面跳转到外部域名、被劫持跳转到恶意页面,对方可以直接调用全部原生能力。
解决方案:域名白名单,只允许可信域名的 H5 调用原生 Bridge 接口;非白名单域名,直接禁用全部 JSBridge 能力。
3.1 域名白名单实现代码
// 可信业务域名列表,只保留自己的业务域名
const TRUST_HOST_LIST = [
"https://business.example.com",
"https://static.example.com",
"https://app-local" // 离线包虚拟域名
];
// 判断当前页面域名是否可信
function isTrustDomain(url: string): boolean {
return TRUST_HOST_LIST.some(host => url.startsWith(host));
}
在页面跳转、onLoadStatusChange 中监听当前页面地址,非可信域名,注销 JSBridge 代理。
.onLoadStatusChange((status) => {
if(status === WebLoadStatus.FinishLoading){
const currentUrl = this.webController.getUrl();
if(isTrustDomain(currentUrl)){
// 可信域名,注册bridge
this.webController.registerJavaScriptProxy(
this.jsBridge,
"nativeBridge",
["getDeviceInfo", "showNativeToast", "pushSpaRoute"]
)
}else{
// 外部不可信域名,删除JSBridge,禁止调用原生能力
this.webController.deleteJavaScriptProxy("nativeBridge");
console.warn("当前域名不在白名单,关闭JSBridge能力", currentUrl);
}
}
})
3.2 JSBridge 入参校验,防注入
就算是可信域名,H5 传递的参数也不能直接信任。所有传入参数必须校验类型、过滤特殊字符,不能直接丢进 runJavaScript。 改造 JSBridge 方法示例,增加参数校验:
class JsBridgeModel {
// 不安全写法:直接接收参数不校验
// async getNativeData(params){ ... }
// 安全版本:增加参数校验
async getNativeData(params: Record<string, Object>) {
// 参数类型校验
if (!params || typeof params !== "object") {
return {code: -1, msg: "参数非法"};
}
// 过滤危险字符,防止注入
if(JSON.stringify(params).includes("javascript:")){
return {code: -1, msg: "参数包含非法内容"};
}
// 业务逻辑...
return {code:0, data:{}}
}
}
禁止做法:不要直接拼接 H5 传入的字符串到 runJavaScript,极易造成 JS 注入。
四、权限最小化:动态权限、按需申请,用完释放
前面第 9 篇我们用到相机、媒体读取权限。安全规范要求:不要在 APP 启动一次性申请全部权限,用到的时候再申请,不用时不持有权限。
错误做法:module.json5 一次性申请相机、位置、文件、媒体所有权限,APP 打开就弹窗索要全部权限,应用市场审核扣分。 正确流程:
- 用户点击上传拍照按钮时,才动态申请 CAMERA 权限
- 用户取消上传,不持有权限
- 不需要的权限,不要在配置文件声明
import { abilityAccessCtrl, Permissions } from '@kit.AccessibilityKit';
// 动态申请权限示例
async function requestPermission(permission: Permissions):Promise<boolean>{
const atManager = abilityAccessCtrl.createAtManager();
let result = await atManager.requestPermissionsFromUser(getContext(), [permission]);
return result.authResults[0] === 0;
}
业务逻辑:点击拍照时,先调用 requestPermission 申请 CAMERA,用户同意,再唤起相机。
权限分级策略:
- 基础页面:不需要任何敏感权限
- 文件上传模块:仅在用户点击上传时申请 READ_MEDIA
- 拍照上传:点击拍照才申请 CAMERA
- 定位相关 H5:打开定位页面时才申请位置权限
五、HTTPS 证书校验,防中间人劫持
如果 H5 页面使用 HTTPS,需要校验服务端证书,防止中间人攻击劫持网络,替换 H5 页面。 通过 onSslError 捕获证书异常,证书不合法,直接阻断页面加载,拒绝继续访问。
.onSslError((event)=>{
console.error("SSL证书异常", event.error);
// 证书错误,拦截页面加载,不继续展示页面
event.handled = true;
this.loadStatus = "页面证书不安全,访问已拦截";
return true;
})
不要在生产环境写忽略证书错误的代码,这个是高危漏洞,安全扫描直接拦截上架。仅本地开发调试临时使用。

六、敏感数据保护:Token、用户信息不要直接暴露给 H5
很多项目直接把登录 token 注入 H5 页面,一旦 H5 被篡改,token 直接被盗取。 推荐方案:
- JSBridge 接口按需返回数据,H5 主动拉取,而不是页面初始化一次性注入全部凭证
- 接口 Token 放在原生请求头(前面第 6 篇 onResourceLoad 注入 Header),H5 拿不到完整 Token
- Token 设置短期有效期,就算泄露,风险窗口有限
- 用户退出登录,立刻清除 Web 缓存、Cookie,销毁 Web 实例
退出登录清理代码:
// 用户登出,清理Web所有会话缓存
async function clearWebSession(webController:WebController){
await webController.clearCache(true);
await webController.clearAllSslCache();
webController.deleteJavaScriptProxy("nativeBridge");
}
七、安全审计日志:操作记录留存,方便溯源
生产环境建议增加审计日志,记录所有高风险操作:
- JSBridge 调用记录(调用方法、调用域名、参数摘要,不要记录完整敏感参数)
- 文件上传记录:上传时间、文件类型、文件大小
- 页面跳转记录,记录跳转域名
- SSL 异常、拦截页面访问日志
简易审计日志示例:
// 审计日志工具,只记录摘要,避免记录隐私明文
function writeSecurityLog(type:string, msg:string){
const log = `[SECURITY][${new Date().toISOString()}] type:${type}, info:${msg}`;
console.info(log);
// 可扩展:上报到后端安全日志平台
}
在 JSBridge 入口、onShowFileSelector 上传触发、证书拦截位置插入日志。
八、常见安全检测不通过的坑点 & 修复方案
表格
| 风险项 | 问题描述 | 修复方案 |
|---|---|---|
| Web 远程调试开启 | release 包 webDebuggingAccess=true | 生产包关闭调试开关 |
| File 访问开启 | fileAccess=true,允许 H5 读取本地文件 | fileAccess(false) |
| JSBridge 无域名白名单 | 任意网页都可以调用原生接口 | 增加可信域名列表,外部域名删除 bridge |
| 权限过度申请 | 一次性申请大量敏感权限 | 按需动态申请权限 |
| 忽略 SSL 证书错误 | 代码忽略 ssl 异常,允许不安全 HTTPS | onSslError 拦截,禁止访问 |
| 明文传递 token 给 H5 | H5 可直接拿到登录凭证 | 原生注入请求头,H5 无法获取 token |
| 不校验 JS 入参 | H5 任意参数直接执行 | 参数类型校验,过滤危险字符 |
九、完整集成安全加固后的最终页面代码
整合前面 9 篇的容器能力,叠加本篇全部安全策略,最终可提交上架的完整 ArkWeb 页面。
import { Web, WebController, WebLoadStatus } from '@kit.ArkWeb';
import { router } from '@ohos/router';
import { JsBridgeModel, SpaRouteItem } from './JsBridgeModel';
import { abilityAccessCtrl, Permissions } from '@kit.AccessibilityKit';
// 可信域名白名单
const TRUST_HOST_LIST = [
"https://business.example.com",
"https://static.example.com",
"https://app-local"
];
@Entry
@Component
struct ArkWebSecurePage {
private webController: WebController = new WebController();
private jsBridge: JsBridgeModel = new JsBridgeModel();
@State spaRouteStack: SpaRouteItem[] = [];
@State loadStatus: string = "页面待加载";
aboutToAppear() {
this.jsBridge.onSpaRouteChange = (route) => {
if (route.isReplace) this.spaRouteStack.pop();
this.spaRouteStack.push(route);
};
}
onPageShow() {
this.webController.resume();
}
onPageHide() {
this.webController.pause();
}
aboutToDisappear() {
this.jsBridge.h5Callback = null;
this.jsBridge.onSpaRouteChange = null;
this.spaRouteStack = [];
this.webController.destroy();
}
private handleSpaBack() {
this.spaRouteStack.length > 1 ? (this.spaRouteStack.pop(), this.webController.runJavaScript(`history.back()`)) : router.back();
}
build() {
Column() {
Text(`SPA栈深度:${this.spaRouteStack.length} | ${this.loadStatus}`)
.fontSize(14)
.margin(10)
Web({
src: "https://business.example.com/index.html",
controller: this.webController
})
.width('100%')
.height('88%')
.javaScriptAccess(true)
.fileAccess(false) // 禁止本地文件访问
.webDebuggingAccess(false) // 关闭远程调试,生产必关
.databaseAccess(false)
// SSL证书校验
.onSslError((event) => {
console.error("SSL证书不安全,阻断访问");
this.loadStatus = "页面证书不安全,访问拦截";
event.handled = true;
return true;
})
// 页面加载完成,校验域名白名单,控制JSBridge注册
.onLoadStatusChange((status) => {
if(status === WebLoadStatus.FinishLoading){
const url = this.webController.getUrl();
if(TRUST_HOST_LIST.some(host => url.startsWith(host))){
writeSecurityLog("TRUST_DOMAIN", `加载可信页面:${url}`);
this.webController.registerJavaScriptProxy(
this.jsBridge,
"nativeBridge",
["getDeviceInfo", "showNativeToast", "pushSpaRoute"]
)
}else{
writeSecurityLog("UNTRUST_DOMAIN", `拦截外部页面bridge:${url}`);
this.webController.deleteJavaScriptProxy("nativeBridge");
}
this.loadStatus = "页面加载完成";
}else if(status === WebLoadStatus.ErrorLoading){
this.loadStatus = "页面加载失败";
}
})
// 文件上传钩子,继承第9篇上传逻辑,增加安全日志
.onShowFileSelector((param) => {
writeSecurityLog("UPLOAD_TRIGGER", "H5触发文件选择");
return true;
})
}
.width('100%')
.height('100%')
.onBackPressed(() => {
this.handleSpaBack();
return true;
})
}
}
// 安全审计日志
function writeSecurityLog(type:string, msg:string){
const logStr = `[SECURITY][${new Date().toISOString()}] type:${type}, msg:${msg}`;
console.info(logStr);
}

十、整套 10 篇系列全局验收清单(上线前一次性自检)
到这里 10 篇连载全部结束,上线前对照清单逐项检查: ✅ 基础容器:Web 组件生命周期,页面销毁 destroy,内存释放 ✅ JSBridge 双向通信:异步回调、参数校验、域名白名单 ✅ SPA 路由:双栈管理,返回键处理,修复白屏 ✅ 请求拦截:Header 注入、资源拦截、下载接管 ✅ 离线包:虚拟域名,rawfile 资源映射,SPA fallback ✅ 文件上传:onShowFileSelector,拍照图库,防抖防重复点击 ✅ 性能优化:内核预热、缓存策略、崩溃监听、内存泄漏防护 ✅ 安全加固:关闭调试、禁止 file 访问、SSL 校验、权限最小化、审计日志
全部检查通过,你的 ArkWeb 混合容器就可以交付测试、提交鸿蒙应用市场审核。
十一、终章总结
《ArkWeb 混合开发手记 01~10》全套连载到此正式完结。 我们从最简单 Web 组件创建起步,一步步搭建企业级鸿蒙混合容器:
- 基础渲染、生命周期管理
- JSBridge 双向通信,前端和鸿蒙原生互调
- SPA 单页应用路由栈管理,解决返回键混乱、白屏问题
- 请求拦截、请求头注入、下载接管
- 离线包 + 虚拟域名,实现无网 H5 访问
- H5 表单文件、拍照、图库上传原生接管
- 性能优化、内存管控、内核崩溃防护
- 最后安全加固,漏洞防护,满足上架安全检测
读完整套手记,你完全可以独立基于 HarmonyOS7 开发一套商用混合应用容器,覆盖绝大多数业务场景。
拓展延伸方向(番外可选)
- Web 页面截图、H5 调用原生扫码
- 多 Web 实例隔离,多窗口 H5 场景
- 埋点采集,H5 和原生统一埋点体系
- 灰度、版本控制、热更新全流程

更多推荐


所有评论(0)