Electron 内嵌页面跨域问题(CORS/PNA)解决全记录

Electron 内嵌页面跨域问题(CORS/PNA)解决全记录

场景:Electron 壳内嵌远端业务页面,页面需要调用本机 AI 服务,却同时撞上 CORS 和 PNA 两道墙。

本文结合一个真实项目,讲清楚问题本质、解决思路、核心代码,以及如何用日志验证链路。

一、背景:一个让人头疼的真实场景

我们有一个 Electron 桌面应用,它的主窗口里用 <webview> 内嵌了一个远端业务页面(内网站点 http://192.168.0.110:133)。

这个远端页面上有一个「智能识别」按钮,点击后需要调用跑在本机的 AI 服务 http://localhost:8090。

看起来是个很普通的请求,但一跑就报错,而且报错还不是一种:

text 复制代码
Access to fetch at 'http://localhost:8090/...' from origin 'http://192.168.0.110:133' has been blocked by CORS policy

// 或者
Private Network Access checks failed: the request client is not a secure context ...

更麻烦的是:AI 服务的后端是一个 C++ 写的二进制(Docker 容器),响应里没有 CORS 头,而且二进制没重编译,短期内改不动。

这就逼着我们只能从 Electron 壳这一侧想办法。

二、问题拆解:跨域其实有两层墙

很多人把这里的报错笼统叫「跨域问题」,但实际上有两道独立的墙,解决办法完全不同:

第一层:CORS 第二层:PNA
全称 Cross-Origin Resource Sharing Private Network Access
触发条件 跨源请求(origin 不同) 非安全上下文访问更私有的地址空间
我们的场景 133 页面 → 8090 http+私网IP 页面 → localhost
报错特征 blocked by CORS policy not a secure context / more-private
能否靠响应头解决 能(补 Access-Control-Allow-Origin) 不能(硬拦截,响应头无解)

#mermaid-svg-2fteuVp9KvOKz6BT{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-2fteuVp9KvOKz6BT .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-2fteuVp9KvOKz6BT .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-2fteuVp9KvOKz6BT .error-icon{fill:#552222;}#mermaid-svg-2fteuVp9KvOKz6BT .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-2fteuVp9KvOKz6BT .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-2fteuVp9KvOKz6BT .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-2fteuVp9KvOKz6BT .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-2fteuVp9KvOKz6BT .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-2fteuVp9KvOKz6BT .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-2fteuVp9KvOKz6BT .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-2fteuVp9KvOKz6BT .marker{fill:#333333;stroke:#333333;}#mermaid-svg-2fteuVp9KvOKz6BT .marker.cross{stroke:#333333;}#mermaid-svg-2fteuVp9KvOKz6BT svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-2fteuVp9KvOKz6BT p{margin:0;}#mermaid-svg-2fteuVp9KvOKz6BT .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-2fteuVp9KvOKz6BT .cluster-label text{fill:#333;}#mermaid-svg-2fteuVp9KvOKz6BT .cluster-label span{color:#333;}#mermaid-svg-2fteuVp9KvOKz6BT .cluster-label span p{background-color:transparent;}#mermaid-svg-2fteuVp9KvOKz6BT .label text,#mermaid-svg-2fteuVp9KvOKz6BT span{fill:#333;color:#333;}#mermaid-svg-2fteuVp9KvOKz6BT .node rect,#mermaid-svg-2fteuVp9KvOKz6BT .node circle,#mermaid-svg-2fteuVp9KvOKz6BT .node ellipse,#mermaid-svg-2fteuVp9KvOKz6BT .node polygon,#mermaid-svg-2fteuVp9KvOKz6BT .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-2fteuVp9KvOKz6BT .rough-node .label text,#mermaid-svg-2fteuVp9KvOKz6BT .node .label text,#mermaid-svg-2fteuVp9KvOKz6BT .image-shape .label,#mermaid-svg-2fteuVp9KvOKz6BT .icon-shape .label{text-anchor:middle;}#mermaid-svg-2fteuVp9KvOKz6BT .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-2fteuVp9KvOKz6BT .rough-node .label,#mermaid-svg-2fteuVp9KvOKz6BT .node .label,#mermaid-svg-2fteuVp9KvOKz6BT .image-shape .label,#mermaid-svg-2fteuVp9KvOKz6BT .icon-shape .label{text-align:center;}#mermaid-svg-2fteuVp9KvOKz6BT .node.clickable{cursor:pointer;}#mermaid-svg-2fteuVp9KvOKz6BT .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-2fteuVp9KvOKz6BT .arrowheadPath{fill:#333333;}#mermaid-svg-2fteuVp9KvOKz6BT .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-2fteuVp9KvOKz6BT .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-2fteuVp9KvOKz6BT .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-2fteuVp9KvOKz6BT .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-2fteuVp9KvOKz6BT .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-2fteuVp9KvOKz6BT .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-2fteuVp9KvOKz6BT .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-2fteuVp9KvOKz6BT .cluster text{fill:#333;}#mermaid-svg-2fteuVp9KvOKz6BT .cluster span{color:#333;}#mermaid-svg-2fteuVp9KvOKz6BT div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-2fteuVp9KvOKz6BT .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-2fteuVp9KvOKz6BT rect.text{fill:none;stroke-width:0;}#mermaid-svg-2fteuVp9KvOKz6BT .icon-shape,#mermaid-svg-2fteuVp9KvOKz6BT .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-2fteuVp9KvOKz6BT .icon-shape p,#mermaid-svg-2fteuVp9KvOKz6BT .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-2fteuVp9KvOKz6BT .icon-shape .label rect,#mermaid-svg-2fteuVp9KvOKz6BT .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-2fteuVp9KvOKz6BT .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-2fteuVp9KvOKz6BT .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-2fteuVp9KvOKz6BT :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 请求本机 localhost:8090
第一层
第二层
响应缺 CORS 头
非安全上下文 → localhost
远端页面

http://192.168.0.110:133

http + 私网IP = 非安全上下文
浏览器两层检查
CORS 跨源检查
PNA 私有网络访问检查
拦截:blocked by CORS policy
硬拦截:not a secure context

关键认知:

  • CORS 是「服务端配合」的问题------响应里带上 Access-Control-Allow-Origin 就能解;
  • PNA 是「客户端上下文」的问题------发起方必须是安全上下文,响应头怎么加都没用。

这也是为什么这个项目最后要两套手段并用。

三、为什么常规方案都行不通

逐个排除:

  1. 改后端加 CORS 头 ------ 后端是 C++ 二进制,重编译成本高,不可行。
  2. 改远端页面 ------ 跨域是浏览器行为,页面侧无法给自己加 CORS 头,改不动。
  3. JSONP / 后端中转 ------ 大改,且 POST 接口不适用。

那只剩下一条路:利用 Electron 主进程的能力,在壳这一层把问题消化掉。

Electron 主进程恰好有两样「纯浏览器做不到」的能力:

  • 能自己起一个 HTTP 服务(Node.js 能力);
  • 能挂 webRequest 钩子改写任意请求/响应头(Chromium 网络层能力)。

整个方案就是这两样能力的组合。

四、技术思路:两套手段,各解一层墙

思路 1:本地反向代理,让页面 origin 变成安全上下文(解 PNA)

PNA 的硬条件是「发起方必须安全上下文」。而 http://192.168.0.110:133 这种「http + 私网 IP」天然不是。

但有一个例外:127.0.0.1 / localhost 这类本机回环地址,Chrome 默认视为可信源(potentially trustworthy origin)。

于是我们在主进程起一个本地反向代理,监听 127.0.0.1:9123,让 <webview> 从 http://127.0.0.1:9123 加载页面。

效果:页面 origin 从「http + 私网IP」变成「127.0.0.1 安全上下文」,此时再请求 localhost:8090,属于 local → local,PNA 硬拦截消失。

思路 2:webRequest 补响应头(解 CORS)

PNA 解决后,跨源还在(127.0.0.1:9123 与 localhost:8090 是不同 origin)。

后端不改,就由主进程挂 webRequest.onHeadersReceived 钩子,在响应返回给页面的路上,把 CORS 头补上。

这等效于「不改后端,在壳的网络层给后端补了 CORS 头」。

思路 3(备选):命令行开关

js 复制代码
// 仅内部排查用,不建议随生产发布
// app.commandLine.appendSwitch('unsafely-treat-insecure-origin-as-secure', 'http://192.168.0.110:133')
// app.commandLine.appendSwitch('disable-features', 'BlockInsecurePrivateNetworkRequests')

暴力绕过 PNA,但属于「关闭浏览器安全特性」,只作排查,不用于生产。

五、整体架构

#mermaid-svg-aFa4Z1CFQ5JsGwrW{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-aFa4Z1CFQ5JsGwrW .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-aFa4Z1CFQ5JsGwrW .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-aFa4Z1CFQ5JsGwrW .error-icon{fill:#552222;}#mermaid-svg-aFa4Z1CFQ5JsGwrW .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-aFa4Z1CFQ5JsGwrW .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-aFa4Z1CFQ5JsGwrW .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-aFa4Z1CFQ5JsGwrW .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-aFa4Z1CFQ5JsGwrW .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-aFa4Z1CFQ5JsGwrW .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-aFa4Z1CFQ5JsGwrW .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-aFa4Z1CFQ5JsGwrW .marker{fill:#333333;stroke:#333333;}#mermaid-svg-aFa4Z1CFQ5JsGwrW .marker.cross{stroke:#333333;}#mermaid-svg-aFa4Z1CFQ5JsGwrW svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-aFa4Z1CFQ5JsGwrW p{margin:0;}#mermaid-svg-aFa4Z1CFQ5JsGwrW .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-aFa4Z1CFQ5JsGwrW .cluster-label text{fill:#333;}#mermaid-svg-aFa4Z1CFQ5JsGwrW .cluster-label span{color:#333;}#mermaid-svg-aFa4Z1CFQ5JsGwrW .cluster-label span p{background-color:transparent;}#mermaid-svg-aFa4Z1CFQ5JsGwrW .label text,#mermaid-svg-aFa4Z1CFQ5JsGwrW span{fill:#333;color:#333;}#mermaid-svg-aFa4Z1CFQ5JsGwrW .node rect,#mermaid-svg-aFa4Z1CFQ5JsGwrW .node circle,#mermaid-svg-aFa4Z1CFQ5JsGwrW .node ellipse,#mermaid-svg-aFa4Z1CFQ5JsGwrW .node polygon,#mermaid-svg-aFa4Z1CFQ5JsGwrW .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-aFa4Z1CFQ5JsGwrW .rough-node .label text,#mermaid-svg-aFa4Z1CFQ5JsGwrW .node .label text,#mermaid-svg-aFa4Z1CFQ5JsGwrW .image-shape .label,#mermaid-svg-aFa4Z1CFQ5JsGwrW .icon-shape .label{text-anchor:middle;}#mermaid-svg-aFa4Z1CFQ5JsGwrW .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-aFa4Z1CFQ5JsGwrW .rough-node .label,#mermaid-svg-aFa4Z1CFQ5JsGwrW .node .label,#mermaid-svg-aFa4Z1CFQ5JsGwrW .image-shape .label,#mermaid-svg-aFa4Z1CFQ5JsGwrW .icon-shape .label{text-align:center;}#mermaid-svg-aFa4Z1CFQ5JsGwrW .node.clickable{cursor:pointer;}#mermaid-svg-aFa4Z1CFQ5JsGwrW .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-aFa4Z1CFQ5JsGwrW .arrowheadPath{fill:#333333;}#mermaid-svg-aFa4Z1CFQ5JsGwrW .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-aFa4Z1CFQ5JsGwrW .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-aFa4Z1CFQ5JsGwrW .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-aFa4Z1CFQ5JsGwrW .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-aFa4Z1CFQ5JsGwrW .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-aFa4Z1CFQ5JsGwrW .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-aFa4Z1CFQ5JsGwrW .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-aFa4Z1CFQ5JsGwrW .cluster text{fill:#333;}#mermaid-svg-aFa4Z1CFQ5JsGwrW .cluster span{color:#333;}#mermaid-svg-aFa4Z1CFQ5JsGwrW div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-aFa4Z1CFQ5JsGwrW .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-aFa4Z1CFQ5JsGwrW rect.text{fill:none;stroke-width:0;}#mermaid-svg-aFa4Z1CFQ5JsGwrW .icon-shape,#mermaid-svg-aFa4Z1CFQ5JsGwrW .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-aFa4Z1CFQ5JsGwrW .icon-shape p,#mermaid-svg-aFa4Z1CFQ5JsGwrW .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-aFa4Z1CFQ5JsGwrW .icon-shape .label rect,#mermaid-svg-aFa4Z1CFQ5JsGwrW .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-aFa4Z1CFQ5JsGwrW .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-aFa4Z1CFQ5JsGwrW .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-aFa4Z1CFQ5JsGwrW :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 主进程
渲染进程
① 页面加载 /local/* 转发
其余路径原样透传
/local/* 转发到 8090(当前闲置)
② 直连 localhost:8090
补 CORS 头后到达
webview 页面

origin: 127.0.0.1:9123
startProxy

node:http 反向代理

监听 127.0.0.1:9123
webRequest.onHeadersReceived

补 CORS 头
远端站点

192.168.0.110:133
本机 AI 服务

127.0.0.1:8090

注意两条链路:

  • ① 页面加载 :走 9123 代理,把远端站点镜像到 127.0.0.1:9123 origin 下;
  • ② 接口请求 :页面直连 localhost:8090,由 webRequest 补头。

六、核心代码实现

6.1 本地反向代理(解 PNA 的关键)

src/main/index.js 的 startProxy():

js 复制代码
function startProxy() {
  const ROUTES = [
    {
      prefix: '/local/',
      host: '127.0.0.1',
      port: 8090,
      rewrite: (p) => '/api/' + p.slice('/local/'.length)
    }
  ]
  const STATIC = { host: '192.168.0.110', port: 133 }
  const PORT = 9123

  const proxy = http.createServer((req, res) => {
    const route = ROUTES.find((r) => req.url.startsWith(r.prefix))
    const target = route || STATIC
    const targetPath = route && route.rewrite ? route.rewrite(req.url) : req.url

    const upstream = http.request(
      {
        hostname: target.host,
        port: target.port,
        path: targetPath,
        method: req.method,
        headers: { ...req.headers, host: `${target.host}:${target.port}` }
      },
      (upRes) => {
        const headers = { ...upRes.headers }
        if (headers['set-cookie']) {
          headers['set-cookie'] = headers['set-cookie'].map((c) => c.replace(/;\s*Domain=[^;]*/gi, ''))
        }
        res.writeHead(upRes.statusCode || 502, headers)
        upRes.pipe(res)
      }
    )
    upstream.on('error', (e) => {
      if (!res.headersSent) res.writeHead(502)
      res.end('proxy error: ' + e.message)
    })
    req.pipe(upstream)
  })

  proxy.listen(PORT, '127.0.0.1', () => {})
}

路由规则:

请求路径 命中 转发目标 路径处理
/local/* ROUTES0 127.0.0.1:8090 /local/x → /api/x
其余 STATIC 192.168.0.110:133 原样透传

6.2 页面从 9123 加载

src/renderer/src/views/IframeView.vue:

js 复制代码
const pageUrl = 'http://127.0.0.1:9123'
vue 复制代码
<!-- 经本地代理 9123 打开:页面 origin 变为 http://127.0.0.1:9123(安全上下文),
     向 localhost:8090 发请求为 local -> local,无 PNA 硬拦截 -->
<webview ref="frameRef" class="frame" :src="pageUrl"></webview>

6.3 webRequest 补 CORS 头(解 CORS 的关键)

src/main/index.js:

js 复制代码
session.defaultSession.webRequest.onHeadersReceived((details, callback) => {
  if (details.url.startsWith('http://localhost:8090')) {
    callback({
      responseHeaders: {
        ...details.responseHeaders,
        'Access-Control-Allow-Origin': ['*'],
        'Access-Control-Allow-Methods': ['GET, POST, OPTIONS'],
        'Access-Control-Allow-Headers': ['Content-Type, Authorization, X-Requested-With']
      }
    })
  } else {
    callback({})
  }
})

七、真实请求链路(含日志验证)

这里要澄清一个容易踩的坑:智能识别接口走的是「直连 + 补头」,并不是走 /local/ 转发代理。

/local/ 那套转发代码在代理里是存在的,但实际接口请求没有用它(日志可证)。

完整时序:
本机 8090 AI 服务 Chromium 网络层(webRequest 钩子) 远端 133 9123 代理 webview 页面(origin: 127.0.0.1:9123) 本机 8090 AI 服务 Chromium 网络层(webRequest 钩子) 远端 133 9123 代理 webview 页面(origin: 127.0.0.1:9123) #mermaid-svg-90T1t7fJgedeIWGq{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-90T1t7fJgedeIWGq .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-90T1t7fJgedeIWGq .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-90T1t7fJgedeIWGq .error-icon{fill:#552222;}#mermaid-svg-90T1t7fJgedeIWGq .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-90T1t7fJgedeIWGq .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-90T1t7fJgedeIWGq .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-90T1t7fJgedeIWGq .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-90T1t7fJgedeIWGq .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-90T1t7fJgedeIWGq .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-90T1t7fJgedeIWGq .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-90T1t7fJgedeIWGq .marker{fill:#333333;stroke:#333333;}#mermaid-svg-90T1t7fJgedeIWGq .marker.cross{stroke:#333333;}#mermaid-svg-90T1t7fJgedeIWGq svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-90T1t7fJgedeIWGq p{margin:0;}#mermaid-svg-90T1t7fJgedeIWGq .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-90T1t7fJgedeIWGq text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-90T1t7fJgedeIWGq .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-90T1t7fJgedeIWGq .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-90T1t7fJgedeIWGq .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-90T1t7fJgedeIWGq .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-90T1t7fJgedeIWGq #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-90T1t7fJgedeIWGq .sequenceNumber{fill:white;}#mermaid-svg-90T1t7fJgedeIWGq #sequencenumber{fill:#333;}#mermaid-svg-90T1t7fJgedeIWGq #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-90T1t7fJgedeIWGq .messageText{fill:#333;stroke:none;}#mermaid-svg-90T1t7fJgedeIWGq .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-90T1t7fJgedeIWGq .labelText,#mermaid-svg-90T1t7fJgedeIWGq .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-90T1t7fJgedeIWGq .loopText,#mermaid-svg-90T1t7fJgedeIWGq .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-90T1t7fJgedeIWGq .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-90T1t7fJgedeIWGq .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-90T1t7fJgedeIWGq .noteText,#mermaid-svg-90T1t7fJgedeIWGq .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-90T1t7fJgedeIWGq .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-90T1t7fJgedeIWGq .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-90T1t7fJgedeIWGq .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-90T1t7fJgedeIWGq .actorPopupMenu{position:absolute;}#mermaid-svg-90T1t7fJgedeIWGq .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-90T1t7fJgedeIWGq .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-90T1t7fJgedeIWGq .actor-man circle,#mermaid-svg-90T1t7fJgedeIWGq line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-90T1t7fJgedeIWGq :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} onHeadersReceived注入 Access-Control-Allow-Origin: * CORS 校验通过,拿到结果 ① GET / 加载页面命中 STATIC,原样转发HTML + JS页面渲染,origin 变成 127.0.0.1:9123② fetch('http://localhost:8090/api/...')转发跨域请求200 响应(无 CORS 头)200 响应(已补 CORS 头)

运行时日志 _ai_req_log.txt 实测:

text 复制代码
POST http://localhost:8090/api/ai_server/start/traffic_detector (type=xhr, wcId=2)
DONE 200 POST http://localhost:8090/api/ai_server/start/traffic_detector
POST http://localhost:8090/api/ai_server/traffic_detector/reco (type=xhr, wcId=2)
DONE 200 POST http://localhost:8090/api/ai_server/traffic_detector/reco

日志全文检索结果:8090 命中 30 条,localhost:8090 命中 25 条,9123 和 /local/ 都是 0 命中。

这证明接口请求全部是直连 localhost:8090 ,没有一次经过 /local/ 转发。

八、如何用日志判断走了哪条链路

主进程还挂了 onBeforeRequest / onCompleted / onErrorOccurred 记录请求,是很好的验证工具:

日志现象 结论
有 localhost:8090 记录且 DONE 200 直连 + 补头(当前真实路径)
请求成功但日志无记录 走了 /local/ 转发(node:http 不经 webRequest,URL 也不含 8090)
有 NET-ERROR 直连失败,查 PNA/CORS 报错原文
wcId=2 webview 内页面发起;wcId=1 为壳页面

九、总结与踩坑点

一句话总结

PNA 靠「页面从 127.0.0.1 安全上下文加载」解,CORS 靠「webRequest 补响应头」解,两者是不同层、不同手段,必须并用。

踩坑点

  1. 不要把两层墙混为一谈 。报 CORS policy 和报 not a secure context 是两回事,后者加 CORS 头没用。
  2. Access-Control-Allow-Origin: * 不带凭据 。如果接口将来要 cookie/认证,要改成具体 origin 并加 Access-Control-Allow-Credentials: true。
  3. 代理转发的请求不经过 webRequest 。主进程 node:http 发起的请求是纯后端调用,不在 webRequest 拦截范围,所以日志里看不到它。想验证代理链路,得给代理自己加日志。
  4. /local/ 前缀 + rewrite 有重复前缀风险 。当前 rewrite 是 '/api/' + 去掉/local/ 后的部分,如果页面请求 /local/api/... 会变成 /api/api/...。
  5. 命令行开关是最后手段 。disable-features 会关闭整个 PNA 保护,只适合排查,别带进生产。
相关推荐
asaotomo2 小时前
截图识题、AI 讲解、笔记沉淀:Chrome 扩展体验
前端·chrome·笔记
前端架构师-老李2 小时前
HTML与CSS基础第五课—HTML的空格、换行、横线相关知识
前端·css·html
SEO_juper2 小时前
Chrome 扩展一键查看当前页面 SEO 信息:不用切页面,点一下全出来
前端·javascript·chrome
Highcharts.js2 小时前
CSUN 2026|Highcharts无障碍数据与AI分享
javascript·人工智能·数据分析·highcharts·图表开发
Nymph_Zhu2 小时前
vue3+element-plus 文件上传
前端·typescript·vue3·element-plus
echome8882 小时前
CSS 布局:Flex 和 Grid 怎么选,附常见坑
前端·css·html
计算机魔术师2 小时前
科技巨头砸7400亿押注AI,为什么苹果成了唯一赢家?
前端
系统集成-鹰翔2 小时前
ERP推单数据库怎么备份:SQLite WAL模式下只复制db文件为什么可能漏任务
javascript·数据库·经验分享·后端
QianhangQianping2 小时前
撤销重做不是「存快照」:一个可视化编辑器的Operation 设计
java·前端·数据库·typescript·编辑器·前端架构·撤销重做