本文作者为口袋前端团队—朱未
什么是跨域
出于安全因素考虑,浏览器有同源策略限制,即限制从一个源加载的文档或脚本如何与来自另一个源的资源进行交互。如果协议、端口和主机只要有一个不同,则认为跨域。当然,跨域并不一定是浏览器限制了发起跨站请求,也可能是跨站请求可以正常发起,但是返回结果被浏览器拦截了。
跨域的分类
主域相同的跨域和完全不同源的跨域。
主域相同的跨域
这种情况通常会出现在iframe
标签中,解决的方法是使用document.domain
,这种方式只适合不同子域框架间的交互,及主域必须相同的不同源。
document.domain = 'targetDomain'
完全不同源的跨域
方法有很多,比如location.hash, window.name, window.postMessage
,这些方法不太常用,具体可以看这里
Ajax跨域请求
JSONP
利用了script
标签可以跨域请求资源的特性。这个方法以前用的比较多,因为兼容性好,但是缺点也很明显,只能发get
请求。
WebSocket
这个协议不实行同源政策,只要服务器支持,就可以通过它进行跨域。
CORS
这是目前最通用的跨域方式。
由于前后端分离开发,跨域问题经常遇到,为了使开发便捷,隶属于 W3C 的 Web 工作组推荐了一种新的机制,即跨域资源共享(Cross-origin Resource Sharing)
优点:
- 兼容性良好
- 只需服务端添加几行代码,前端不需要修改
跨域资源共享标准允许在下列场景中使用跨域 HTTP 请求:
- 由XMLHttpRequest
和Fetch
发起的跨域 HTTP 请求
- Web 字体( CSS 中通过@font-face
使用跨域字体资源)
- WebGL 贴图
- 使用drawImage
将Images/video
会面绘制到canvas
- 样式表
- 脚本
两种请求
CORS 的请求分两种,这也是浏览器为了安全做的一些处理,不同情况下浏览器执行的操作也是不一样的,主要分为两种请求,简单请求和复杂请求。
简单请求
判断简单请求有两个条件,只要同时满足就是简单请求。
1. 请求是下列之一:
HEAD
GET
POST
2. HTTP的头信息不超出以下几种字段:
Accept
Accept-Language
Content-Language
Last-Event-ID
Content-Type:只限于三个值application/x-www-form-urlencoded、multipart/form-data、text/plain
对于简单的跨域请求,浏览器会自动在请求的头信息加上Origin
字段,表示本次请求来自哪个源(协议 + 域名 + 端口),服务端会获取到这个值,然后判断是否同意这次请求并返回。
解决方案
因此只要服务端在header
中设置Access-Control-Allow-Origin
的值为*
或是origin
中指明的站点即可。
复杂请求
如果不是简单请求,那就是复杂请求,比如请求的方法是 PUT 或者 DELETE ,比如Content-Type
字段的类型是application/json
,比如设置了自定义头信息。
称之为复杂请求,主要是因为他比简单请求多了个预检请求。
预检请求
非简单请求的 CORS 请求,会在正式通信之前,增加一次 HTTP 查询请求,称为预检请求(preflight)。
浏览器先询问服务器,当前网页所在的域名是否在服务器的许可名单之中,以及可以使用哪些 HTTP 动词和头信息字段。只有得到肯定答复,浏览器才会发出正式的XMLHttpRequest
请求,否则就报错。
预检请求用的请求方法是 OPTIONS ,表示这个请求是用来询问的。头信息里面,关键字段是 Origin ,表示请求来自哪个源。除了origin
字段,还有两个字段非常重要:Access-Control-Request-Method
和Access-Control-Request-Headers
。分别表示允许的请求方法和请求头。
解决方案
因此对于复杂请求的解决方案就是在响应头中设置来自浏览器的请求方法和请求头。
例如前端发起的请求如下:
$.post({
url: 'http://192.168.1.130:8082/UserWebServer/sms/sendPhoneCode',
data: {
type: 'reset',
phone: $('#tele').val(),
captcha: $('#pic').val(),
},
success: function (res) {
console.log(res);
if (res.status === 1007) {
toast(res.desc)
$('#pic').val('');
picVerifyCode()
} else if (res.status === 1012) {
toast(res.desc)
picVerifyCode()
} else if (res.status === 1000) {
toast('验证码发送成功!')
alert(res.desc)
}
},
error: function (e) {
toast('网络错误,请稍后再试!')
},
beforeSend: function (xhr) {
xhr.setRequestHeader("platform", "web");
xhr.setRequestHeader("imei", result);
},
})
以java
为例,后端的代码为
httpServletResponse.setHeader("Access-Control-Allow-Origin","*");
httpServletResponse.setHeader("Access-Control-Allow-Methods","POST,GET,OPTIONS");
httpServletResponse.setHeader("Access-Control-Allow-Headers","imei,platform");
除此之外,还有两个比较重要的属性,分别是Access-Control-Max-Age
和Access-Control-Allow-Credentials
。
Access-Control-Max-Age
指定了preflight
请求的结果能被缓存多久,单位为秒。在此期间,不用发出另一条预检请求。
Access-Control-Allow-Credentials
指定了当浏览器的credentials
设置为true
时是否允许浏览器读取response
的内容。当用在对preflight
预检测请求的响应中时,它指定了实际的请求是否可以使用credentials
。请注意:简单 GET 请求不会被预检;如果对此类请求的响应中不包含该字段,这个响应将被忽略掉,并且浏览器也不会将相应内容返回给网页。并且,对于附带身份凭证的请求,服务器不得设置 Access-Control-Allow-Origin
的值为*
,必须设置为具体站点。
目前利用 CORS 解决跨域是认同度较高的解决跨域问题的最佳实践,关于 CORS 更多的信息查看这里
纯前端解决跨域问题
其实只是用nodejs
来解决问题,还是服务端的知识。比如 VUE 的脚手架工具就提供了跨域的解决方法。
// config/index.js
module.exports = {
// ...
dev: {
proxyTable: {
// proxy all requests starting with /api to jsonplaceholder
'/api': {
target: 'http://jsonplaceholder.typicode.com',
changeOrigin: true,
pathRewrite: {
'^/api': ''
}
}
}
}
}
上面的例子会将/api/posts/1
代理到http://jsonplaceholder.typicode.com/posts/1
关于 VUE 脚手架配置跨域的详细内容看这里,VUE 脚手架其实是用到了http-proxy-middleware
,具体可以查看它的github