You need to enable JavaScript to run this app.
最新活动
大模型
产品
解决方案
定价
生态与合作
支持与服务
开发者
了解我们

React Router v7 SPA模式下如何配置严格的内容安全策略(CSP)?

React Router v7 SPA模式下如何配置严格的内容安全策略(CSP)?

问题背景

我最近在把一个基于React Router v6 + Webpack的应用迁移到React Router v7(SPA模式)+ Vite,卡在了严格内容安全策略(CSP)的配置上。

在RR6时期,我用的是常规的index.html,通过外部引入bundle:

<script src="/js/admin-panel-bundle.js"></script>

当时的CSP配置完全没问题:

<meta http-equiv="Content-Security-Policy" content="default-src 'self'; style-src 'self' 'unsafe-inline';">

但到了RR7的SPA模式,要求必须用根Layout组件生成完整的HTML文档,React Router会通过<Scripts />组件自动插入一个内联的<script type="module">脚本。我的Layout代码大概是这样:

export const Layout = ({ children }: { children: React.ReactNode }) => {
  return (
    <html lang='en'>
      <head>
        <meta charSet='UTF-8' />
        <meta httpEquiv='X-UA-Compatible' content='IE=edge' />
        {/* 现在没法直接用这个了,会完全阻断代码执行 */}
        {/* <meta httpEquiv='Content-Security-Policy' content="default-src 'self'; style-src 'self' 'unsafe-inline';" /> */}
      </head>
      <body>
        {children}
        <Scripts />
      </body>
    </html>
  );
};

现在应用里必然存在内联JS,直接导致应用无法加载,控制台全是CSP错误。

我尝试过的方案

我试着切换到基于nonce的CSP,先硬编码了一个nonce值做实验:

<meta http-equiv="Content-Security-Policy" content="default-src 'self'; style-src 'self' 'unsafe-inline'; script-src 'self' 'nonce-random';">

然后给<Scripts />组件加上匹配的nonce:

<Scripts nonce="random" />

但浏览器还是会报错阻断执行:

Executing inline script violates the Content Security Policy directive 'script-src 'self' 'nonce-random''.

核心疑问

想请教大家:在RR7 SPA模式下,是否有可能配置不包含'unsafe-inline'的严格CSP?毕竟<Scripts />组件总会生成内联ES模块脚本。

具体想知道:

  • RR7 + SPA模式 + 严格CSP有没有官方推荐的配置模式?
  • 是不是SPA模式下必须要开'unsafe-inline'才能正常运行?
    我在RR7的文档里没找到明确的指导,而且单独用nonce似乎也不起作用,求可行的解决方案或最佳实践!

解决方案

一、为什么你的nonce方案没生效?

你踩了两个常见坑:

  1. nonce格式要求:CSP规范要求nonce必须是随机生成的base64编码字符串,硬编码的固定值很多浏览器会直接忽略(不符合安全预期);
  2. 时机一致性问题:当浏览器解析到CSP meta标签时,后续渲染的<Scripts />内联脚本需要和CSP中的nonce完全同步,硬编码场景下容易出现匹配偏差。

二、正确的Nonce配置方案(推荐)

最佳实践是动态生成随机nonce,并确保CSP标签和<Scripts />组件的nonce完全一致。

1. 用Vite插件动态注入Nonce

在Vite的构建阶段生成随机nonce,注入到CSP标签和Layout组件中:

// vite.config.ts
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import crypto from 'crypto';

export default defineConfig({
  plugins: [
    react(),
    {
      name: 'csp-nonce-injector',
      transformIndexHtml(html) {
        // 生成符合要求的base64随机nonce
        const nonce = crypto.randomBytes(16).toString('base64');
        // 替换CSP meta标签和Scripts组件的nonce
        return html
          .replace(
            /<meta http-equiv="Content-Security-Policy".*?>/,
            `<meta http-equiv="Content-Security-Policy" content="default-src 'self'; style-src 'self' 'unsafe-inline'; script-src 'self' 'nonce-${nonce}';">`
          )
          .replace(
            /<Scripts.*?>/,
            `<Scripts nonce="${nonce}" />`
          );
      },
    },
  ],
});

2. 在React Layout中同步Nonce

如果你的Layout是纯React组件,也可以通过Vite的环境变量传递nonce:

// Layout.tsx
export const Layout = ({ children }: { children: React.ReactNode }) => {
  // 从Vite注入的环境变量获取nonce
  const nonce = import.meta.env.VITE_CSP_NONCE;
  return (
    <html lang='en'>
      <head>
        <meta charSet='UTF-8' />
        <meta httpEquiv='X-UA-Compatible' content='IE=edge' />
        <meta 
          httpEquiv='Content-Security-Policy' 
          content={`default-src 'self'; style-src 'self' 'unsafe-inline'; script-src 'self' 'nonce-${nonce}';"`} 
        />
      </head>
      <body>
        {children}
        <Scripts nonce={nonce} />
      </body>
    </html>
  );
};

然后在Vite插件中把nonce注入到环境变量:

// vite.config.ts 插件部分
transformIndexHtml(html) {
  const nonce = crypto.randomBytes(16).toString('base64');
  // 把nonce注入到全局环境变量
  return html.replace(
    /<\/head>/,
    `<script>import.meta.env.VITE_CSP_NONCE = "${nonce}";</script></head>`
  );
}

三、备选方案:哈希式CSP

如果没法动态生成nonce,也可以用哈希方案:

  1. 先把RR7的<Scripts />组件生成的内联模块脚本内容复制出来;
  2. 用SHA-256算法计算其哈希值(转成base64格式);
  3. 把哈希值加到CSP的script-src中:
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; style-src 'self' 'unsafe-inline'; script-src 'self' 'sha256-abcdef123456...';">

⚠️ 注意:这个方案维护成本高,只要RR7版本更新或<Scripts />生成的代码变化,就需要重新计算哈希。

四、关键说明:关于unsafe-inline

在RR7 SPA模式下,完全避免内联脚本是不可能的,但用nonce/哈希方案后,你不需要加'unsafe-inline'——因为CSP规范规定,当指定了nonce-*sha256-*时,'unsafe-inline'会被自动忽略(除非你明确添加),依然符合严格CSP的要求。

五、额外注意事项

  1. 开发环境适配:开发环境下Vite会有大量热更新的内联脚本,建议只在生产环境启用严格CSP,开发环境可以放松限制;
  2. 模块脚本覆盖script-src已经覆盖了模块脚本,若想更明确可以加上script-src-elem 'nonce-${nonce}'
  3. 样式的unsafe-inline:React的CSS-in-JS或第三方库常使用内联样式,style-src 'self' 'unsafe-inline'通常是必须的,除非所有样式都用外部文件。

总结

RR7 SPA模式下完全可以配置严格CSP(无需'unsafe-inline'),动态生成nonce是最佳实践,核心是保证CSP标签和<Scripts />组件的nonce完全一致。哈希方案可作为临时替代,但维护成本高。

如果还是有问题,优先检查:

  1. Nonce是否为base64编码的随机字符串;
  2. CSP中的nonce和<Scripts />的nonce是否完全匹配;
  3. 动态注入逻辑是否在渲染前执行。

火山引擎 最新活动