原因
在讲解跨域的解决方案之前,应该先了解跨域是什么场景下产生的。大家都知道跨域问题是基于 JavaScript 的同源策略(同源指:同协议,同域名,同端口),而被浏览器施加的安全限制(跨域请求时,浏览器检查响应头是否具有 Access-Control-Allow-Origin 字段允许,没有则拦截响应结果,并报错),目的是保护用户信息安全。
那么为什么限制了就安全呢,危险在哪?
假设无同源策略,你在 A 网站登陆,然后再登陆 B 网站,那么 B 网站就可以读取 A 网站的 Cookie,而由于 HTTP 的无状态特性,用户登陆信息等往往储存在 Cookie 中,所以 B 网站根据这些信息可以随意盗用 A 用户账号。即无跨域保护,别人就可以非法提交信息。(注意:最初浏览器机制是,你向哪个域名发请求,就带上对应域名的 cookie)
为什么服务器之间的请求没有跨域问题呢?
因为服务器间请求使用的是 RPC 通信,或者 HTTP 限定 IP 且不执行 JavaScript。
解决方案
这里给我认为的几种最常用的方案,算是小总结。
JSONP
JSONP是一种简单方便的跨域方式,服务器改动小,几乎所有浏览器都支持。
原理是利用 html 标签的 src 属性不受同源策略限制,从而发出 GET 请求。具体代码如下
function jsonp(url) {var script = document.createElement('script');script.src = url;document.body.appendChild(script);}// 调用,把回调函数拼接成参数传入jsonp('http://127.0.0.1:8080/api?callback=doSomeThing');// 回调函数function doSomeThing(data) {// do some thing}
服务器响应代码
doSomeThing({name: 'xxx'})
既然是利用 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 对象
不满足以上条件的即为非简单请求,在正式发送请求前,需要发送预检请求:
// 请求方法为 optionsOPTIONS /resources HTTP/1.1// 是否允许 DELETE 请求方法Access-Control-Request-Method: DELETE// 是否允许设置 X-PINGOTHER 和 Content-Type 字段Access-Control-Request-Headers: X-PINGOTHER, Content-Type
服务器会返回响应报文
// 一般来说设置为允许访问的域名,如果值为 * 允许所有人提交Access-Control-Allow-Origin: www.example.com (*)// 是否允许浏览器发送 cookieAccess-Control-Allow-Credentials: true// 设置 Cache-Control Content-Language Content-Type Expires Last-Modified Pragma 以外的响应字段Access-Control-Expose-Headers: token// 允许非简单请求的请求方法Access-Control-Allow-Methods: GET, POST, OPTIONS// 允许非简单亲戚额外设置的请求头字段Access-Control-Allow-Headers: X-PINGOTHER, Content-Type// 响应有效时间Access-Control-Max-Age: 86400// 额外响应字段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 的信息
// 页面 A, orgin: 'www.A.com'// 页面 A 通过 iframe[id="B"] 打开页面 Bconst B = document.querySelector('#B').contentWindow/*** 向页面 B 发送 message: hello page B* argument[0]: string | object 第一个参数是发给其他 window 的数据(message)* argument[1]: '*' | uri 第二个参数是目标 window 的 uri* argument[2]: object | false 是一串和 message 同时传递的 Transferable 对象,在此可以忽略*/B.postMessage('hello page B', 'www.B.com', false)// 监听 message 事件,通过 callback 回调函数接收页面 B 发送的数据window.addEventListener('message', callback, false)function callback(event) {console.log(evnet.origin) // 页面 B 的 URIconsole.log(evnet.data) // 页面 B 返回的数据console.log(evnet.source) // 页面 B 的 window 对象(目标 window,用于多个 window 通信时)}
// 页面 B, orgin: 'www.B.com'// 如需往页面 A 发数据window.parent.postMessage('hello page A', 'www.A.com', false)// 监听 message 事件,通过 callback 回调函数接收页面 A 发送的数据window.addEventListener('message', callback, false)function callback(event) {if (event.origin !== 'www.A.com') return // 判断通信来源console.log(event.data) // 接收页面 A 发来的数据,用来 dosomethingevent.source.postMessage('hello page B', event.origin) // 往源地址返回信息}
以上是简单的两个不同源页面通信 demo,需要注意:使用时一定要根据 origin 和 source 验证请求者的身份,避免安全问题
Proxy
Proxy 是通过反向代理来实现的跨域,即通过在本地建立服务器,将资源放在本地服务器上,则去除跨域的限制,然后将浏览器请求通过本地服务器转发至后端服务器而获得数据。目前在脚手架中广泛应用,实际使用可以用 Nginx 或者 Node 来代理
原理是利用服务器之间通信没有跨域限制
// 使用 express 建立本地代理服务器var express = require('express');// 使用 http-proxy-middleware 代理请求var proxy = require('http-proxy-middleware');var app = express();app.use('/api',proxy({ target: 'http://www.example.com', changeOrigin: true }));app.listen(3000);// 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 可以自己定义
"C:\Program Files (x86)\Google\Chrome\Application\chrome.exe" --disable-web-security --user-data-dir=D:\MyChromeDevUserData
—user-data-dir 后面的路径创建文件夹 D:\MyChromeDevUserData

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