什么是SRI

SRI是Subresource Integrity的简写,表示的是子资源的完整性。那么什么又是子资源呢,比如我们常见的页面中会引入各种第三方脚本库。
<link rel="stylesheet"
href="https://stackpath.bootstrapcdn.com/bootstrap/3.4.1/css/bootstrap.min.css"
crossorigin="anonymous">
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"
crossorigin="anonymous"></script>
一般情况下,为了提高网页的响应速度以及性能,我们通常会把这些子资源放到CDN上。对于大的互联网公司来说,一般会有自己的云服务,也基本会有自己的CDN服务。但是对于小公司来说,一般会使用云服务厂商提供的CDN功能。这里就会有一个问题,如果我们托管在云服务厂商的CDN上的资源万一被篡改了,那么就会对我们的业务产生一些影响。虽然这种事情一般情况下不会发生,但是如果我们的业务对安全要求很高的话,那么还是要对这种情况做好防范处理。
如何使用SRI
上面简单介绍了SRI的作用,那么具体怎么实践呢?下面我们来一起实践一下如何使用SRI。
首先我们随便创建一个index.html,然后在里面添加如下内容:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>SRI-demo</title>
</head>
<body>
<h1>SRI-demo</h1>
<script src="http://localhost:3000/test.js"
integrity="sha384-yGduQba2SOt4PhcoqN6zsgbwhbpK8ZBguLWCSdnSRc6zY/MmfJEmBguDBXJpvXFg"
crossorigin="anonymous"></script>
</body>
</html>
然后创建一个test.js文件,里面内容如下:
document.write("Hello World!");
然后在本地使用Node.js的express框架或者其他的工具,让test.js能够在本地通过http://localhost:3000/test.js访问。
对于上面script标签的integrity属性,我们可以通过如下的命令,通过openssl工具获取对应的sha384算法生成的字符串:
cat test.js | openssl dgst -sha384 -binary | openssl base64 -A
如果是Windows环境的话,需要使用另外的方式获取对应的字符串。
然后在浏览器中打开index.html,你会看到页面上展示:Hello World!。如果我们这个时候把test.js的内容更改一下,在原来的基础上,把Hello World!后面的感叹号去掉,如下所示:
document.write("Hello World");
那么这个时候页面就是空白的,不再展示Hello World。对应的控制台也会报错,不过不同的浏览器报错信息不一样:
Chrome报错如下:

总之都会提醒你,当前下载的子资源通过计算后的哈希字符串,跟标签上的不一致,浏览器拒绝执行对应的代码。
这里还有一些需要注意的地方,如果我们的test.js资源跟我们的index.html是不同的源,那么还需要在标签上添加crossorigin="anonymous",表明这个资源的请求是需要跨源资源共享的。不然浏览器会报错如下:

如果对跨源资源共享还不是很明白的同学可以参考Cross-Origin Resource Sharing。
当然对应的服务端也需要设置对应的响应头部:Access-Control-Allow-Origin: *,如果是使用express的话,可以使用cors来简单的设置一下。具体如下所示:
app.use(
cors({
origin: "*",
})
);
如何在框架中使用SRI
- 对于Vue项目来说,通过使用Vue CLI我们可以很简单的就使用这个功能。通过在
vue.config.js中增加一个配置:integrity: true,我们就可以在构建的时候后看到,打包后的index.html中引入的资源都是带有integrity属性的,如下面所示:
<link href="/css/app.fb0c6e1c.css" rel="stylesheet"
integrity="sha384-1Ekc46o2fTK9DVGas4xXelFNSBIzgXeLlQlipQEqYUDHkR32K9dbpIkPwq+JK6cl">
<script src="/js/chunk-vendors.0691b6c2.js"
integrity="sha384-j7EDAmdSMZbkzJnbdSJdteOHi77fyFw7j6JeGYAf4O20/zAyQq1nJ91iweLs6NDd"></script>
<script src="/js/app.290d19ae.js"
integrity="sha384-S3skbo1aIjA4WCmQH6ltlpwMgTXWrakI5+aloQEnNKpEKRfbNyy1eq6SrV88LGOh"></script>
- 对于其他框架来说,如果打包工具使用的是Webpack的话,可以直接使用对应的插件webpack-subresource-integrity,相关的安装和使用说明可以参考这里。
关于Integrity的一些细节
在实际的使用过程中,还有很多细节需要注意的,下面给大家再深入的介绍一下。
- 目前使用计算资源文件哈希值的算法有
sha256,sha384,sha512,这些都是属于SHA-2的安全散列算法。 - 目前已经不推荐使用
MD5和SHA-1的计算哈希值的算法。 - 首先
Integrity的值可以存在多个,每个值之间使用空格分隔开。 - 如果多个值分别使用的是不同的安全散列算法,比如如下所示:
<script src="http://localhost:3000/test.js"
crossorigin="anonymous"
integrity="
sha256-LsK9lSOT7mZ9iEbLTm9cwaKTfuBdypNn2ID1Z9g7ZPM=
sha384-yGduQba2SOt4PhcoqN6zsgbwhbpK8ZBguLWCSdnSRc6zY/MmfJEmBguDBXJpvXFg
sha512-2qg2xR+0XgpsowJp3VCqWFgQalU9xPbqNTV0fdM9sV9ltHSSAcHni2Oo0Woo6aj860KvFu8S1Rdwb8oxJlMJ2Q==
"></script>
那么这个时候浏览器是根据那个安全散列算法来进行处理的呢?还是说只要有一个匹配就可以了呢?
答案是:浏览器首先会选择安全性最高的那个计算方式,如果是上面这个例子的话,浏览器会选择sha512这种计算哈希值的算法。因为sha512的安全性大于sha384,sha384的安全性大于sha256,然后会忽略掉其余通过其他方式计算出的哈希值。这个时候需要注意的是,如果浏览器根据sha512计算出来的哈希字符串跟提供的不一样的话,那么不管sha384或者sha256提供的哈希值是否正确,浏览器都会认为这个资源计算出来的哈希值跟提供的哈希值不一样。所以不会执行对应的代码。
- 如果多个值分别使用的是相同的安全散列算法,比如如下所示:
<script src="http://localhost:3000/test.js"
crossorigin="anonymous"
integrity="
sha384-yGduQba2SOt4PhcoqN6zsgbwhbpK8ZBguLWCSdnSRc6zY/MmfJEmBguDBXJpvXFg
sha384-c+xXeW2CdZ1OuDKSrMpABg4MrVFWi3N5VKDC6CTgSRRnPr0dgprowjuFPomHgXlI
sha384-E6ULLMoeKAMASZMjQ00AvU+3GzK8HPRhL/bM+P4JdcHLbNqGzU14K9ufSPJCnuex
"></script>
那么这个时候只要有一个值跟浏览器计算的结果是一样的,那么这个资源就可以被认为是没有被篡改的;资源的内容是可以被执行的。
Integrity属性暂时只支持link和script标签,以后会支持更多的关于子资源的标签,比如:audio,embed,iframe,img等。