Angular SSR+Express+Helmet环境下Nonce配置解决GA内联脚本CSP问题
解决Angular SSR + Express + Helmet下Google Analytics内联脚本的CSP问题(Nonce方案)
步骤1:在Express服务端生成并传递Nonce
在Express中间件中生成随机Nonce,注入响应上下文,并配置Helmet的CSP策略关联该Nonce:
import * as express from 'express'; import * as helmet from 'helmet'; import { randomBytes } from 'crypto'; import { renderModule } from '@angular/platform-server'; import { AppServerModule } from './src/main.server'; import { APP_BASE_HREF } from '@angular/common'; import { readFileSync, existsSync } from 'fs'; import { join } from 'path'; const app = express(); const distFolder = join(process.cwd(), 'dist/your-app-name/browser'); const indexHtml = existsSync(join(distFolder, 'index.original.html')) ? readFileSync(join(distFolder, 'index.original.html'), 'utf-8') : readFileSync(join(distFolder, 'index.html'), 'utf-8'); // 生成唯一Nonce的中间件(每次请求生成新值) app.use((req, res, next) => { // 生成16字节随机数转base64,符合CSP规范 res.locals.nonce = randomBytes(16).toString('base64'); next(); }); // 配置Helmet的CSP策略 app.use( helmet.contentSecurityPolicy({ directives: { defaultSrc: ["'self'"], // 脚本源:允许带当前请求Nonce的内联脚本、GA外部脚本域名 scriptSrc: [ (req, res) => `'nonce-${res.locals.nonce}'`, 'https://www.googletagmanager.com', 'https://www.google-analytics.com' ], // 样式源:兼容Angular SSR的内联样式需求 styleSrc: ["'self'", "'unsafe-inline'"], // 图片源:允许GA统计图片加载 imgSrc: ["'self'", "data:", "https://www.google-analytics.com"], // 连接源:允许GA的API请求 connectSrc: ["'self'", "https://www.google-analytics.com"] } }) ); // Angular SSR渲染路由 app.get('*', (req, res) => { const nonce = res.locals.nonce; renderModule(AppServerModule, { document: indexHtml, url: req.url, extraProviders: [ { provide: APP_BASE_HREF, useValue: req.baseUrl }, { provide: 'NONCE', useValue: nonce } ] }).then(html => { // 替换模板中的Nonce占位符 const finalHtml = html.replace('{{ nonce }}', nonce); res.send(finalHtml); }); }); app.listen(3000, () => { console.log('Server running on http://localhost:3000'); });
步骤2:修改Angular的index.html模板
为Google Analytics的内联脚本添加nonce属性,使用占位符等待服务端渲染时替换:
<!-- Google Analytics 内联脚本(带Nonce) --> <script nonce="{{ nonce }}"> window.dataLayer = window.dataLayer || []; function gtag(){dataLayer.push(arguments);} gtag('js', new Date()); gtag('config', 'G-xxxxxxx'); </script> <!-- 外部GTM脚本(已在CSP中允许域名,无需Nonce) --> <script src="https://www.googletagmanager.com/gtag/js?id=G-xxxxxxx"></script>
关键注意事项
- Nonce必须每次请求随机生成,固定值会失去安全防护意义。
- Helmet的
scriptSrc使用函数形式获取当前请求的Nonce,确保每个请求的CSP策略与内联脚本的Nonce完全匹配。 - Angular SSR渲染时必须完成Nonce占位符的替换,否则内联脚本仍会触发CSP报错。
- 全程避免使用
'unsafe-inline',Nonce方案已完全替代其作用,符合安全最佳实践。
内容的提问来源于stack exchange,提问作者ClayMoreeee
相关产品推荐
相关产品推荐

