一、跨域问题的由来
同源策略(Same origin policy)是一种约定,它最早由浏览器厂商Netscape公司提出,现在已经成为浏览器最基础最核心的安全功能。所谓同源(即指在同一个域)就是两个页面的具有相同的协议(protocol),主机(host)和端口号(port),例如http://a.com与https://a.com就不是同源的,因为协议不同。
浏览器从一个域名的网页去请求另一个域名的资源时,域名、端口、协议任一不同,都是跨域。
在前后端分离的模式下,前后端的域名是不一致的,此时就会发生跨域访问问题。
同源策略主要限制了三个方面:
- 当前域下的 js 脚本不能够访问其他域下的 cookie、localStorage 和 indexDB。
- 当前域下的 js 脚本不能够操作访问操作其他域下的 DOM。
- 当前域下 ajax 无法发送跨域请求。
但是有三个标签是允许跨域加载资源:
<img src='xxx'>
<link href='xxx'>
<script src='xxx'>
跨域并不是请求发不出去,请求能发出去,服务端能收到请求并正常返回结果,只是结果被浏览器拦截了。
二、跨域的解决办法
常见的解决方案有很多种,比如:
- JSONP跨域
- 跨域资源共享 CORS
- document.domain + iframe 跨域解决方案
- window.name + iframe 跨域解决方案
- location.hash + iframe 跨域解决方案
- postMessage跨域解决方案
- WebSocket协议跨域解决方案
- node代理跨域解决方案
- nginx代理跨域解决方案
在这些众多的解决方案中,最常用的是JSONP和CORS,其中比较传统的跨域解决方案是 JSONP。JSONP 虽然能解决跨域问题,但是有一个很大的局限性,那就是 只支持 GET 请求,而不支持其他类型的请求,在 RESTful 时代几乎就没什么用。
另外一种常见的解决方案是 CORS(跨域源资源共享,Cross-Origin Resource Sharing),它是一个 W3C 标准,或者说是一种针对浏览器的技术规范,提供了 Web 服务从不同网域传来沙盒脚本的方法,以避开浏览器的同源策略,进而实现跨域访问。
我这里主要是在Golang项目中利用CORS技术,来解决跨域方案。
三、CORS概述
CORS是一个W3C标准,全称是"跨域资源共享"(Cross-origin resource sharing)。 它允许浏览器向跨源服务器,发出XMLHttpRequest请求,从而克服了AJAX只能同源使用的限制。
CORS需要浏览器和服务器同时支持。目前,所有浏览器都支持该功能,IE浏览器不能低于IE10。IE8+:IE8/9需要使用XDomainRequest对象来支持CORS。
整个CORS通信过程,都是浏览器自动完成,不需要用户参与。对于开发者来说,CORS通信与同源的AJAX通信没有差别,代码完全一样。浏览器一旦发现AJAX请求跨源,就会自动添加一些附加的头信息,有时还会多出一次附加的请求,但用户不会有感觉。 因此,实现CORS通信的关键是服务器。只要服务器实现了CORS接口,就可以跨域通信。
浏览器将CORS请求分成两类:简单请求和非简单请求。
简单请求
若请求满足所有下述条件,则该请求可视为简单请求:
- 请求方法是以下三种方法之一:HEAD、GET、POST。
- 除了被用户代理自动设置的头字段(例如 Connection、User-Agent等),HTTP的头信息不超出以下几种字段:Accept、Accept-Language、Content-Language、Last-Event-ID、Content-Type。
- Content-Type标头所指定的媒体类型的值仅限于下列三者之一:
- text/plain
- multipart/form-data
- application/x-www-form-urlencoded
- 如果请求是使用 XMLHttpRequest 对象发出的,在返回的 XMLHttpRequest.upload 对象属性上没有注册任何事件监听器;也就是说,给定一个 XMLHttpRequest 实例 xhr,没有调用 xhr.upload.addEventListener(),以监听该上传请求。
- 请求中没有使用 ReadableStream对象。
简单请求基本流程
- 对于简单请求,浏览器直接发出CORS请求。具体来说,就是在发送AJAX请求的1时候,浏览器如果发现是跨域的请求,浏览器会自动在请求中增加一个Origin字段。 Origin字段用来说明,本次请求来自哪个源(协议 + 域名 + 端口)。服务器根据这个值,决定是否同意这次请求。
-
GET /cors HTTP/1.1 Origin: http://api.bob.com Host: api.alice.com Accept-Language: en-US Connection: keep-alive User-Agent: Mozilla/5.0 ...
- 如果Origin指定的源,不在许可范围内,服务器会返回一个正常的HTTP回应。当浏览器接收到服务器的响应之后会去响应头查找Access-Control-Allow-Origin字段,如果没有则会报错。抛出的错误会被XMLHttpRequest的onerror回调函数捕获。
- 如果Origin指定的域名在许可范围内,服务器返回的响应,会多出几个头信息字段。
字段 | 必须/可选 | 解释 |
---|---|---|
Access-Control-Allow-Origin | 必须 | 允许跨域的源,要么是一个*,表示接受任意域名的请求。 |
Access-Control-Allow-Credentials | 可选 | 表示服务器是否允许客户端发送Cookie。默认情况下,Cookie可以包含在请求中,一起发给服务器,如果服务器不需要浏览器发送Cookie,删除该字段即可。 |
Access-Control-Expose-Headers | 可选 | CORS请求时,XMLHttpRequest对象的getResponseHeader()方法只能拿到6个基本字段:Cache-Control、Content-Language、Content-Type、Expires、Last-Modified、Pragma。如果想拿到其他字段,就必须在Access-Control-Expose-Headers里面指定。如指定Access-Control-Expose-Headers: FooBar,则可通过getResponseHeader(‘FooBar’)获取FooBar字段的值。 |
非简单请求
非简单请求是那种对服务器有特殊要求的请求,比如请求方法是PUT或DELETE,或者Content-Type字段的类型是application/json。
预检请求
非简单请求的CORS请求,会在正式通信之前,增加一次HTTP查询请求,称为"预检"请求(preflight)。
浏览器先询问服务器,当前网页所在的域名是否在服务器的许可名单之中,以及可以使用哪些HTTP动词和头信息字段。只有得到肯定答复,浏览器才会发出正式的XMLHttpRequest请求,否则就报错。
"预检"请求用的请求方法是OPTIONS,表示这个请求是用来询问的。头信息里面,关键字段是Origin,表示请求来自哪个源。
除了Origin字段,"预检"请求的头信息包括两个特殊字段。
- Access-Control-Request-Method:该字段是必须的,用来列出浏览器的CORS请求会用到哪些HTTP方法,上例是POST。
- Access-Control-Request-Headers:该字段是一个逗号分隔的字符串,指定浏览器CORS请求会额外发送的头信息字段,上例是Authorization和Content-Type
预检请求的回应
服务器收到"预检"请求以后,检查了Origin、Access-Control-Request-Method和Access-Control-Request-Headers字段以后,确认允许跨源请求,就可以做出回应
浏览器正常请求回应
一旦服务器通过了"预检"请求,以后每次浏览器正常的CORS请求,就都跟简单请求一样,会有一个Origin头信息字段。服务器的回应,也都会有一个Access-Control-Allow-Origin头信息字段。
四、实战案例
这里以作者自己开发的前后端项目为例。
简单请求
我们可以看到在没有任何跨域配置的情况下跨域访问失败。
简单请求跨域配置
这里我们使用Gin官方提供的cors解决方案gin-contrib/cors:
package middleware
import (
"github.com/gin-contrib/cors"
"github.com/gin-gonic/gin"
)
// Cors 跨域配置
func Cors() gin.HandlerFunc {
config := cors.DefaultConfig()
config.AllowMethods = []string{"GET"}
config.AllowHeaders = []string{"Origin"}
config.AllowOrigins = []string{"http://localhost:8080"}
config.AllowCredentials = true
return cors.New(config)
}
重新运行服务器,发起请求:
可以看到响应包中增加了加了几个字段
响应数据也能正常接收到了。
复杂请求
使用POST方法向服务器发出点赞请求,可以看到浏览器发出了预检请求。
复杂请求跨域配置
我们根据请求头和报错信息对cor中间件的配置进行相应修改:
package middleware
import (
"github.com/gin-contrib/cors"
"github.com/gin-gonic/gin"
)
// Cors 跨域配置
func Cors() gin.HandlerFunc {
config := cors.DefaultConfig()
config.AllowMethods = []string{"GET", "POST"}
config.AllowHeaders = []string{"Origin", "Content-Type", "Authorization"}
config.AllowOrigins = []string{"http://localhost:8080"}
config.AllowCredentials = true
return cors.New(config)
}
重新运行服务器,发起请求:
可以看到第二次的预检请求和发送请求都操作成功了。