一个混合应用,原生侧已经完成了登录,用户信息、token 都存在内存里。进入“活动中心”页面,Web 组件加载运营的 H5 页面,页面加载完成后弹出了一个登录框。用户明明刚在原生里登录过,H5 却完全不认识他。你在原生登录成功之后调了 WebCookieManager.configCookie(url, cookie),返回值是 0,语义是成功。H5 页面加载完成,document.cookie 读出来是空的。你把 configCookie 挪到 onPageBegin 之前、onControllerAttached 之后、onPageEnd 里各试了一遍,有时候能读到一部分 Cookie,有时候什么都没有,有时候 H5 登录之后原生侧调接口仍然带着未登录的 Cookie。

问题不在 configCookie 有没有调用,在于你把“给 WebView 设置 Cookie”当成了一个全局开关。configCookie 的语义是“在指定的 URL 下,写入一条指定 domain 和 path 的 Cookie”。这条 Cookie 会不会被 H5 页面读到、会不会被带到 H5 发出的请求里,取决于这条 Cookie 的 domain、path、SameSite 属性,以及你写入的时机是否早于页面发起的请求。这些条件中任何一条不满足,configCookie 返回的成功只代表“这条记录被写进了 Cookie 库”,不代表“H5 会用它”。

domain 和 path 是 Cookie 的发送门槛,不是备注

configCookie 的第二个参数是 url,第三个是 cookie 字符串。很多人把 url 当成“在哪个页面下设置这个 Cookie”,把 cookie 当成一个键值对,忽略了 cookie 字符串里还可以携带 domain、path、Secure、SameSite 这些属性。这些属性不是可选的装饰,它们决定了浏览器引擎在发起请求时,会不会把这条 Cookie 带上。

一个 Cookie 能被带到某个请求上,需要同时满足三个条件:请求的域名匹配 Cookie 的 domain,请求的路径匹配 Cookie 的 path,请求的 scheme 匹配 Cookie 的 Secure 属性。configCookie 的 url 参数决定了这次写入操作的“上下文域名”,但 Cookie 最终的作用域由 cookie 字符串里显式声明的 domain 决定。不写 domain 时,默认使用 url 的 host;url 是 https://m.example.com/page,默认 domain 是 m.example.com,这条 Cookie 不会被带到 api.example.com 的请求上。

typescript

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

function setAuthCookie(token: string): void {
  // domain 显式写父域,才能被子域请求带上
  const cookie = `auth_token=${token}; domain=.example.com; path=/; Secure; SameSite=Lax`;
  // url 是写入上下文,domain 决定作用域
  webview.WebCookieManager.configCookie('https://m.example.com/', cookie);
}

domain=.example.com 前面的点不是装饰,它表示“这个 Cookie 对 example.com 及其所有子域生效”。不写这个点,domain 只匹配精确的 example.com,m.example.com 和 api.example.com 都拿不到。path=/ 表示根路径下所有请求都带,不写或者写成 /activity,只有路径前缀匹配的请求才会带。Secure 表示只在 HTTPS 请求中发送,如果你的 H5 页面或者接口有 HTTP 的,这条 Cookie 不会被带上。SameSite=Lax 控制跨站请求时 Cookie 的发送行为,Strict 最严格,None 需要配合 Secure,不写时不同内核版本的默认值可能不同。

configCookie 的写入时机,必须早于页面发起请求

Cookie 是随请求头发送的,请求发出之后,服务端返回的响应里有没有 Set-Cookie、页面有没有登录态,在请求发出的那一刻就已经决定了。configCookie 写入的 Cookie,只对写入之后发起的请求生效。如果 H5 页面的 HTML 加载时就已经发起了登录态校验的接口请求,你在 onPageEnd 里才 configCookie,那个请求已经带着空的 Cookie 出去了,服务端的响应是“未登录”,页面已经渲染了登录框。

正确的时机是 onControllerAttached 之后、页面开始加载之前。onControllerAttached 保证 controller 可用,loadUrl 或者 Web 组件构造函数里的 src 触发的加载在它之后。在这个窗口里 configCookie,页面首次加载时的所有请求都会带上你写入的 Cookie。

typescript

Web({ src: this.url, controller: this.controller })
  .onControllerAttached(() => {
    // 页面尚未开始加载,此时写入的 Cookie 对首次请求生效
    webview.WebCookieManager.configCookie(
      'https://m.example.com/',
      `auth_token=${this.token}; domain=.example.com; path=/; Secure; SameSite=Lax`
    );
  })
  .onPageEnd(() => {
    // 页面已加载完成,此时写入只影响后续的 XHR/fetch 请求
    // 已经发出的请求和已经渲染的页面不会重来
  })

onPageEnd 里写入的 Cookie 对页面后续的 fetch 或 XMLHttpRequest 有效,但对页面首次加载时的请求无效。如果登录态校验发生在页面加载阶段,onPageEnd 写入就晚了。

H5 侧设置的 Cookie,原生侧看不到,直到 saveCookieAsync

反向的问题同样常见。H5 页面里调用 document.cookie = 'auth_token=xxx',或者服务端在响应头里返回了 Set-Cookie,原生侧调接口时却没有带上这个 Cookie。这是因为 Web 内核管理着 Cookie 库,H5 侧设置的 Cookie 先进入 Web 内核的内存 Cookie 存储,它不会自动同步到原生的网络请求层。

原生侧要读取 WebView 的 Cookie,需要调用 webview.WebCookieManager.fetchCookie(url) 主动查询;H5 侧修改的 Cookie 要持久化到磁盘,需要调用 saveCookieAsync()。fetchCookie 返回的是当前 URL 下匹配的 Cookie 字符串,saveCookieAsync 把内存中的 Cookie 刷到磁盘上,应用重启之后才能恢复。

typescript

async function syncCookieToNative(url: string): Promise<string> {
  // H5 侧设置的 Cookie 先落在 Web 内核内存里,fetchCookie 才能读到
  const cookieStr = await webview.WebCookieManager.fetchCookie(url);
  return cookieStr;
}

// H5 登录完成之后,把 Cookie 持久化,应用重启后仍然有效
webview.WebCookieManager.saveCookieAsync().then(() => {
  // 刷盘完成,后续原生请求可以带上这条 Cookie
});

fetchCookie 的调用时机要在 H5 的 document.cookie 写入之后,在原生需要这个 Cookie 之前。如果你在 H5 页面刚加载时就 fetchCookie,H5 的登录逻辑可能还没执行,Cookie 库里什么都没有。saveCookieAsync 不是可选的清理动作,应用退出时 Cookie 如果还在内存里没刷盘,下次启动 WebView 的 Cookie 库是空的。

Cookie 不跨进程,也不跨 Web 组件实例的独立配置

WebCookieManager 的配置是进程级的。configCookie 写入的 Cookie 对同一个进程内的所有 Web 组件生效,不同进程之间不共享 Cookie。如果你的应用有多个 UIAbility 运行在独立进程里,A 进程里设置的 Cookie,B 进程里的 Web 组件读不到。

同一个进程内的多个 Web 组件共享同一个 Cookie 库,但每个 Web 组件有自己的 WebviewController,configCookie 不区分设置给哪个 controller,它写入的是进程级的 Cookie 存储。如果两个 Web 组件加载了不同域名的页面,Cookie 的 domain 匹配规则决定了它们各自能看到哪些 Cookie,domain 不匹配的 Cookie 不会出现在 document.cookie 里。

应用沙箱被清除数据时,Cookie 库跟着被清空。用户“清除应用数据”之后,之前 saveCookieAsync 持久化的 Cookie 也没了,H5 页面需要重新登录。这和 Preferences 被清空是同一个层级的行为,不是 Cookie 管理出了问题。

SameSite 和第三方 Cookie,跨站场景下的静默拦截

一个更隐蔽的边界来自 SameSite 属性。如果你的 H5 页面通过 iframe 被嵌入到另一个站点的页面里,SameSite=Lax 或 Strict 的 Cookie 在跨站 iframe 的请求中不会被发送。SameSite=None 需要配合 Secure,且要求浏览器引擎允许第三方 Cookie。WebView 的 Cookie 策略可以通过 setThirdPartyCookieEnabled 控制,默认值在不同版本和不同配置下可能不同。

你的页面在主站打开时登录态正常,被嵌入到合作方页面里时登录态丢失,第一嫌疑人不是 Cookie 没设,是 SameSite 或第三方 Cookie 策略拦截了它。DevTools 的 Network 面板里请求的 Cookie 头会直观地显示这条请求带了哪些 Cookie,比对主站和嵌入场景下的差异,比在代码里猜快得多。

typescript

// 允许第三方 Cookie,跨站 iframe 场景下需要
webview.WebCookieManager.setThirdPartyCookieEnabled(true);

setThirdPartyCookieEnabled(true) 的代价是放宽了隐私边界,不应该无条件打开。只在确实有跨站嵌入需求、且 Cookie 的 SameSite 策略配合的情况下开启,不要把它当成“Cookie 不生效就打开”的万能开关。

Cookie 清理是显式的,不会因为页面关闭而消失

configCookie 写入的 Cookie,在页面关闭、Web 组件销毁之后仍然留在 Cookie 库里。如果用户退出登录,你需要显式调用 webview.WebCookieManager.deleteAllCookies() 或者按 URL 删除指定 Cookie,否则下一个用户登录之前,旧的登录态 Cookie 仍然会被带在请求里,服务端可能认为“上一条会话还没结束”。

退出登录的清理应该和 configCookie 的写入对称:登录成功时写入哪些域、哪些路径的 Cookie,退出时就把它们删干净。deleteAllCookies() 会清空整个进程的 Cookie 库,如果有多个业务共用一个 Cookie 库,全量删除会误伤其他业务的 Cookie。按 URL 和 domain 精确删除,比全量清空更安全。

typescript

async function clearAuthCookies(): Promise<void> {
  // 按域精确删除,避免误伤其他业务 Cookie
  await webview.WebCookieManager.deleteCookie(
    'https://m.example.com/',
    'auth_token'
  );
  // 删除之后持久化,防止应用重启后从磁盘恢复旧 Cookie
  await webview.WebCookieManager.saveCookieAsync();
}

deleteCookie 之后不调 saveCookieAsync,内存里的 Cookie 删了,磁盘上的还在,应用重启之后旧 Cookie 会被重新加载回来。删除和持久化必须成对出现。

排查顺序:先看 domain/path,再看写入时机,最后看 H5 侧的同步

下次“原生登录了 H5 还要重登”或者“H5 登录了原生接口还是未登录”的时候,按这个顺序走。

第一步,打印 configCookie 写入的完整 Cookie 字符串,确认 domain、path、Secure、SameSite 有没有写全。domain 不匹配请求的 host、path 不匹配请求的路径、Secure 的 Cookie 走了 HTTP,这三种情况都会让 Cookie 静默不发送。configCookie 返回 0 只说明写入成功,不说明发送成功。

第二步,确认写入时机在页面发起登录态校验请求之前。onControllerAttached 之后、页面开始加载之前是安全窗口,onPageEnd 写入对首次请求无效。

第三步,如果是 H5 侧设置的 Cookie 原生侧读不到,确认调用了 fetchCookie 并且调用时机在 H5 写入之后,确认 saveCookieAsync 在应用退出之前执行了。H5 侧的 Cookie 在 Web 内核内存里,不主动读、不主动刷盘,原生侧看不到也留不住。

第四步,如果是跨站 iframe 场景,检查 SameSite 和 setThirdPartyCookieEnabled 的配置。DevTools 的 Network 面板里看请求的 Cookie 头,比看 document.cookie 更接近真实发送行为。

第五步,退出登录时确认按域精确删除并调用了 saveCookieAsync。删除内存不删磁盘,重启之后旧 Cookie 会回来。

Cookie 不是“设了就会带”,它是 domain、path、Secure、SameSite 四个条件共同决定的一条发送规则。configCookie 的成功是写入回执,不是发送承诺;H5 侧设置的 Cookie 原生侧不会自动知道,要主动 fetchCookie 和 saveCookieAsync;退出登录的删除要连磁盘一起清,否则重启之后旧登录态会复活。下一次 H5 页面问登录态而原生明明已经登录的时候,先别怀疑 H5 的鉴权逻辑——打开 DevTools 看一眼请求的 Cookie 头,那个 auth_token,到底有没有在 domain 的匹配范围里。

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐