文章首发于公众号【程序员读书】,欢迎关注。
前言
在Web应用开发过程中,前端开发的小伙伴可能经常会遇到跨域的问题,尤其是在前后端分离的情况下进行开发;那么什么是跨域呢?为什么会跨域呢?出现跨域后要怎么解决呢?
在《编程概念精讲》的第三篇文章中,我们来详细了解一下跨域的问题。
跨域的概念
什么是跨域
跨域,可以理解为跨域名访问,是指运行在浏览器中某个域名(URL)下的脚本(JavaScript)读取另一个域名(URL)在当前浏览器的存储数据(如cookie),或者在某个域名下向另一个域名下的接口发送Ajax请求。
而浏览器出于安全的考虑会限制这种跨域访问,浏览器采用的这种安全机制称为同源策略,主要限制了以下行为:
- 无法读取其他域下的
Cookie、LocalStorage和IndexDB里的数据。 - 无法读取其他域下的
DOM和JS对象无法获取。 - 无法使用Ajax向其他域名发送请求(注意:form表单请求是可以的)。
浏览器的同源策略
那么浏览器是如何判断两个URL是否为同源的呢?
所谓的同源是指两个URL的域名(host)、协议(protocol)和端口(port)均为相同,则两个URL为同源,比如下面的表格示例中,只有最后一种情况才是同源。
| 域名1 | 域名2 | 是否同源 |
|---|---|---|
| http://www.a.com/index.html | http://www.b.com/index.html | 不同源,域名不同 |
| http://www.a.com/index.html | https://www.a.com/index.html | 不同源,协议不同 |
| https://www.a.com:8080/index.html | https://www.a.com/index.html | 不同源,端口不同 |
| https://www.a.com/index.html | https://www.a.com/profile.html | 同源 |
不过,虽然同源策略为浏览器基本的安全机制,但其实并不能保证Web的百分百的安全。
虽然大部分情况下我们不需要跨域访问其他域名的接口,但如果碰到了,我们需要知道怎么解决,下面来了解一下。
如何解决跨域
解决跨域的方式有很多种,但这里我们只讲两种最常见的跨域解决的方法,即JSONP和CORS,其他的方法,有兴趣的小伙伴可以自己去找资料了解一下。
JSONP
如果向其他域名下的接口发送简单获取数据的请求,那可以使用JSONP,因为这种方式只能发送GET请求。
其实,JSONP请求本质上是利用script标签请求不受同源策略限制,从而绕过同源策略,来模拟发送AJAX请求。
提示:除了
script标签,img,link,irame等标签或者一些CSS属性也一样不受同源策略限制。
下面我们通过一个示例来了解JSONP跨域访问机制:
假设有一个A站点提供接口,将A站点部署在域名https://www.a.com之上,如下:
package main import ( "encoding/json" "net/http" ) func main() { type User struct { ID int Username string } users := map[string]*User{"1": &User{1, "小张"}, "2": &User{2, "小明"}} http.HandleFunc("/user", func(w http.ResponseWriter, r *http.Request) { r.ParseForm() uid := r.FormValue("uid") callback := r.FormValue("callback") u, ok := users[uid] if ok { r, err := json.Marshal(u) if err != nil { panic(err) } if callback != "" { c := append([]byte(callback), '(') w.Write(c) } w.Write(r) if callback != "" { w.Write([]byte(")")) } } }) http.ListenAndServe(":80", nil) }
另外有一个B站点,部署在http://www.b.com的域名之上,如下:
<html>
<head>
<title>JSONP测试</title>
<script>
function getUsername(data){
console.log(data.Username)
}
</script>
<script src="http://www.a.com/user?callback=getUsername&uid=1">
</script>
</head>
<body>
</body>
</html>
上面的示例中,B站的页面跨域访问A站点的接口,我们看到在浏览器的控制台输出了从A站点接口返回的数据,说明通过JSONP跨域访问成功了。
CORS
CORS是一个W3C标准,中文全称为跨域资源共享,英文全称为Cross-origin resource sharing,通过CORS机制,可以向跨域服务器请求Ajax请求,而不用受同源策略的限制。
与JSONP只能发送GET请求相比,CORS在服务端允许的情况下,可以发送任意请求,是通用的跨域解决方案。
CORS需要浏览器与服务器同时配合,在浏览器端,当判断时跨域访问时,会自动携带CORS机制所需要的头部信息,开发人员不需要在代码中作特殊处理,一切都是由浏览器自动完成。
CORS将HTTP请求分为两种,简单请求与非简单请求。
简单请求
满足以下条件的请求,才称为简单请求:
(1) HTTP请求方法(method)为下面三种方法之一:
- HEAD
- GET
- POST
(2)HTTP的头信息(header)不超出以下几种字段:
- Accept
- Accept-Language
- Content-Language
- Last-Event-ID
- Content-Type:只限于三个值
application/x-www-form-urlencoded、multipart/form-data、text/plain
简单请求的流程:
- 浏览器发现请求跨域时,在发送请求的头部中带上
Origin,指明当前见面的源(协议+地址+端口),比如:
GET /cors HTTP/1.1
Origin: http://www.a.com
Host: www.a.com
Accept-Language: en-US
Connection: keep-alive
User-Agent: Mozilla/4.0(compatible;MSIE 5.5;Windows NT 5.0).
服务器在收到请求后,如果
Origin所指定的域名不被允许请求数据,而返加一个正常的请求,浏览器判断服务器服务器不允许跨域请求,会抛出一个错误,这个错误会被XMLHttpRequest的回调函数onerror捕获并处理。如果服务器允许
Origin所指定的域名的请求,则服务器在正常返回数据的同时,还会返回下面以的头部信息,浏览器判断有以下头部字段,就进入正常的XMLHttpRequest回调处理函数。
HTTP/1.0 200 OKAccess-Control-Allow-Origin:* Access-Control-Allow-Credentials:false Access-Control-Expose-Headers:SetValue
Content-Type:application/json;charset=utf-8
Access-Control-头部字段说明
与CORS跨域相关的字段均以Access-Control-开头,我们来看一下这几个字段的作用:
- Access-Control-Allow-Origin
这个字段是必填的,其值或为浏览器所携带的Origin值,或者为*,表示允许所有域名的请求,浏览器就是通过服务器是否返回该字段来判断服务器是否允许跨域请求的。
- Access-Control-Allow-Credentials
这个字段是可选的,表示是否允许浏览器发送cookie数据,其值为布尔类型,true表示允许发送,false为不允许发送。
不过,即便服务器设置该字段为true,在发送Ajax请求时,也需要设置withCredentials属性,浏览器才会向服务器发送cookie数据,如:
var xhr = new XMLHttpRequest();
xhr.withCredentials = true;
而如果要发送cookie数据,则Access-Control-Allow-Origin不能设为*,而是必须与Orgin保持一致。因为,cookie还是受同源策略的限制,只有用服务器域名设置的cookie才会上传,其他域名的cookie并不会上传。
另外(跨源)原网页代码中的document.cookie也无法读取服务器域名下的cookie。
- Access-Control-Expose-Headers
这个字段是可选的,如果你不需要获取什么特殊头部字段,则不需要设置,因为通过XMLHttpRequest请求发送CORS请求时,getResponseHeader()方法只能获取到Cache-Control、Content-Language、Content-Type、Expires、Last-Modified、Pragma六个字段。
这时候如果如果想要获取服务器返回的其他字段,就需要在Access-Control-Expose-Headers里面指定。比如像我们上面的示例中,getResponseHeader('SetValue')可以返回SetValue字段的值。
上面的简单请求的大概流程图如下所示::
非简单请求
不满足简单请求条件的,则为非简单请求,比如说HTTP请求方法为PUT或Content-Type的值为application/json等情况都是非简单请求。
非简单请求相比于简单请求,分为两步:预检请求与正常请求。
预检请求
请求流程:
当浏览器发现Ajax请求为CORS的非简单请求时,会在正常请求前发起一个预检请求,用于判断服务器是否允许跨请求且能接收额外的头部信息,一般会多带上以下几个头部字段:
Origin:http://www.a.com
Access-Control-Request-Method:PUT
Access-Control-Request-Headers:Put-User-Header
- Access-Control-Request-Method
为必填字段用于告诉服务器该跨域请求会用到哪些HTTP方法,比如我们上面的例子为PUT方法。
- Access-Control-Request-Headers
为非必填,用于告诉服务器此时请求会带上哪些额外的头部字段信息。
响应流程
如果服务器不允许该请求,则只是返回正常的HTTP响应,此时浏览器会报错,如果允许,则会返回下面的字段:
Access-Control-Allow-Origin: *
Access-Control-Allow-Methods: GET, POST, PUT
Access-Control-Allow-Headers: Put-User-Header
Access-Control-Allow-Credentials:true
Access-Control-Max-Age:86400
- Access-Control-Allow-Methods
服务器允许哪些请求方法,可以返回多个,这样可以避免多次预检请求。
- Access-Control-Allow-Headers
说明服务器支持的所有HTTP头部字段,以逗号分隔。
- Access-Control-Max-Age
预检请求的过期时间,如果未过期,则在此期间所有跨域请求均不需要再发起预检请求。
正常请求
一旦经过预检请求,浏览器已经确定服务器允许跨域请求,那么之后的每一次跨域请求都可以正常发起,而不需要再发起预检求(如果服务器有返回Access-Control-Max-Age,那么过期后,还需要再发起预检请求),正常请求的过程与上面的简单请求是一样的。
上面所述非简单请求的流程大概如下所示:
小结
看到这里,我们知道跨域是由于浏览器的同源策略引起的,而解决的方法也挺多的,不过我们这里介绍两种用得比较多的,JSONP和CORS,这两种方式都是绕过同源策略,所不同的是JSONP只能发送GET请求,而CORS则比较强大,CORS根据请求的类型与是否携带自定义头部信息而分为简单请求和简单请求两种,且CORS的方式需要服务器端的配合。
求关注
码字不易,求扫码关注公众号,作者写作更有动力!