做Svelte项目做了快三年,最近几次给应用做上线前的安全加固,几乎每次都会被CSP(Content Security Policy)卡一轮。先是一打开页面控制台满屏的“Refused to execute inline script”,然后又是接口请求被connect-src拦掉,最后折腾完又发现第三方统计脚本把strict-dynamic干翻了。这套流程走下来,我对“Svelte + CSP”的组合算是彻底摸透了。
这篇东西不是把MDN文档翻译一遍,而是把我在真实项目里怎么设计、怎么配置、怎么排查CSP策略的完整过程记录下来。不管你是用SvelteKit做整套应用,还是用Vite + Svelte写个中后台前端,只要想给项目加上CSP或者正在被CSP报错折磨,这篇内容应该能帮你少走不少弯路。我会从CSP的基础取舍讲起,给到SvelteKit和普通Vite项目的两套落地配置,最后再把我踩过的坑整理成一份可以直接对着排查的故障表。
1. 为什么Svelte应用里做CSP特别容易踩坑
1.1 Svelte的编译模型和CSP天然有摩擦
很多前端开发者第一次接触CSP时,觉得这不就是个响应头吗,往服务器上一挂就完事。但放到Svelte项目里,事情就没那么简单。
Svelte和React、Vue最大的区别在于它是一个编译器。你在.svelte文件里写的那些模板语法、组件逻辑,最后都会在构建阶段被编译成纯JavaScript操作DOM的代码,而不是带着一大坨运行时框架跑。这听起来和CSP没什么冲突,但问题出在Svelte生成的代码经常会以内联脚本的形式出现。尤其是SvelteKit这类框架,为了做路由预加载、页面数据序列化、hydration,它会在HTML里插入一段内联的启动脚本。如果CSP的script-src设置成'self'这种常规配置,这些内联脚本全部会被浏览器拒绝执行。
另一个容易忽略的点是Svelte的组件里可以写<style>块,这些样式在开发模式和生产模式的处理方式不同。生产构建下样式会被提取成独立的CSS文件,这样倒还好。但如果你在组件里通过style:指令去动态绑定样式,或者用了某些需要运行时注入样式的库,CSP的style-src又把你的路堵死了。
Svelte应用对CSP的敏感度比普通React应用要高得多,因为SvelteKit默认就依赖内联脚本来工作,这是框架的架构决定的,不是你写代码不小心造成的。很多人在这一步就开始慌了,然后直接放弃CSP或者加'unsafe-inline'把所有东西全放行,这恰恰是本末倒置。
1.2 你看到的报错可能来自三个不同层级
排查CSP问题之前,先搞清楚报错是从哪一层冒出来的,这能省掉大量时间。我习惯把Svelte项目的CSP报错分成三层。
第一层是静态资源层。HTML本身可能带了内联脚本,比如你在src/app.html里手写的初始化脚本、SvelteKit自动注入的preload脚本,这一层管的是“页面加载时能执行什么”。
第二层是运行时动态加载层。项目用了动态import()拆包,或者某个第三方库在运行时往<head>里插了<script>标签,又或者基于new Function()做代码生成。这一层归script-src下面的strict-dynamic和nonce机制管。
第三层是网络请求层。页面加载后前端向后端发API请求、上传日志、加载图片字体,这些分别受connect-src、img-src、font-src控制。很多Svelte项目会配置多个API地址或走网关代理,这里最容易出现“页面能打开但所有接口都挂掉”的诡异现象。
我在实际排查时,会先把浏览器的Console窗口单独拉出来,按Error级别过滤,然后一段段看报错信息里的violated directive字段。这个字段会直接告诉你违反了哪条指令,不要自己瞎猜。搞清楚是哪一层的问题,再决定是改代码、改配置还是改服务器响应头。
2. 动手前先搞懂:CSP指令到底要怎么写
2.1 常用指令的取舍与最小授权原则
真正开始配置CSP之前,先花五分钟想清楚你愿意承担多少维护成本。CSP不是越严格越好,严格到连自己都跑不动,最后同事怨声载道,策略迟早被删掉。
我常用的基础指令集是这样的:
default-src 'self' script-src 'self' 'strict-dynamic' 'nonce-xxxx' style-src 'self' 'unsafe-inline' img-src 'self' data: https: connect-src 'self' https://api.example.com font-src 'self' data: object-src 'none' base-uri 'self' frame-ancestors 'self'逐条说下思路。
default-src 'self'是所有没写明的指令的兜底值,这里卡死,防止遗漏。
script-src是最核心的一条。我没有在基础配置里直接放'unsafe-inline',因为SvelteKit的csp模块会自动给内联脚本打hash或者nonce,后面会展开讲。加上'strict-dynamic'之后,所有被nonce信任的脚本创建的子脚本也会被信任,这样动态加载import()、模块化脚本才不会被拦。
style-src里我留了'unsafe-inline',这其实是一个有意识的妥协。Svelte组件中样式写在<style>里编译后通常会被提取为外部CSS,但一些动态绑定的style属性以及第三方组件的内联样式仍然存在。如果此时把内联样式彻底禁掉,最容易出现的后果是日期选择器、富文本编辑器这类组件变得完全没有样式,这种问题的排查成本远大于保留'unsafe-inline'带来的风险。
connect-src这里一定要把项目所有可能访问的接口域名列全。很多SvelteKit项目同时有自己后端的API地址、第三方上传服务地址、错误监控上报地址,漏一个就有一类请求失败。
object-src 'none'和base-uri 'self'属于成本极低但收益很高的两条指令,建议直接加上,防止某些老旧的插件加载攻击。frame-ancestors 'self'则能避免你的应用被别的网站通过iframe嵌入,这个在做登录类系统时特别有用。
2.2 选nonce还是hash,我建议按这两个条件判断
CSP里最难理解也最常被问的就是'unsafe-inline'的替代方案:nonce和hash。两者都能让浏览器放行指定的内联脚本,但使用场景完全不同。
Nonce的意思是给每个内联脚本一个一次性随机数,服务端在返回HTML时生成一个随机值,同时写到CSP响应头和<script nonce="...">标签里。浏览器比对两者一致才会执行脚本。它的特点是动态的,每次页面请求都要重新生成。Hash则是把内联脚本的内容做SHA-256摘要,写到CSP里,只要脚本内容不变,策略就始终有效。它的特点是静态的,适合内容固定的内联脚本。
我给项目做选择的逻辑很简单:如果用了SvelteKit这类自带服务端渲染的框架,直接走nonce路线。SvelteKit的csp配置开启后会自动生成并注入nonce,不需要你自己操心同步问题。如果是纯静态部署的Vite + Svelte项目,那么大多数情况下没有动态生成响应头的地方,这时用hash更现实。你只要构建一次,把<script>内容对应的hash值记下来写进CSP头就行。当然,前提是这个脚本内容在多次构建之间保持稳定。如果每次构建hash都会变,反而更麻烦。
一个需要特别注意的坑是:一旦在script-src里使用了nonce或hash,'unsafe-inline'会自动被浏览器忽略。这是CSP规范里的设计——当你提供了更安全的替代方案,浏览器会无视'unsafe-inline'。所以不要抱着“我两个都写上总没问题吧”的心态配置,这只会让安全审计人员觉得你不懂CSP。
2.3 strict-dynamic带来的现代CSP写法
早几年写CSP时,大家普遍会把第三方脚本域名直接放进白名单,比如加一条https://cdn.bootcdn.net。这种做法在今天是很难维持的,因为你依赖CDN域名,而这个域名下任何一个文件被劫持,整个页面就沦陷了。
'strict-dynamic'解决的就是这个信任链问题。它的规则很简单:只要是受信任脚本(通过nonce或hash校验)创建的后续动态脚本,也自动获得信任。也就是说,HTML里那个带nonce的主脚本可以new一个script标签去加载SDK,这个SDK加载出来的子脚本不需要再出现在CSP白名单里。
举个实际场景。我在项目里接入了前端错误监控SDK,它需要在运行时加载一个远程脚本。配置了'strict-dynamic'之后,主页面脚本通过document.createElement('script')去加载SDK,只要主脚本的nonce是合法的,这个动态脚本就会被允许执行。我不用再去关心SDK最终从哪个域名加载,也不用担心以后换CDN导致策略失效。
P.S. 如果你想进一步了解strict-dynamic对老浏览器的影响,一个比较省事的兜底方案是在加了strict-dynamic的同时保留'self',这样老浏览器会忽略strict-dynamic并按照白名单校验,新浏览器则优先采用严格模式。
3. SvelteKit项目的CSP落地配置:推荐方案
3.1 在svelte.config.js里开启内建CSP支持
SvelteKit官方其实内置了CSP的支持,这在很多框架里是不多见的,用起来也简单。只需在svelte.config.js里增加一个csp配置项:
import adapter from '@sveltejs/adapter-auto'; const config = { kit: { adapter: adapter(), csp: { mode: 'auto', directives: { 'base-uri': ['self'], 'object-src': ['none'], 'script-src': ['self'], 'style-src': ['self', 'unsafe-inline'], 'img-src': ['self', 'data:', 'https:'], 'connect-src': ['self', 'wss:'], 'font-src': ['self', 'data:'], 'frame-ancestors': ['self'] } } } }; export default config;重点解释mode: 'auto'。SvelteKit文档里说得很清楚:在auto模式下,框架会自动分析HTML中需要放行的内联脚本,然后为它们生成nonce或hash并注入到CSP策略中。对大多数项目来说,这近乎是零成本的配置方式。它会自动处理SvelteKit自身用于hydration、页面数据序列化产生的那些内联脚本,不用我们手工维护hash列表。
服务端渲染直接以完整的CSP响应头输出给浏览器;纯静态部署的时候,SvelteKit会把策略写成<meta http-equiv="Content-Security-Policy">内嵌在HTML中。两种模式我都在生产环境跑过,没有发现功能上的差异,只是一般能上HTTP响应头就优选用响应头,因为meta标签方式的策略在某些浏览器里有一些限制(如report-uri无法生效)。
3.2 自定义connect-src、report-uri与更多指令
内置配置保证了最基础的可用性,但实际项目中我们需要扩写很多指令。最典型的就是connect-src,SvelteKit应用八成会跟WebSocket或不同域名的API打交道。我之前有个项目就是这样,前端部署在A域名,接口在B域名,日志上报在C域名,如果只写connect-src: 'self',B和C会被全部拦截。
所以我的做法是在默认指令的基础上覆盖自定义的connect-src:
csp: { mode: 'auto', directives: { 'script-src': ['self'], 'connect-src': [ 'self', 'https://api.example.com', 'https://log.example.com', 'wss://api.example.com' ], // 其他指令保持默认 } }除此之外强烈建议加上report-uri或report-to。这两个指令不同之处在于report-uri是老标准,report-to是新标准,实际项目中可以只用其中之一,也可以都写上。它的作用是让浏览器在违反策略时,把违规详情上报到你指定的服务端接口,而不是只默默在控制台输出一条错误。这样可以集中收集策略误伤情况。
SvelteKit里配置起来也简单:
csp: { mode: 'auto', directives: { // 上面那些指令 }, reportOnly: { // 仅报告的配置 } }这里我特别说一下reportOnly字段。它相当于一个“影子CSP”——浏览器会执行所有检查并上报违规,但不会真的拦截任何内容。我每次调整CSP脚本时,都会先在reportOnly模式下跑个两三天,收集线上真实用户的违规数据,确认没有误伤之后再正式切换。如果你直接上线一个严格CSP,很容易毫无过渡地打断线上用户的操作。
3.3 验证配置:用中间人页面和浏览器控制台
配置好之后,怎么确认它真的生效并且没有误杀东西?这里我有一套固定的验证流程。
先用curl -I看一下响应头是否正常返回:
curl -I https://your-app.example.com正常情况下能看到content-security-policy响应头。如果走的是meta标签方式,就直接查看HTML源码,确认<meta http-equiv="Content-Security-Policy">出现在<head>最前面。
然后打开浏览器访问首页,把DevTools切到Console,按级别过滤Error。正常情况下页面所有功能都应该正常执行,控制台不应该出现任何CSP相关的红色报错。接着系统地过一遍核心用户流程:登录、跳转、数据加载、文件上传、图表渲染、支付回调。每个环节都用浏览器审计面板看一下有没有资源被拦或者请求失败。
这个流程看着繁琐,但真的非常值得做。我之前有一次只改了connect-src,以为影响面很小,就没过流程,结果图表组件里加载的云端字体被font-src卡住了,图表区域全部回退成系统字体,视觉上乱了套,还是用户在群里截图反馈才被发现。
4. 普通Vite + Svelte项目怎么补上CSP
4.1 把CSP交给服务端:Nginx和Express的设置方式
不是所有人都在用SvelteKit,很多项目还是常规的Vite + Svelte,打包出来是一堆静态文件,然后扔到Nginx或者云存储上。这种场景下,把CSP写进构建配置的收益不大,因为响应头最终由Nginx来出。
最简单的落地方式是直接在服务器配置里加响应头。例如Nginx:
server { listen 443 ssl; server_name your-app.example.com; root /var/www/svelte-app; index index.html; add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'sha256-xxxxx'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; connect-src 'self' https://api.example.com; object-src 'none'; base-uri 'self'; frame-ancestors 'self';"; }使用add_header的注意事项是:如果你在一个server块里有多个add_header指令,Nginx默认只会输出当前作用域里第一条匹配的add_header,所以最好把CSP和其他安全头写在一起。
Express服务端则更直接:
app.use((req, res, next) => { res.setHeader( 'Content-Security-Policy', "default-src 'self'; script-src 'self' 'sha256-xxxxx'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; connect-src 'self' https://api.example.com; object-src 'none';" ); next(); });这里的'sha256-xxxxx'指的就是你构建产物中那个内联脚本的hash值。具体怎么拿到这个hash值,我在下一节说。
4.2 构建期注入nonce:一个轻量插件思路
如果静态站点的内联脚本比较多,用hash一个个维护实在痛苦,另一个思路是利用服务端模板在返回HTML时注入nonce,并在响应头中动态返回对应的CSP策略。
一个比较轻量的做法是用一个极小的Express中间件,读取HTML文件,替脚本标签加上随机nonce,同时重新生成响应头:
import express from 'express'; import { readFileSync } from 'fs'; import crypto from 'crypto'; const app = express(); const html = readFileSync('dist/index.html', 'utf8'); app.use((req, res, next) => { const nonce = crypto.randomBytes(16).toString('base64'); const injectedHtml = html.replace(/<script/g, `<script nonce="${nonce}"`); res.setHeader( 'Content-Security-Policy', `script-src 'nonce-${nonce}' 'strict-dynamic'; object-src 'none'; base-uri 'self';` ); res.send(injectedHtml); }); app.listen(8080);这段代码核心思路是:不让CSP和HTML各自独立生成nonce,而是先由中间件生成一个nonce,然后同时注入到HTML标签和响应头里。放到真实项目中,最好还要处理非脚本标签的替换、缓存策略、多页面入口等,但作为起步方案已经足够跑通。
用这个方案时有一点要注意:如果你的HTML里已经通过<link rel="modulepreload">加载了Svelte打包后的模块,那么这些模块脚本同样需要携带nonce,否则会被script-src拦截。上面例子中我把所有<script>标签都处理了,但modulepreload不算是脚本执行,它受script-src约束的机制和普通脚本不同,踩过这个坑,具体看下一节的排查表。
4.3 Svelte模板里内联样式的合法化处理
静态项目的另一个高频问题出在样式上。Svelte组件的样式会被编译进JS或者提取成CSS文件,这部分跟CSP握手良好,因为最终都是外部文件。但如果你在模板里用style:指令动态设置样式,或者第三方组件通过JS改DOM样式,浏览器就会触发style-src的检查。
最省事但安全度最低的方法是把style-src加上'unsafe-inline',这也是很多项目实际采用的做法。我后来找到一个相对平衡的方案:在构建时强制提取Svelte的所有静态样式为独立CSS文件,同时禁止<style>块中的动态样式注入。
Vite + Svelte插件的配置可以这样写:
import { svelte } from '@sveltejs/vite-plugin-svelte'; export default { plugins: [ svelte({ compilerOptions: { css: 'external' } }) ] };把css设置成external后,Svelte会把组件样式抽取成独立CSS文件,而不是通过JS插入到文档中。这样一来,CSP的style-src就可以只写'self'而不需要'unsafe-inline',整个页面完全走外部资源路径。
代价也有。动态绑定在元素上的style:指令仍然会以inline style的形式出现,这是浏览器API本身的行为,绕不开。如果你项目里大量依赖动态样式绑定,我建议还是老老实实保留style-src 'unsafe-inline',或者通过CSS自定义属性把这些值抽到类名上,从源头上减少内联样式的产生。
5. 实战排障实录:Svelte + CSP高频报错
5.1 “Refused to execute inline script”怎么定位
这是最常见的CSP错误,同时也是信息量最大的一条。先完整decode一下这条报错:
Refused to execute inline script because it violates the following Content Security Policy directive: "script-src 'self'". Either the 'unsafe-inline' keyword, a hash ('sha256-...'), or a nonce ('nonce-...') is required for inline execution.定位方法分三步。
第一步,在DevTools的Elements面板里查看到底是哪一段内联脚本被拦。被拦的脚本通常会被标记成灰色。第二步,判断这段脚本是框架生成的还是自己手写的。如果是SvelteKit框架生成的,优先检查csp配置是否开启了mode: 'auto';如果是自己手写的初始化逻辑,把它移到外部JS文件里就会干净很多。第三步,对于确实无法外部化的脚本,用hash或nonce放行。我个人的意见是能外部化的代码尽量外部化,hash和nonce是给确实绕不过的场景准备的,不是给懒人准备的。
另外有个细节:浏览器控制台显示的错误位置是HTML的行列号,但构建后的HTML可能是经过压缩的,行号不够直观。这时用curl -o -把HTML拉下来格式化再看,会轻松很多。
5.2 connect-src误伤API请求的排查路径
connect-src误伤API的情况,表现和报错长这样:
Refused to connect to 'https://api.example.com/v1/xxx' because it violates the following Content Security Policy directive: "connect-src 'self'"排查逻辑不复杂。先在控制台确认发起请求的域名,然后检查connect-src里有没有这个域名。这里容易漏的是:浏览器对https和wss是不同协议域名,要分别写明;如果API是https://api.example.com,那么http://api.example.com不会自动放行;端口不同也会导致匹配失败,https://api.example.com:8443和https://api.example.com是两回事。
更隐蔽的是跨域请求里的预检请求(OPTIONS)。浏览器在跨域POST之前会先发一个OPTIONS请求做CORS预检,这个预检请求同样会被connect-src拦截。你会在Network里看到OPTIONS请求直接failed,而真正的POST请求根本没发出去。这种情况下,先把connect-src的范围扩大,把预检域名加进去,再看后端CORS是否允许这个来源。
5.3 style-src漏配导致组件样式全部丢失
Svelte项目在开启CSP后样式丢失,九成是style-src写死了'self',而动态插入的样式被拦掉。现象非常迷惑:页面结构正常、内容正常、交互也正常,但UI就像素级地回到了1999年。
处理这个问题时我先确认一点:如果项目使用了Svelte的<style>标签并且css配置为'injected',那么样式是通过JS以<style>节点注入的,这种情况必须给style-src放行'unsafe-inline'。如果已经用css: 'external'强制提取成独立CSS文件,那么style-src保持'self'即可。
另外,第三方组件的样式不受你构建配置的控制。很多UI库会在运行时动态注入样式,遇到这种组件,要么换一个纯CSS方案,要么在style-src里开'unsafe-inline'。这两条路都不完美,但总比线上样式全崩要好。
5.4 unsafe-eval:哪些第三方库会触发它
CSP报错里最难以驯服的其实是'unsafe-eval'相关的问题。报错格式如下:
Refused to evaluate a string as JavaScript because...这意味着有代码在执行eval()、new Function(), 或者等价能力的操作。Svelte本身在dev模式下可能会用到eval来支持HMR热更新,但生产构建一般不会。问题通常出在第三方库身上。
我遇到过的典型情况:某个老版本的日期处理库在运行时用new Function做模板字符串转义、某个图表库用了动态代码生成、某个富文本编辑器为了兼容低版本浏览器内置了eval。这些库你是很难改源码的,所以处理方式无外乎两种:换掉这个库,或者在script-src里加入'unsafe-eval'。我自己的优先级是先换库,换不了再加'unsafe-eval',而且要开reportOnly盯一周确认没有别的意外。
有几个细节要注意:new Function和eval虽然性质相同,但CSP会以同样的方式拦截。Svelte只在开发模式用到eval,如果你在开发模式开了CSP,而生产环境没有,那不是你环境配错了,是预期行为。还有,很多构建工具的dev source map功能也会依赖eval,开CSP调试时容易误伤,可以把dev server的CSP单独放宽松一点,线上再用严格策略。
5.5 report-only模式:先建监控再上强度
如果你正在给一个已经上线的老项目加CSP,我强烈建议先别直接开严格策略,而是走一轮report-only。
SvelteKit的配置里,csp.reportOnly字段就是干这个的。它同样会执行检查、同样会在控制台打日志,但不会真的阻止任何资源加载或脚本执行。这样可以一边收集线上真实违规数据,一边判断哪些情况属于“必须放行的合法使用”、哪些属于“需要改代码的隐患”。
收集几天数据后,再对照report里的内容逐一分类。我的习惯是建一个二维表格来管理:
| 违规项类型 | 示例 | 处理策略 |
|---|---|---|
| 框架自身内联脚本 | SvelteKit的hydration脚本 | 交给框架自动nonce/hash解决 |
| 常见第三方统计脚本 | 站点分析、埋点SDK | 优先用strict-dynamic覆盖,必要时加白名单域名 |
| 历史遗留旧代码 | jQuery时代逻辑、eval字符串拼接 | 推动重构,彻底移除eval |
| 用户端临时环境 | 用户装了某些浏览器插件注入脚本 | 忽略,插件注入不受页面CSP控制 |
这个表的每一行都要有负责人和截止时间,不然漏掉一条,正式切严格模式的时候线上就会炸一个功能。
6. 三个值得长期坚持的CSP实践习惯
6.1 把CSP写进CI的门禁脚本
CSP策略容易在不知不觉中腐化。新同事加入项目,随手在index.html里加了个内联脚本;某个第三方库升级后改用动态加载脚本;这些都不会报红,因为CSP没有匹配到被拦的资源就会保持静默。但一旦将来你收紧CSP,这些历史债务就会集中爆发。
我的做法是在CI里加一个简单的检查步骤:拉取构建产物HTML和JS,扫描其中内联脚本的内容,对比CSP里声明的hash或nonce机制是否覆盖。更加简单粗暴的做法是直接用无头浏览器加载页面,然后监听console里的CSP违规报错,有报错就fail构建。用Playwright写这个脚本工作量并不大:
import { chromium } from 'playwright'; const browser = await chromium.launch(); const page = await browser.newPage(); const cspErrors = []; page.on('console', (msg) => { if (msg.type() === 'error' && msg.text().includes('Content Security Policy')) { cspErrors.push(msg.text()); } }); await page.goto('http://staging.example.com'); await page.waitForLoadState('networkidle'); if (cspErrors.length > 0) { console.error(cspErrors.join('\n')); process.exit(1); } await browser.close();是的,这个方法很简单,但真的能拦住大部分回归。
6.2 定期回归检查第三方依赖
CSP策略写完之后,最怕的是依赖升级。第三方SDK升级后可能切了CDN域名、加了新的接口域名、改成了动态脚本加载,这些都会让原本可用的CSP策略瞬间作废。
我的习惯是每个月做一次全量依赖审查。把package.json里的第三方库过一遍,尤其是那些在浏览器端运行的SDK,逐个确认它们需要的CSP权限是否在现有策略内。如果策略已经很严了,新引入的库大概率会撞墙,那就提前处理,而不是等线上反馈。
这个工作同样可以写脚本辅助:把CSP指令解析后,对照页面上实际加载的所有资源URL,自动把不在白名单里的资源列出来。手动做也不是不行,只是项目大了之后,肉眼核对容易疲劳。
6.3 用CSP Evaluator做灰度前的快速体检
上线前的最后一关,我会把最终的CSP策略扔到CSP Evaluator这个工具里过一遍。它会用一组启发式规则评估策略的安全性,特别是针对strict-dynamic的配置是否合理、是否存在无意义的'unsafe-inline'残留、'unsafe-eval'是否确实必要等。
这个工具能配置的默认策略一般比我们日常写的要严格,但它最大的意义是督促你不要偷懒。比如我常犯的一个毛病是:为了让某个老库能运行,在script-src里加了'unsafe-eval',后来这个库换掉了,但'unsafe-eval'却一直没删。CSP Evaluator就会直接标记这个配置有风险。每次灰度前花五分钟过一遍,成本很低,收益却很确定。
真正做到项目后期,我觉得CSP策略更像是一份活文档,它会随着项目演进不断变化。你不需要追求“消灭所有违规”,而是要保证每一处放行都有明确的原因、有对应的代码位置、有负责人知道为什么这么放。这套思路放在Svelte项目里尤其重要——框架本身对CSP的支持已经做得很好,剩下需要操心的,是项目里那些被框架隐藏起来的动态行为是否跟你写的策略保持一致。