原因

在讲解跨域的解决方案之前,应该先了解跨域是什么场景下产生的。大家都知道跨域问题是基于 JavaScript 的同源策略(同源指:同协议,同域名,同端口),而被浏览器施加的安全限制(跨域请求时,浏览器检查响应头是否具有 Access-Control-Allow-Origin 字段允许,没有则拦截响应结果,并报错),目的是保护用户信息安全。

那么为什么限制了就安全呢,危险在哪?

假设无同源策略,你在 A 网站登陆,然后再登陆 B 网站,那么 B 网站就可以读取 A 网站的 Cookie,而由于 HTTP 的无状态特性,用户登陆信息等往往储存在 Cookie 中,所以 B 网站根据这些信息可以随意盗用 A 用户账号。即无跨域保护,别人就可以非法提交信息。(注意:最初浏览器机制是,你向哪个域名发请求,就带上对应域名的 cookie)

为什么服务器之间的请求没有跨域问题呢?

因为服务器间请求使用的是 RPC 通信,或者 HTTP 限定 IP 且不执行 JavaScript。

解决方案

这里给我认为的几种最常用的方案,算是小总结。

JSONP

JSONP是一种简单方便的跨域方式,服务器改动小,几乎所有浏览器都支持。
原理是利用 html 标签的 src 属性不受同源策略限制,从而发出 GET 请求。具体代码如下

  1. function jsonp(url) {
  2. var script = document.createElement('script');
  3. script.src = url;
  4. document.body.appendChild(script);
  5. }
  6. // 调用,把回调函数拼接成参数传入
  7. jsonp('http://127.0.0.1:8080/api?callback=doSomeThing');
  8. // 回调函数
  9. function doSomeThing(data) {
  10. // do some thing
  11. }

服务器响应代码

  1. doSomeThing({
  2. name: 'xxx'
  3. })

既然是利用 src 属性,那么 img 标签的可以跨域吗?

答案是可以的,但是需要注意的是,利用 img 标签的 src 属性只能发出请求,但却无法解析服务器返回的字符串;所以一般使用 srcipt 标签

回调函数在前端代码中并没有 被调用,那它如何执行的?

这就是 JSONP 跨域一般只用 script 标签的原因,script 标签请求的脚本直接作为代码运行,就像是外联 JS 文件,只不过没有读取文件内容,而是由服务器直接返回运行代码。传的 data 对象也作为 JS 对象,而不是字符串。

CORS

跨域资源共享(Cross-Origin Resource Sharing,简称:CORS)通过设置额外的 HTTP 头来告诉浏览器 ,让运行在一个 origin (domain) 上的 Web 应用被准许访问来自不同源服务器上的指定的资源。这是 W3C 为了解决跨域请求资源而提出的标准,目前除 IE10 以下的浏览器都支持。
原理是通过设置 HTTP 头部字段(Access-Control-Allow-Origin)来让浏览器取消响应拦截,由浏览器支持和服务端改动。
CORS 请求分为简单和非简单请求,其中简单请求需要满足只使用以下字段:

  • 请求方法:
    • GET
    • HEAD
    • POST
  • 请求字段:
    • Accept
    • Accept-Language
    • Content-Language(限定为以下值)
      • text/plain
      • multipart/form-data
      • application/x-www-form-urlencoded
    • Content-Type
    • DPR
    • Downlink
    • Save-Data
    • Viewport-Width
    • Width
  • 请求中的任意 XMLHttpRequestUpload 对象均没有注册任何事件监听器
  • 请求中没有使用 ReadableStream 对象

不满足以上条件的即为非简单请求,在正式发送请求前,需要发送预检请求:

  1. // 请求方法为 options
  2. OPTIONS /resources HTTP/1.1
  3. // 是否允许 DELETE 请求方法
  4. Access-Control-Request-Method: DELETE
  5. // 是否允许设置 X-PINGOTHER 和 Content-Type 字段
  6. Access-Control-Request-Headers: X-PINGOTHER, Content-Type

服务器会返回响应报文

  1. // 一般来说设置为允许访问的域名,如果值为 * 允许所有人提交
  2. Access-Control-Allow-Origin: www.example.com (*)
  3. // 是否允许浏览器发送 cookie
  4. Access-Control-Allow-Credentials: true
  5. // 设置 Cache-Control Content-Language Content-Type Expires Last-Modified Pragma 以外的响应字段
  6. Access-Control-Expose-Headers: token
  7. // 允许非简单请求的请求方法
  8. Access-Control-Allow-Methods: GET, POST, OPTIONS
  9. // 允许非简单亲戚额外设置的请求头字段
  10. Access-Control-Allow-Headers: X-PINGOTHER, Content-Type
  11. // 响应有效时间
  12. Access-Control-Max-Age: 86400
  13. // 额外响应字段
  14. token: FJALDF4523JAOI546DFAF

注意:发送 cookie 可能需要同时将 XMLHttpRequest 请求的实例属性 withCredentials 设为 true
使用 CORS 会不会有安全问题?
这是 HTTP 协议专门用来解决跨域问题的,所以正常设置是不会有安全问题。一般来说避免将 Access-Control-Allow-Origin 设为 ,除非是 CDN 之类公共资源;如果设为 ,则 Access-Control-Allow-Credentials 值不能为 true。

postMessage

postMessage 主要用于一个 window 访问另一个 window 的情况,例如页面 A 使用 iframe 打开页面 B,然而 A,B 之间不同源,但需要通信,就可以使用 postMessage。
原理是当前 window 通过 postMessage 发送信息给目标 window,然后目标 window 通过 addEventListener 监听 message 事件,从而源 window 的信息

  1. // 页面 A, orgin: 'www.A.com'
  2. // 页面 A 通过 iframe[id="B"] 打开页面 B
  3. const B = document.querySelector('#B').contentWindow
  4. /**
  5. * 向页面 B 发送 message: hello page B
  6. * argument[0]: string | object 第一个参数是发给其他 window 的数据(message)
  7. * argument[1]: '*' | uri 第二个参数是目标 window 的 uri
  8. * argument[2]: object | false 是一串和 message 同时传递的 Transferable 对象,在此可以忽略
  9. */
  10. B.postMessage('hello page B', 'www.B.com' false)
  11. // 监听 message 事件,通过 callback 回调函数接收页面 B 发送的数据
  12. window.addEventListener('message', callback, false)
  13. function callback(event) {
  14. console.log(evnet.origin) // 页面 B 的 URI
  15. console.log(evnet.data) // 页面 B 返回的数据
  16. console.log(evnet.source) // 页面 B 的 window 对象(目标 window,用于多个 window 通信时)
  17. }
  1. // 页面 B, orgin: 'www.B.com'
  2. // 如需往页面 A 发数据
  3. window.parent.postMessage('hello page A', 'www.A.com' false)
  4. // 监听 message 事件,通过 callback 回调函数接收页面 A 发送的数据
  5. window.addEventListener('message', callback, false)
  6. function callback(event) {
  7. if (event.origin !== 'www.A.com') return // 判断通信来源
  8. console.log(event.data) // 接收页面 A 发来的数据,用来 dosomething
  9. event.source.postMessage('hello page B', event.origin) // 往源地址返回信息
  10. }

以上是简单的两个不同源页面通信 demo,需要注意:使用时一定要根据 origin 和 source 验证请求者的身份,避免安全问题

Proxy

Proxy 是通过反向代理来实现的跨域,即通过在本地建立服务器,将资源放在本地服务器上,则去除跨域的限制,然后将浏览器请求通过本地服务器转发至后端服务器而获得数据。目前在脚手架中广泛应用,实际使用可以用 Nginx 或者 Node 来代理
原理是利用服务器之间通信没有跨域限制

  1. // 使用 express 建立本地代理服务器
  2. var express = require('express');
  3. // 使用 http-proxy-middleware 代理请求
  4. var proxy = require('http-proxy-middleware');
  5. var app = express();
  6. app.use(
  7. '/api',
  8. proxy({ target: 'http://www.example.com', changeOrigin: true })
  9. );
  10. app.listen(3000);
  11. // http://localhost:3000/api/foo/bar -> http://www.example.com/api/foo/bar

目前大部分基于 Webpack 的脚手架都是使用 http-proxy-middleware 去代理转发请求
为什么服务器之间没有跨域限制?
因为跨域保护是因为浏览器的安全问题,浏览器会执行各种具有攻击性的 JavaScript 脚本。而服务器上没有执行 JavaScript 的能力,从而也不会触发攻击,仅仅是 HTTP 的请求发送,所以不会有安全问题,也就没有跨域问题

Chrome

Chrome 浏览器作为前端开发者的主流工具,他的跨域保护功能是可以关闭的,所以在日常开发中也可以通过关闭跨域来达成跨域的效果
原理是关闭浏览器跨域机制

  • 右键点击 Chrome 的桌面图标,再点击属性
  • 打开选项卡中的快捷方式,找到目标属性
  • 在目标属性后面添加以下代码

    • —disable-web-security —user-data-dir=D:\MyChromeDevUserData
    • 其中 D:\MyChromeDevUserData 可以自己定义
      1. "C:\Program Files (x86)\Google\Chrome\Application\chrome.exe" --disable-web-security --user-data-dir=D:\MyChromeDevUserData
  • —user-data-dir 后面的路径创建文件夹 D:\MyChromeDevUserData

    image.png

总结

其他跨域方式如 window.name,document.domain,甚至用 websocket 都行,没有哪种是最好的。根据不同场景灵活应用不同的方法去解决问题即可