静态站点子资源完整性 SRI 落地实践:防止缓存劫持与资源篡改
前言
前面我们完成了 Service Worker 缓存、缓存版本管理、缓存垃圾回收整套离线方案。但这里存在一个漏洞:就算我们做好了缓存更新与 GC,如果 CDN、静态存储链路被劫持,返回被篡改的 JS / CSS / 图片资源,浏览器依然会正常加载并执行。
SRI(Subresource Integrity,子资源完整性) 就是用来解决这个问题的:提前在 HTML 中写入资源的哈希摘要,浏览器拉取资源后自动校验哈希,不一致则拒绝加载,从源头拦截篡改资源。本文基于我们的校园景点静态站点,完整落地 SRI,同时适配构建脚本、CI 流水线、离线缓存场景。
核心原理
SRI 通过 integrity 属性绑定资源哈希,搭配 crossorigin 属性使用:
- 构建阶段:对每一个静态资源(js、css、svg)生成
sha256/sha384/sha512哈希; - 注入到 HTML 的标签中:
<script src="xxx.js" integrity="sha384-xxxx" crossorigin="anonymous"></script>; - 浏览器下载资源后,实时计算资源哈希,和标签内摘要比对;
- 校验失败:直接阻止资源加载,抛出控制台错误,页面不会执行篡改后的代码。
注意:SRI 只对跨域资源或者开启 CORS 的同域 CDN 资源生效,
crossorigin不能省略。
一、Node 脚本:批量生成 SRI 哈希
新建 scripts/sri-generator.js,集成到我们已有的资源检查脚本中:
const fs = require('fs');
const path = require('path');
const crypto = require('crypto');
// 计算文件 integrity hash,默认 sha384
function getSriHash(filePath, algo = 'sha384') {
const buffer = fs.readFileSync(filePath);
const hash = crypto.createHash(algo).update(buffer);
const base64 = hash.digest('base64');
return `${algo}-${base64}`;
}
// 扫描目录,输出资源路径+哈希
function scanAssets(dir, extList = ['.js', '.css']) {
const result = {};
const files = fs.readdirSync(dir, { withFileTypes: true });
for (const f of files) {
const fullPath = path.join(dir, f.name);
if (f.isDirectory()) {
Object.assign(result, scanAssets(fullPath, extList));
} else if (extList.includes(path.extname(f.name))) {
const relPath = path.join('/assets', f.name);
result[relPath] = getSriHash(fullPath);
}
}
return result;
}
// 写入 sri-manifest.json,给HTML注入脚本读取
const assetMap = scanAssets('./dist/assets');
fs.writeFileSync('./dist/sri-manifest.json', JSON.stringify(assetMap, null, 2), 'utf8');
console.log('✅ SRI manifest generated');
运行命令:
node scripts/sri-generator.js
输出 sri-manifest.json,作为构建产物清单,后续模板引擎读取这个清单自动给标签加上 integrity。
二、HTML 模板改造示例
原始标签:
<link rel="stylesheet" href="/assets/style.css">
<script src="/assets/main.js"></script>
注入 SRI 之后:
<link rel="stylesheet" href="/assets/style.css"
integrity="sha384-xxxxxxxxxxxxxxxxxxxx"
crossorigin="anonymous">
<script src="/assets/main.js"
integrity="sha384-yyyyyyyyyyyyyyyyyyyy"
crossorigin="anonymous"></script>
图片资源一般不建议开启 SRI:图片篡改风险低,而且图片更新后哈希会变动,容易和缓存策略冲突;只对可执行、可渲染的脚本、样式表开启。
三、和现有 Workbox 离线缓存联动的关键约束
这里有一个非常容易踩坑的组合问题:SRI + Service Worker 缓存
- 资源更新之后,文件内容变了 → 哈希一定会变;
- HTML 里面的 integrity 同时更新;
- 如果 SW 缓存了旧版本资源,新 HTML 拿到新哈希,去加载缓存里的旧资源,哈希校验失败 → 资源加载失败。
✅ 配套策略:
- 资源文件名加入内容哈希(
main.8f2ac.js),文件内容变化则文件名变化,天然隔离新旧资源; - SRI 哈希和资源内容哈希保持同步,在构建阶段一起生成;
- 缓存 GC 策略继续清理旧命名的资源缓存,避免缓存池膨胀;
- 不要对 HTML 入口页面做长期强缓存,保证用户能拿到带最新 integrity 的 HTML。
四、CI 流水线集成与校验卡点
在 GitCode CI 中增加两个检查步骤:
- 构建产物必须生成合法的
sri-manifest.json,缺少则构建失败; - 扫描所有 HTML,校验引用的资源是否都存在对应的 integrity 字段,缺失直接阻断发布;
- 禁止手动硬编码修改 integrity 值,所有哈希由脚本自动生成。
简易校验片段:
// 检查HTML中资源是否都有integrity
function checkSriInHtml(html, manifest) {
const assetUrls = [...html.matchAll(/href|src="(\/assets\/.+?)"/g)].map(m=>m[1]);
for(const url of assetUrls) {
if(['.js','.css'].some(e=>url.endsWith(e)) && !manifest[url]) {
throw new Error(`缺少SRI哈希:${url}`);
}
}
}
五、工程权衡与边界
优势
- 防御 CDN 劫持、中间人篡改静态脚本样式;
- 浏览器原生能力,不需要额外引入 SDK,性能损耗极低;
- 和我们现有的资源路径白名单、构建检查脚本、缓存体系完美串联。
局限 & 坑点
- 资源微调一点点,哈希就会失效,适合内容不可随意变更的静态资产;
- 动态拼接、远程第三方脚本(非自己管控)不能加 SRI;
- 必须配套 CORS,很多免费静态托管平台需要确认跨域头配置;
- 图片、字体等大资源开启 SRI 收益很低,维护成本更高。
六、异常降级方案
SRI 校验失败时浏览器默认直接丢弃资源,页面容易白屏。我们可以增加兜底监控:
- 监听
onerror事件捕获 SRI 校验失败; - 上报异常埋点到性能统计接口;
- 提示用户刷新页面,拉取最新 HTML 和资源。
<script onerror="console.error('SRI校验失败,资源被篡改或版本不匹配')" src="/assets/main.js" integrity="xxx" crossorigin="anonymous"></script>
总结
SRI 不是缓存的替代品,而是安全层的加固。在我们整套静态站点体系里:
资源路径白名单(构建拦截非法链接)→ 增量发布 + 图片压缩(体积优化)→ LCP/性能埋点(观测)→ Workbox 离线缓存(可用性)→ 缓存GC(存储清理)→ SRI完整性校验(安全防篡改)。
到这里,一套兼顾体验、性能、离线可用、安全的静态站点链路已经闭环。
- 点赞
- 收藏
- 关注作者
评论(0)