2000字谈谈跨域

219 阅读6分钟

1.何为跨域以及同源策略

跨域指的是一个域下的文档或者脚本试图去请求另外一个域下的资源。而浏览器出于安全考虑做了同源策略的限制。同源策略限制指的是请求端和被请求端不同时具备协议、域名、端口相同,则受限。受限并不是不能够发送请求,而是请求响应的数据被浏览器拦截了。

同源策略限制的行为

  • Cookie、LocalStorage 和 IndexDB无法读取
  • DOM 和 Js对象无法获得
  • AJAX 请求不能发送

2.解决方案

无论是什么样子的跨域资源解决方案,本质上都是需要服务端的支持。跨域获取资源之所以能够成功,本质是服务器默许了你有权限获取相应资源。下面我们所运用的种种方式,实际上是客户端和服务端互相配合。

2.1 JSONP

浏览器的“同源策略”只是阻止了通过AJAX技术跨域获取资源,而并没有禁止跨域获取资源这件事本身,正因如此,我们可以通过<link>标签,<img>标签以及<script>标签中的href属性或src属性获取异域的CSSJS资源和图片。<script>标签通过src属性加载的JS资源,会与HTML文件下的其他JS文件共享一个全局作用域。也就是说<script>标签加载到的资源可以被全局作用域下的函数所使用。

下面我来举个例子:

请求端
--index.html---
<script src="/hack.js"></script>     //先引入js文件

---hack.js---
let random=Math.random();     //为了避免冲突,生成一个随机数作为全局对象的属性名。
window[random]=(data)=>{      //定义一个全局函数
    conssole.log(data)
};

let script=document.createElement('script')
script.type="text/javascript';
script.src=`http://qq网站/share.js?callback=${random}`      //将定义的参数传给要接收端。
document.body.appendChild(script);        
响应端

---server.js---
if(path==='/share.js'){
    response.statusCode=200   
    response.setHeader('Content-Type','text/javascript;charset=utf-8')
   let query=query.callback;      //能够查询到访问这个网站的查询参数。
    let string=fs.readFileSync('public/share.js').toString(),
    let data=fs.readFileSync('public/data.json').toString(),
    let string2=string.replace('{{data}}',data).replace('{{xxx}}',query);
    response.write(string2)
    response.end()
}

---share.js---
window['{{xxx}}']('{{data}}')

在请求端定义一个函数,然后响应端调用该函数,利用script标签引入文件,两个文件在同一个全局作用域,达到了跨域的目的。

强调: JSONP技术与AJAX技术无关,虽然同样牵扯到跨域获取资源这个主题,但我们应该已经清楚的看到,JSONP的本质是绕过AJAX获取资源的机制,使用原始的src属性获取异域资源;jsonp也有一个弊端,就是无法发送POST请求,也就是说JSONP技术只能用于请求异域资源,无法上传数据或修改异域数据;

2.2 CORS

利用CORS实现AJAX跨域获取资源,重点在于服务器返回的响应是否清楚的告诉浏览器此次跨域请求的合法性。

对于简单的请求,服务器端需要向浏览器做出的说明就少,而对于复杂的请求,服务器端需要向浏览器做出的说明就多。那如何区别请求的复杂程度呢。

简单请求

只要同时满足一下两个条件,就属于简单请求: 条件一:使用下面方法之一:

  • \color{red}{GET}
  • \color{red}{HEAD}
  • \color{red}{POST}

条件二:HTTP头信息只限于以下字段:

  • \color{red}{Accept}
  • \color{red}{Accept-Language}
  • \color{red}{Content-Language}
  • \color{red}{Last-Event-ID}
  • \color{red}{Content-Type}(只能为application/x-www-form-urlencodedmultipart/form-datatext/plain其中一种)

当浏览器检测到一个简单的跨域AJAX请求,浏览器会自动为我们添加一个头部信息:Origin它的值为请求发送代码所在的源;

GET /cors HTTP/1.1
Origin: http://api.men.com
Host: api.alice.com
Accept-Language: en-US
Connection: keep-alive

服务器根据origin字段的值来决定是否同意这次请求。

如果origin不在许可范围内,服务器返回正常的HTTP响应,但是这次响应没有包含Access-Control-Allow-Origin字段,所以浏览器就知道出错,便抛出错误。

如果origin指定的域名在许可范围内,则服务器返回的响应,会多出几个头部信息。

Access-Control-Allow-Origin: http://api.men.com   
Access-Control-Allow-Credentials: true
Access-Control-Expose-Headers: FooBar
Content-Type: text/html; charset=utf-8
  • \color{red}{Access-Control-Allow-Origin}(必须):该字段用来告知浏览器服务端接受的能够发送跨域AJAX请求的域。
  • \color{red}{Access-Control-Allow-Credentials}(可选):该字段用来告知浏览器是否允许客户端向服务端发送Cookie
  • \color{red}{Access-Control-Expose-Headers}(可选):该字段用来向客户端暴露可获取的响应头;

注意:默认情况下不允许跨域AJAX请求携带cookie,如果需要携带cookie,必须前后端都设置。

客户端:

var xhr = new XMLHttpRequest()
xhr.withCredentials = true

服务器端:

Access-Control-Allow-Credentials: true

服务器端除了设置该字段以外,Access-Control-Allow-Origin字段还需要设置一个明确的域,不可以再使用*号。

复杂请求

复杂的AJAX跨域请求一共会发送两次HTTP请求,第一次为预检请求,第二次为正式的AJAX跨域请求。

var url = 'http://api.macycle.com/cors'
var xhr = new XMLHttpRequest()
xhr.open('put', url, true) // 这里我们设置请求的方式为'put'
xhr.setRequestHeader('X-Custom-Header', 'Value') // 这里我们自定义了一个请求头字段
xhr.send()

HTTP请求方法属于非简单的请求,浏览器发现之后,就先自动的发一个预检请求,要求服务器确认是否允许这样的请求。预检请求的头信息如下:

OPTIONS /cors HTTP/1.1
Origin: http://api.men.com     //请求发出的地址
Access-Control-Request-Method: PUT
Access-Control-Request-Headers: X-Custom-Header    //指定浏览器CORS请求会额外发送的头信息字段
Host: api.macycle.com
Accept-Language: en-US
Connection: keep-alive
User-Agent: Mozilla/5.0...

服务器收到预检请求之后,检查了originAccess-Control-Request-MethodAccess-Control-Request-Headers字段以后,确认允许跨源请求,就可以做出回应。

HTTP/1.1 200 OK
Date: june, 01 Dec 2020 01:15:39 GMT
Server: Apache/2.0.61 (Unix)
Access-Control-Allow-Origin: http://api.macycle.com    //允许该网址可以跨域请求
Access-Control-Allow-Methods: GET, POST, PUT
Access-Control-Allow-Headers: X-Custom-Header
Content-Type: text/html; charset=utf-8
Content-Encoding: gzip
Content-Length: 0
Keep-Alive: timeout=2, max=100
Connection: Keep-Alive
Content-Type: text/plain

服务器通过了预检请求,一会后每次服务器正常的cors请求就跟简单请求一样了。

总结

  • CORS支持所有类型的HTTP请求,是跨域HTTP请求的根本解决方法。
  • JSONP只支持GET请求,JSONP的优势在于支持老式浏览器,以及可以向不支持CORS的网站请求数据

参考资料:

阮一峰跨域资源共享 CORS 详解

MDN HTTP访问控制(CORS)

跨域获取资源的方法