---
title: "静态资源返回 200 却加载失败：排查 SRI 摘要不匹配"
date: 2026-11-13
slug: web-subresource-integrity-byte-mismatch
canonical: https://kernelranch.com/blog/2026/11/web-subresource-integrity-byte-mismatch
markdown_url: https://kernelranch.com/blog/2026/11/web-subresource-integrity-byte-mismatch.md
tags: ["web", "security", "cdn", "nodejs"]
---脚本地址返回 HTTP 200，加载仍可能被完整性校验拒绝。SRI 将页面里声明的摘要与实际取得的资源字节比较。构建工具或 CDN 即便只改写一个换行符，也会改变摘要。排查时应对照部署清单与客户端实际取得的字节。

## 1. 本机复现

保存为 `integrity_lab.mjs`，运行 `node integrity_lab.mjs`。同一个本地服务返回原始样本和多一个换行的样本，用 Fetch 的 `integrity` 参数校验。

```javascript
import assert from 'node:assert/strict';
import { createHash } from 'node:crypto';
import { once } from 'node:events';
import { createServer } from 'node:http';

const original = 'console.log("ok");\n';
const changed = original + '\n';
const sri = 'sha384-' + createHash('sha384').update(original).digest('base64');
const server = createServer((req, res) => {
  res.writeHead(200, { 'Content-Type': 'text/javascript; charset=utf-8' });
  res.end(req.url === '/original.js' ? original : changed);
});
server.listen(0, '127.0.0.1');
await once(server, 'listening');
const base = 'http://127.0.0.1:' + server.address().port;

try {
  const originalResponse = await fetch(base + '/original.js', { integrity: sri });
  assert.equal(await originalResponse.text(), original);
  console.log('matching SHA-384:', originalResponse.status);

  const changedResponse = await fetch(base + '/changed.js');
  assert.equal(changedResponse.status, 200);
  assert.equal(await changedResponse.text(), changed);
  console.log('changed resource without integrity:', changedResponse.status);

  await assert.rejects(fetch(base + '/changed.js', { integrity: sri }), TypeError);
  console.log('changed resource with integrity: rejected');
} finally {
  server.close();
  await once(server, 'close');
}
```

本地实测输出如下。

```text
matching SHA-384: 200
changed resource without integrity: 200
changed resource with integrity: rejected
```

## 2. 把摘要绑定到发布产物

1. 在压缩和打包步骤完成后，对最终产物生成 `sha384-` 加 Base64 摘要。HTTP 传输采用内容压缩时，核对的是 Fetch 解码后的资源内容。
2. 给产物使用版本化 URL，把该版本的 URL 和摘要一起写入 HTML。发布和回滚时同时核对这两个值，并检查 CDN 是否又做了换行转换、二次压缩混淆或内容注入等字节改写。
3. 跨源脚本还要满足 CORS。部署时可以用 `crossorigin="anonymous"` 发起跨源请求，并确认资源服务器返回适当的允许来源响应头。HTTP 200 只能说明请求到了可响应的地址，浏览器还会执行跨域和完整性校验。

## 小结

对实际发布的资源内容生成摘要，并随资源版本一起更新。先核对客户端拿到的字节，再排查跨源加载条件。

## 数据来源与检索时间

检索时间 2026-11-13。

| 来源 | URL | 对应内容 |
|---|---|---|
| W3C Subresource Integrity 工作草案 | https://www.w3.org/TR/2026/WD-sri-2-20260320/ | 摘要格式、验证过程和跨源约束 |
| WHATWG Fetch | https://fetch.spec.whatwg.org/#concept-request-integrity-metadata | 请求完整性元数据 |

## 核验说明

- Windows、Node.js v24.19.0 在回环接口验证 SHA-384 匹配和多一个换行导致的拒绝。浏览器脚本元素、跨源 CORS 与 CDN 部署依据规范说明。
