在 React / Vue 里安全插入动态脚本:一文读懂 nonce 的正确用法

📅 发布时间:2026/7/13 3:00:57 👁️ 浏览次数:
在 React / Vue 里安全插入动态脚本:一文读懂 nonce 的正确用法
在前端项目里动态插入脚本非常常见埋点 SDK、第三方分析脚本、A/B 实验、广告脚本……很多同学顺手就是一段document.createElement(script)然后appendChild功能上没问题但在安全视角下这样做往往会被现代安全策略直接“打死”。这篇文章想聊的就是在开启 CSPContent Security Policy内容安全策略时如何在 React / Vue 这样的框架中用nonce的方式安全、合规地“动态生成脚本”既满足安全要求又不牺牲业务灵活性。一、为什么需要 nonce先把 CSP 讲清楚在没有 CSP 的年代前端对 XSS 的防御基本靠“自觉”自己做输入输出转义不乱用innerHTML不随便执行字符串脚本等等后来大家发现仅靠“自觉”远远不够于是有了 CSP——让浏览器帮你“管得更严”。简单理解 CSP服务器通过响应头Content-Security-Policy告诉浏览器“哪些资源可以加载、哪些脚本可以执行”。比如最典型的一条Content-Security-Policy: script-src self;意思是只允许加载来自同源的脚本其他域名一律禁止。为了防止 XSS很多团队会直接禁掉所有“内联脚本”Content-Security-Policy: script-src self; # 没有 unsafe-inline这时候问题来了HTML 里scriptconsole.log(1)/script会被拦截JS 里用eval(...)、new Function(...)执行字符串也会被拦截甚至你在运行时动态创建script如果 CSP 没配置好也有可能被拦这就对「动态生成脚本」提出了更高的要求——不能再随便写了。nonce 的作用就是在 CSP 很严格的情况下给“特定脚本”一个一次性的通行证。二、nonce 是什么从浏览器视角看一眼nonce 是 script 标签上的一个属性scriptnonce随机字符串// 内联脚本内容/script配合 CSP 头使用Content-Security-Policy: script-src self nonce-随机字符串;浏览器会这样判断如果一个脚本标签带有nonce随机字符串且 CSP 中允许nonce-随机字符串就可以执行。没有nonce的内联脚本依旧被禁。这个“随机字符串”应该是高熵、每次请求都不同防重放。这带来两个非常重要的安全特性攻击者即使能插入一段scriptalert(1)/script也拿不到正确的 nonce脚本不会执行。后端每次响应生成不同 nonce攻击者无法“复用”已知 nonce。到这里我们可以得出一个核心结论在 CSP 环境下想动态生成脚本就必须想办法给它加上正确的 nonce让浏览器认出“这是自家人”。三、在 React / Vue 中nonce 一般从哪儿来由于 CSP 头是服务器下发的而 nonce 又是 CSP 中的一部分所以nonce 的生成通常在服务端完成然后以某种方式“塞”到前端应用里。常见的传递方式有几种你可以视项目情况选一种或组合写在服务器渲染的script标签上scriptidapp-entrysrc/static/app.jsnonceabc123/script前端可以通过 DOM 查到这个 nonce。写在 meta / 自定义标签上metanamecsp-noncecontentabc123/或者dividcsp-nonce-holderdata-nonceabc123/div写进首屏内联变量scriptnonceabc123window.__CSP_NONCE__abc123;/script只要有一个地方能拿到这个 nonce后续在 React / Vue 中你就能用它给动态脚本“盖章”。下面我们分框架看具体写法。四、在 React 中使用 nonce 动态生成脚本4.1 获取 nonce 的推荐方式假设我们的index.html中这样写以 CRA 或 Vite 模板为例!DOCTYPEhtmlhtmllangzh-CNheadmetacharsetUTF-8/metanamecsp-noncecontentabc123/titleReact App/title/headbodydividroot/divscriptsrc/src/main.tsxnonceabc123/script/body/html然后在 React 应用里写一个小工具函数统一读取 nonce// src/utils/csp.tsexportfunctiongetCspNonce():string|null{if(typeofdocumentundefined)returnnull;constmetadocument.querySelector(meta[namecsp-nonce]);returnmeta?.getAttribute(content)||null;}这样就可以在任意 React 组件 / 业务模块里使用getCspNonce()了。4.2 用原生 DOM 动态插入脚本并绑定 nonce这是最通用、最不依赖框架的方式import{getCspNonce}from./utils/csp;exportfunctionloadAnalytics(){constnoncegetCspNonce();constscriptdocument.createElement(script);script.srchttps://example.com/analytics.js;script.asynctrue;if(nonce){script.noncenonce;// 核心绑定同一个 nonce}document.head.appendChild(script);}在 React 组件中调用它import { useEffect } from react; import { loadAnalytics } from ./analytics; export function App() { useEffect(() { loadAnalytics(); }, []); return divHello React with CSP nonce/div; }要点尽量把“动态插入脚本”的逻辑从组件中抽出去写在独立模块如analytics.ts里方便复用和测试。记得在useEffect中调用而不是在组件 render 函数体里直接操作 DOM。4.3 用 React JSX 直接生成script较少用但可以React DOM 对nonce是支持的你可以在 JSX 里直接这么写const nonce getCspNonce(); return ( divSome content/div {nonce ( script nonce{nonce} dangerouslySetInnerHTML{{ __html: window.__APP_CONFIG__ { env: prod }; , }} / )} / );但有几点需要明确使用dangerouslySetInnerHTML本身就有安全风险如果可以尽量避免拼接用户可控内容。对于加载外部脚本更推荐前面那种document.createElement(script)方式。五、在 Vue 中使用 nonce 动态生成脚本Vue 的思路与 React 完全一致先拿到 nonce再通过原生 DOM 动态插入脚本。5.1 在 Vue 里读取 nonce假设你同样在 HTML 模板里配置了 metametanamecsp-noncecontentabc123/可以在一个工具文件中封装// src/utils/csp.tsexportfunctiongetCspNonce():string|null{if(typeofdocumentundefined)returnnull;constmetadocument.querySelector(meta[namecsp-nonce]);returnmeta?.getAttribute(content)||null;}5.2 在 Vue 组件中动态插入脚本以 Vue 3 script setup为例script setup langts import { onMounted } from vue; import { getCspNonce } from /utils/csp; function loadAnalytics() { const nonce getCspNonce(); const script document.createElement(script); script.src https://example.com/analytics.js; script.async true; if (nonce) { script.nonce nonce; } document.head.appendChild(script); } onMounted(() { loadAnalytics(); }); /script template divVue app with CSP nonce/div /template如果你是 Options APIimport{getCspNonce}from/utils/csp;exportdefault{name:App,mounted(){constnoncegetCspNonce();constscriptdocument.createElement(script);script.srchttps://example.com/analytics.js;script.asynctrue;if(nonce){script.noncenonce;}document.head.appendChild(script);},};5.3 用指令封装可选的语法糖如果你项目里动态脚本很多可以做一个指令直接在模板上用// src/directives/script.tsimporttype{DirectiveBinding}fromvue;import{getCspNonce}from/utils/csp;interfaceScriptOptions{src:string;async?:boolean;defer?:boolean;}exportconstvScript:DirectiveBindingScriptOptions[mounted](el,binding){const{src,asynctrue,deferfalse}binding.value||{};if(!src)return;constnoncegetCspNonce();constscriptdocument.createElement(script);script.srcsrc;script.asyncasync;script.deferdefer;if(nonce){script.noncenonce;}document.head.appendChild(script);};注册到应用上后template div v-script{ src: https://example.com/analytics.js } Vue app /div /template这只是语法糖本质还是动态插入脚本 赋值 nonce。六、和 CSP 配置配合别忘了服务端那一半前端把 nonce 做对了只是完成了一半服务端的 CSP 头必须正确配置两边的 nonce 才能对得上。一个常见的配置方式以 Node / Express 为例app.use((req,res,next){// 1. 生成高熵 nonce一般使用 crypto 随机constnoncecrypto.randomBytes(16).toString(base64);// 2. 挂在 res.locals 上供模板引擎 / SSR 使用res.locals.cspNoncenonce;// 3. 设置 CSP 头允许这个 nonce 的脚本执行res.setHeader(Content-Security-Policy,[default-src self,script-src self nonce-${nonce} strict-dynamic https://example.com,].join(; ));next();});在 HTML 模板里输出metanamecsp-noncecontent{{cspNonce}}/scriptsrc/static/app.jsnonce{{cspNonce}}/script几个实践要点nonce 必须每个响应唯一至少是每个页面请求唯一。CSP 中对应的是nonce-值注意前缀nonce-是标准格式的一部分。如果你用了strict-dynamic就算 script 标签是通过可信脚本动态插入的只要带上 nonce子脚本也能被信任。七、常见坑与注意事项7.1 忘记给动态脚本加 nonce现象本地开发一切 OK线上开启 CSP 后动态脚本全挂。控制台出现类似错误Refused to load the script because it violates the following Content Security Policy directive: “script-src ‘self’ ‘nonce-***’ …”.排查步骤看 Network / Headers 里 CSP 的具体配置。看生成的 script 标签上是否真的有 nonce。确认 nonce 值和 CSP 中nonce-xxx的内容一致。7.2 不要把 nonce 当作“固定秘钥”错误示例// 千万别这样constnoncemy-secret-nonce;nonce 不是 API Key它不需要“记住”。nonce 应该是短期的一次性令牌每次响应随机生成而且不依赖“保密性”来保证安全而是依赖“不可预测性”和“短生命周期”。7.3 SSR / 同构应用要考虑“首屏 运行时”一致如果你在使用 Next.js / Nuxt 等 SSR 框架情况会稍微复杂一点首屏 HTMLSSR 输出要带上正确的 nonce。前端打包好的 JS 在“水合”后继续运行时如果还会动态插入脚本也要用同一个 nonce。通常做法是在 SSR 时把 nonce 放到某个全局变量例如window.__CSP_NONCE__或 meta前端 hydrate 后从那里读取。不要在客户端自己“再随机一个 nonce”——那样浏览器会认为是另外一个“人”CSP 头并不认识。7.4 避免把用户可控内容拼到dangerouslySetInnerHTML里即使加了 nonce也不要松懈nonce 只能防止“外部注入的 script 标签执行”但如果你自己在dangerouslySetInnerHTML中拼接了用户输入那仍然可能让恶意脚本“走后门”进入你的逻辑。原则很简单CSP 是最后一道防线不是替代代码健壮性的借口。总结在启用 CSP 的现代前端项目中“随意动态插入脚本”的时代已经过去。nonce 的本质是浏览器根据 CSP 头中的nonce-xxx只信任带有相同 nonce 的脚本。在 React / Vue 中安全地“动态生成脚本”关键有两步从服务端传递 nonce 到前端meta />