前端签名校验本质是用Web Crypto API比对数字签名,验证“谁签的”和“内容没改过”,而非阻止篡改;关键在于前后端对签名原文必须字节级一致,否则verify()返回false。

前端签名校验不是防篡改,是验证“谁签的”和“没改过”
HTML 页面本身没有签名能力,所谓“前端签名校验”,本质是用 Web Crypto API 在浏览器里做一次数字签名比对:服务端用私钥对某段原文(比如 DOM 结构哈希、JSON 配置、脚本内容)签名,前端拿到公钥 + 原文 + 签名,调用 crypto.subtle.verify() 判断是否匹配。它不阻止篡改,只告诉你“此刻 DOM 是否和签名时一致”。
常见错误现象:Signature verification failed 往往不是密钥错了,而是前后端对“原文”的定义不一致——比如服务端签名的是 document.body.innerHTML,前端却用了 document.documentElement.outerHTML;或者服务端用了 JSON.stringify(obj),前端忘了 sort_keys=True,导致键序不同。
- 签名对象必须是字节级确定的:推荐用
new TextEncoder().encode(str)或直接取ArrayBuffer,避免.text()引入编码归一化问题 - DOM 内容提取要稳定:不用
innerHTML(会丢失注释、顺序可能变),优先用document.body.cloneNode(true).outerHTML+ 正则清理空白(但需约定规则) - 公钥必须是 PEM 去头尾后 Base64 解码的
Uint8Array,不能直接传字符串给importKey()
如何提取“可签名”的 DOM 片段而不踩坑
直接对整个 document 签名不可靠:meta 标签里的时间戳、动态插入的 script、甚至 devtools 注入的调试节点都会导致每次计算结果不同。真正能签的,是业务上“应该不变”的那部分。
典型可签名片段包括:document.getElementById('main-content').outerHTML、关键配置节点的 textContent、或由服务端预先生成并注入的 JSON 脚本内容(如 <script type="application/json" id="config">{...}</script>)。
立即学习“前端免费学习笔记(深入)”;
- 避免用
document.body.innerHTML:它会忽略style和script标签内的换行处理差异,不同浏览器输出不一致 - 禁用动态渲染干扰:校验前确保所有异步组件已 hydrate 完成,否则签的是骨架 DOM,不是最终态
- 若含用户输入内容(如表单值),必须排除——这部分本就不该参与完整性校验
- 建议服务端返回一个
data-signature-target属性,明确指定校验范围,前端按此 selector 提取
Web Crypto API verify() 调用失败的三个高频原因
crypto.subtle.verify() 返回 false 不等于“验签失败”,更可能是输入数据没对齐。它不抛异常,只返回布尔值,容易误判。
- 签名值是 Base64url 编码(RFC 4648 §5),不是标准 Base64:需替换
-→+、_→/,再补等号,才能用atob()解码为Uint8Array - 哈希算法必须和服务端完全一致:服务端用
SHA-256签名,前端就不能用SHA-512计算摘要 - 公钥格式必须是
"spki"(SubjectPublicKeyInfo),不是 PKCS#1 的-----BEGIN RSA PUBLIC KEY-----;后者需先转成 SPKI 格式再导入
示例片段(非完整流程):
const signature = new Uint8Array(atob(base64url.replace(/-/g, '+').replace(/_/g, '/')).split('').map(c => c.charCodeAt(0)));
const data = new TextEncoder().encode(targetHTML);
await crypto.subtle.verify({ name: "RSASSA-PKCS1-v1_5", hash: "SHA-256" }, publicKey, signature, data);
为什么不能只靠 DOM 校验?真正的防线在哪儿
DOM 签名校验只能确认“当前页面结构和服务端签发时一致”,但它无法防御:中间人替换整个 HTML 文件、CDN 缓存污染、服务端模板被注入恶意脚本、或用户禁用 JS 后绕过校验逻辑。它只是纵深防御中的一环。
真正起作用的组合是:Subresource Integrity (SRI) 保资源哈希、CSP 控制加载域、HTTPS 防传输劫持、加上服务端对关键操作(如支付、权限变更)的独立状态校验。而 DOM 签名,只适合验证“静态展示层是否被意外覆盖”——比如运营后台发布的活动页,防止构建产物被 CI/CD 流程误覆盖。
最容易被忽略的地方:签名原文一旦包含时间戳、随机 nonce 或用户 session ID,就失去了可复现性,根本无法在校验时还原。所以签名目标必须是纯静态、服务端可控、且无运行时变量的部分。



















