浏览器SOP与CORS
同源策略(SOP)
同源策略(Same origin policy)是浏览器安全模型,是浏览器为了源的安全做出的限制。
源其实就服务器,也就是说,同源策略是通过限制浏览器的行为,来保护服务器的数据,禁止非同源之间窃取对方资源。
例如,“http://127.0.0.1:3000/index.html”的脚本可以访问到“http://127.0.0.1:3000“的资源,但是如果尝试访问”http://127.0.0.1:3001“的资源,那就会报错,浏览器会屏蔽掉3001端口的响应。
同源策略只限制脚本本身的行为,但是不限制静态资源的加载。
例如,可以通过script标签从其它服务器加载jquery文件,但是使用fetch请求就会报错,jsonp就是这个原理,所以CSRF攻击能够通过HTML标签进行。
同源策略除了会屏蔽掉来自非同源的资源外,还会隔离localStorage、sessionStorage、cookie、indexDB,从而保证每个源的数据安全。
接下来我们尝试发送下跨域请求,看看浏览器都有哪些处理。
跨域请求
下面用node启动两个服务器,通过js从3000端口向3001端口发送一个跨域请求。
index.html
<script> fetch("http://127.0.0.1:3001/api"); </script>
client.js
1 const http = require("http"); 2 const fs = require("fs"); 3 const { exec } = require("child_process"); 4 5 http.createServer((req, res) => { 6 const url = req.url; 7 8 if (url === "/") { 9 fs.createReadStream("index.html").pipe(res); 10 } 11 }).listen(3000); 12 13 exec("start http://127.0.0.1:3000/");
server.js
1 const http = require("http"); 2 3 http.createServer((req, res) => { 4 const url = req.url; 5 6 if (url === "/api") { 7 console.log("收到请求"); 8 return res.end("响应的内容"); 9 } 10 }).listen(3001);
然后查看node,已经接收到了请求,但是浏览器这边报CORS错误。
原因是响应头里没有“Access-Control-Allow-Origin”标头,表示3001端口没有跨域白名单,所以浏览器屏蔽掉了来自3001端口的资源,并显示CORS错误。
下面看下CORS是什么。
跨域资源共享策略(CORS)
上面说SOP的目的是保护服务器的资源,那如果这个资源是公开的呢,或者这个资源有跨域白名单呢?所以CORS就应运而生了。
跨域资源共享策略(Cross-origin Resource Sharing)就是在非同源之间协商共享资源的一种策略。
上面的“Access-Control-Allow-Origin”标头的作用就是公布一个允许跨域的白名单。
现在我们给3001端口添加一个标头,允许任意源的访问。
res.writeHead(200, "ok", { "Access-Control-Allow-Origin": "*", });
可以看到,浏览器接收到这样一个白名单后,就知道了3001端口的信息是公开的,于是把响应的内容暴露给了js。
CORS的具体流程如下:
- 浏览器会自动在每个跨域请求中添加Origin头,用于声明请求方的源。
- 资源服务器根据请求中Origin标头返回访问控制策略”Access-Control-Allow-Origin“标头,并在其中声明允许读取响应内容的源。
- 浏览器检查资源服务器在”Access-Control-Allow-Origin“标头中声明的源,是否与请求方的源相符,如果相符合,则允许请求方脚本读取响应资源,否则屏蔽掉。
另外,根据MDN的文档定义,请求方法为:GET、POST、HEAD,且请求头Content-Type为:text/plain、multipart/form-data、application/x-www-form-urlencoded的就属于简单请求。
我们刚才的请求就是简单请求,而非简单请求会在正式通信之前,增加一次HTTP查询请求,称为"预检"请求(preflight)。
"预检"请求用的请求方法是OPTIONS,表示这个请求是用来询问的。
浏览器先询问服务器,当前网页所在的域名是否在服务器的许可名单之中,以及可以使用哪些头信息字段。只有得到肯定答复,浏览器才会发出正式的XMLHttpRequest请求,否则就报CORS错误。
下面我们发送一个预检请求。
预检请求
我们在给fetch请求添加一个自定义标头,这样就会触发预检请求。
1 <script> 2 fetch("http://127.0.0.1:3001/api", { 3 headers: { 4 xxx: "yyy", 5 }, 6 }); 7 </script>
刷新后发现报了一个CORS错误,说“Access-Control-Allow-Headers”白名单里没有“xxx”这个标头。
我们在3001端口添加一个白名单试试。
1 res.writeHead(200, { 2 "Access-Control-Allow-Origin": "*", 3 "Access-Control-Allow-Headers": "xxx", 4 });
这次浏览器发现响应标头里有一个标头白名单,预检请求通过了,然后呢向3001端口发送正式请求。
CORS策略是由请求源、请求方式、额外标头或值、本次预检请求有效期、许可证以及客户端需要的其它信息共同约束的,下面是一份CORS标头清单。
CORS标头
Access-Control-Allow-Origin | CORS请求的源白名单,要么是一个固定的源:"http://127.0.0.1:3000",要么是"*"。 |
Access-Control-Allow-Methods | 非简单请求的请求方式白名单,例如:"PUT,DELETE"。 |
Access-Control-Allow-Headers | 非简单请求的请求标头白名单,例如:"test-1,test-2"。 |
Access-Control-Max-Age |
预检请求的有效期,单位是s,在此期间,正式请求可以直接发送。 |
Access-Control-Allow-Credentials |
一个布尔值,表示是否允许发送Cookie。如果是true,就允许,如果不允许,就不需要该字段。 CORS请求默认不发送Cookie。如果要把Cookie发到服务器,开发者必须在AJAX请求中打开 需要注意的是,如果要发送Cookie, |
Access-Control-Expose-Headers |
CORS请求时, 如果想拿到其他字段,就必须在 |
Origin | CORS请求时浏览器自动添加的当前源。 |