1、引言

在混合开发和浏览器架构体系中,端侧下载拦截与管控一直是体现浏览器内核控制力的试金石。
image.png
当用户在前端页面点击一个下载链接时,往往并非直接发起到真实文件的请求,而是会经历多次的 301/302 重定向调度;更复杂的场景下,服务端的防盗链机制会严密校验请求的源地址(Referrer)。过去,由于底层内核封装过深,我们一旦拦截到 Web 的下载任务(WebDownloadItem),只能被动拿到最终解包出来的流或者目标地址,这导致开发者难以进行精准的源头审计,更无法在定制下载器时透传原始上下文给服务端以绕过安全防盗链。

为了彻底打通这一安全与溯源的壁垒,HarmonyOS NEXT 6.1.1 (API 24) 针对 ArkWeb 核心的下载控制模块(WebDownloadItem)新增了两道关键溯源接口:

  1. getOriginalUrl():一针见血地还原引发下载动作的初始、最原始 URL,无视任何中间重定向乱象。
  2. getReferrerUrl():无缝提取引发下载的引荐页来源,为第三方定制下载器补充防盗链通行证。
    image.png

    2、Kit能力介绍

本次更新隶属于 @kit.ArkWeb 套件中的下载委托层(WebDownloadDelegate)。ArkWeb 为开发者提供了接管浏览器默认下载行为的高阶权利。当开发者通过 webview.WebviewController.setDownloadDelegate 挂载了自定义的下载委托后,每一次下载任务的产生都会生成一个高度封装的 WebDownloadItem 对象,并抛给端侧由应用自行决定写入位置和进度分发。

3、Kit API介绍

在 API 24 之前,WebDownloadItem 主要提供了获取 GUID、获取下载进度百分比、甚至建议文件名(getSuggestedFileName)的能力。本次补齐了溯源闭环,这两个接口均仅支持在 Stage 模型下使用,无须特殊权限。

// 1. 获取下载文件的原始 URL 地址(重定向前用户点击或系统请求的第一现场地址)
getOriginalUrl(): string;

// 2. 获取下载文件的 referrer 地址(即用户在点击下载链接前停留的那个网页)
getReferrerUrl(): string;

4、Kit 6.1 新增特性介绍

4.1 getOriginalUrl:拨开重定向的迷雾

许多大厂的下载链接采用统一分发调度策略,例如 https://d.example.com/latest,但在真实发起 HTTP 请求时,会返回 302 状态码,跳转到带有时效签名与 CDN 节点的真实长链接。
此时如果我们试图自行接管后续下载行为或用于统计归因,如果只拿到真实长链接,是无法与业务中台核对发版配置的。getOriginalUrl 就能将这个伪装褪去,直接告诉应用代码:用户点下的是那个 /latest

4.2 getReferrerUrl:重塑防盗链请求的利器

很多企业的私有附件系统设置了严格的 Referrer 防盗链策略,如果直接把 WebDownloadItem 中的链接扔给第三方框架(如 request 模块)进行断点续传下载,由于缺失原始上下文,会直接遭遇 403 拒绝访问。
通过提取 getReferrerUrl(),开发者可以在自己构建的 HTTP Request Header 中原样塞入 Referer 字段,实现防盗链策略的完美“欺骗”与通过。

5、6.1新增特性项目实战

我们将以上特性整合成一个直观的 「ArkWeb 下载溯源防线控制舱」

5.1 智能中控入口页:Index.ets

在我们的 Demo 中,准备了一个文本输入框来模拟发起 startDownload 请求,并隐藏了一个微型 Web 实例用于承载 Web 控制器内核。

5.2 核心特性控制舱:ArkWebDemo.ets

我们在控制舱内注册了 WebDownloadDelegate 监听器,并在其 onBeforeDownload 回调钩子内强行拦截下载流程,打出日志,最终指定将文件写入安全的应用沙盒中。

import { webview } from '@kit.ArkWeb';
import { BusinessError } from '@kit.BasicServicesKit';

@Entry
@Component
struct ArkWebDemo {
  controller: webview.WebviewController = new webview.WebviewController();
  delegate: webview.WebDownloadDelegate = new webview.WebDownloadDelegate();
  @State logs: string[] = [];
  @State inputUrl: string = 'https://www.example.com/download/test.zip';

  private appendLog(msg: string) {
    let now = new Date();
    let timeStr = `${now.getHours()}:${now.getMinutes()}:${now.getSeconds()}.${now.getMilliseconds()}`;
    this.logs.unshift(`[${timeStr}] ${msg}`);
  }

  aboutToAppear() {
    this.appendLog('🚀 ArkWeb 下载溯源控制舱已初始化');
    this.setupDownloadDelegate();
  }

  setupDownloadDelegate() {
    try {
      this.delegate.onBeforeDownload((webDownloadItem: webview.WebDownloadItem) => {
        // [核心新增能力 1] 获取未发生重定向前的起始请求地址
        let originalUrl = webDownloadItem.getOriginalUrl();
        // [核心新增能力 2] 获取触发此次下载事件的引荐页面来源
        let referrerUrl = webDownloadItem.getReferrerUrl();
        
        let guid = webDownloadItem.getGuid();
        let fileName = webDownloadItem.getSuggestedFileName();
        
        this.appendLog(`📥 拦截到下载请求 [Guid: ${guid}]`);
        this.appendLog(`🔗 原始触发 URL: ${originalUrl}`);
        this.appendLog(`🔎 Referrer 溯源: ${referrerUrl}`);
        this.appendLog(`📄 建议文件名: ${fileName}`);

        // 拼接沙盒下载路径:写入到应用的 el2 cache 目录
        let downloadPath = "/data/storage/el2/base/cache/web/" + fileName;
        this.appendLog(`📁 开始写入沙盒路径: ${downloadPath}`);
        
        // 允许并启动内核接管写入流程
        webDownloadItem.start(downloadPath);
      });

      this.delegate.onDownloadUpdated((item) => this.appendLog(`⏳ 进度: ${item.getPercentComplete()}%`));
      this.delegate.onDownloadFailed((item) => this.appendLog(`❌ 下载失败 [Guid: ${item.getGuid()}]`));
      this.delegate.onDownloadFinish((item) => this.appendLog(`✅ 下载完成 [Guid: ${item.getGuid()}]`));

      // 绑定委托机制到控制台
      this.controller.setDownloadDelegate(this.delegate);
      this.appendLog('🛡️ 下载委托监听已挂载');
    } catch (error) {
      let err = error as BusinessError;
      this.appendLog(`❌ 委托挂载失败 Msg: ${err.message}`);
    }
  }

  triggerDownload() {
    try {
      this.appendLog(`🌐 尝试触发下载: ${this.inputUrl}`);
      this.controller.startDownload(this.inputUrl);
    } catch (error) {
      let err = error as BusinessError;
      this.appendLog(`❌ 触发下载异常 Msg: ${err.message}`);
    }
  }

  build() {
    Column() {
      Text('ArkWeb 下载溯源防线').fontSize(22).fontWeight(FontWeight.Bold).margin({ top: 40, bottom: 20 })
      TextInput({ text: this.inputUrl }).onChange((val) => this.inputUrl = val).width('90%').margin({ bottom: 10 })
      Button('触发下载并溯源').onClick(() => this.triggerDownload()).backgroundColor('#007DFF').width('90%').margin({ bottom: 20 })
      List({ space: 8 }) {
        ForEach(this.logs, (log: string) => {
          ListItem() { Text(log).fontSize(12).fontFamily('monospace').fontColor('#333333') }
        }, (log: string) => log)
      }.width('90%').height('40%').backgroundColor('#EFEFEF').padding(10).borderRadius(8)
      
      // 隐形 Web 底座
      Web({ src: 'www.example.com', controller: this.controller }).width(1).height(1).visibility(Visibility.Hidden)
    }.width('100%').height('100%')
  }
}

6、运行效果

当在界面中点击 “触发下载并溯源” 后,控制台日志自下而上依次展示了完美的溯源链路:

  1. 委托成功挂载的初始提示。
  2. “尝试触发下载: https://www.example.com/...” 被打印。
  3. onBeforeDownload 回调被瞬间引爆,屏幕精准提取并打印出了 🔗 原始触发 URL 以及 🔎 Referrer 溯源 数据(由于此处是直连且无特殊重定向,两者往往一致或为空;但当在带有网页嵌套及重定向的真实前端交互中,它将精准刻画用户点击跳转图谱)。
  4. 系统提取出 建议文件名,并将其与 /data/storage/el2/base/cache/web/ 结合,将数据流稳定注入沙盒。
  5. 最终提示 ✅ 下载完成
    image.png
    image.png

    7、避坑指南

[!WARNING] 注意事项
生命周期闭环切勿遗漏 start 调用:当你在 onBeforeDownload 中拦截了系统内核抛出的 webDownloadItem 后,这相当于你彻底劫持了本次下载过程。此时,你必须显式地调用 webDownloadItem.start(path) 给出一个合法的文件写入路径;或者如果你决定不下载,需要进行相关销毁处理。如果拦截后既不保存路径也不调用任何操作,内核引擎会陷入等待流状态,可能会诱发底层资源隐形泄露或崩溃!

8、总结

通过 HarmonyOS 6.1.1 在 ArkWeb 下载项上补充的 getOriginalUrlgetReferrerUrl 的“侦测探针”,浏览器内核的安全控制颗粒度更上一层楼。它不仅让应用侧彻底摆脱了“无法感知前置重定向”的被动局面,还赋予了第三方下载器接力时完美复刻 HTTP 上下文(尤其是突破防盗链)的核心权力。这标志着 ArkWeb 正在成为一个真正具备全链路掌控感的高级浏览引擎。


轻口味
39.5k 声望5.9k 粉丝

移动端十年老人,主要做IM、音视频、AI方向,目前在做鸿蒙化适配,欢迎这些方向的同学交流:wodekouwei