什么是跨域呢?
在了解跨域之前,应该先了解一下什么叫做同源策略。
同源策略指的是:浏览器在安全层面的考虑,只允许本域下的接口进行交互。不同源的客户端请求不能够获取到对方的资源。
这是一种典型的防止CSRF攻击的手段。下面进行解释一下:
当你访问了淘宝,登上了账号并且准备买东西的时候。你不小心点到了其他网页去,这个时候你是已经在淘宝登录了你的账号,那么其他页面也是能够获得你的淘宝信息的。
假如有一些非法分子利用漏洞将你的信息全部盗走,然后模拟你的信息进行消费岂不是酿成了大祸。
因此,为了防止这种情况的发生,浏览器是禁止跨域进行资源访问的。
本域是指:协议、域名、端口都相同。三者有一个不同那么就是跨域。下面举个例子(图片转载链接文章已给出)。
如果不解决跨域问题,那么就无法获取交互资源,出现报错。
如何解决跨域问题?
- Cors跨域中间件
- JsonP
- Nginx代理
Cros
什么是Cors呢?
Cors跨域资源分享(Cross Origin Resource Share),其实就是在浏览器中加上几个响应头,来让浏览器能够跨域访问资源。
这个响应头的设置就是:Access-Control-Allow-Origin: *
,*就是允许所有的域名跨域。
预请求和Options
什么是预请求?为什么要使用预请求呢?
当一个请求跨域的目标不是简单请求的时候就会发起预请求,也就是 Options
。如果没有预请求,假如一个毁灭性的Post请求发来,直接进行处理,处理完成之后返回的时候,浏览器虽然能告诉你你没有权限跨域访问资源又给你拦截起来了,但Post请求的目的已经达成了。
以下条件构成了简单请求:
-
Method
: 请求的方法是GET
、POST
及HEAD
-
Header
: 请求头是Content-Type
(有限制)、Accept-Language
、Content-Language
等 -
Content-Type
: 请求类型是application/x-www-form-urlencoded
、multipart/form-data
或text/plain
非简单请求一般需要开发者主动构造,在项目中常见的 Content-Type: application/json
及 Authorization: <token>
为典型的非简单请求。与之有关的三个字段如下:
-
Access-Control-Allow-Methods
: 请求所允许的方法, 用于预请求 (preflight request) 中 -
Access-Control-Allow-Headers
: 请求所允许的头,用于预请求 (preflight request) 中 -
Access-Control-Max-Age
: 预请求的缓存时间
Cors对应的响应头都有哪些?
-
Access-Control-Allow-Origin
:允许资源共享的域名 (*表示所有) -
Access-Control-Allow-Credentials
:请求是否可以带有Cookie -
Access-Control-Allow-Methods
:请求所允许的方法,用于预请求中 -
Access-Control-Allow-Headers
:请求所允许的头,用于预请求中 -
Access-Control-Max-Age
:预请求的缓存时间 -
Access-Control-Expose-Headers
:哪些头可以在响应中暴露
简单请求过程
对于一个简单请求,浏览器不用发出预请求,直接发出CORS请求。其实就是在头部加一个Origin
字段。
下面是一个例子:
GET /test HTTP/1.1
Origin: http://www.xxxxx.com
Accept-Language: en-US
Connection: keep-alive
User-Agent: Mozilla/5.0...
HTTP报文中,这个Origin就包括了本域内容:协议http + 域名 xxxx + 端口号 80。服务器根据这个Origin提供的信息来决定是否同意这次请求。
如果Origin指定的源不在许可范围之内,那么服务器就会返回一个正常的HTTP响应。如果浏览器发现这个响应不带有Access-Control-Allow-Origin
这个首部字段,就会抛出一个XMLHttpRequest
的错误。而状态码依旧是200。
如果Origin可以被通过,那么他将会收到一些额外的响应头
Access-Control-Allow-Origin: http://www.xxxxx.com
Access-Control-Allow-Credentials: true - 是否携带cookie
Access-Control-Expose-Headers: FooBar - 允许暴露的首部
Content-Type: text/html; charset=utf-8
非简单请求过程
非简单请求是那种对服务器有特殊要求的请求,比如请求方法是PUT
或DELETE
,或者Content-Type
字段的类型是application/json
。
非简单请求的CORS请求,会在正式通信之前,增加一次HTTP查询请求,称为"预检"请求(preflight)。
浏览器先询问服务器,当前网页所在的域名是否在服务器的许可名单之中,以及可以使用哪些HTTP动词和头信息字段。只有得到肯定答复,浏览器才会发出正式的XMLHttpRequest
请求,否则就报错。
Cors如何设置多域名?
Cors的核心就在于Access-Control-Allow-Origin
这个字段。只要设置了他基本就能解决跨域问题。而这个字段的值只有两种情况:
-
*
:所有域名 -
xxx.com
:特定域名
如果使用*
,是无法携带Cookie的。
- 如果这个请求的域名携带了
Origin
,说明他是跨域请求,根据Origin设置相应的首部字段。 - 如果这个请求的域名没有携带
Origin
,说明他不是跨域请求,那就直接放行。
如何避免 CDN 为 PC 端缓存移动端页面
假如有两个域名访问同一个跨域资源,由于缓存问题,因此返回的Origin会不同。因此就需要用到Vary
,代表不同的Origin缓存不同的资源。
中间件代码 -> 这里用的Gin框架
func Cors(c *gin.Context) {
method := c.Request.Method
header := c.Writer.Header()
origin := c.Request.Header.Get("Origin")
// 如果origin为 "" 说明不是跨域,null 不等于 ""
if origin != "" {
header.Add("Vary", "Origin")
if !isAllowOrigin(origin) {
// 该请求不允许
c.Abort()
return
}
// 说明这个请求是一个预请求
if method == http.MethodOptions {
reqMethod := header.Get("Access-Control-Request-Method")
if reqMethod != "PUT" && reqMethod != "DELETE" {
// 如果方法不对,不提供Access Origin字段。
c.Abort()
return
}
// 必须,接受指定域的请求,可以使用*不加以限制,但不安全
//header.Set("Access-Control-Allow-Origin", "*")
c.Header("Access-Control-Allow-Origin", origin)
// 预请求最大缓存时间
c.Header("Access-Control-Max-Age", "86400")
// 必须,设置服务器支持的所有跨域请求的方法
c.Header("Access-Control-Allow-Methods", "POST, GET, PUT, DELETE, OPTIONS")
// 服务器支持的所有头信息字段,不限于浏览器在"预检"中请求的字段
c.Header("Access-Control-Allow-Headers", "Content-Type, Content-Length, Token")
// 可选,设置XMLHttpRequest的响应对象能拿到的额外字段
c.Header("Access-Control-Expose-Headers", "Access-Control-Allow-Headers, Token")
// 可选,是否允许后续请求携带认证信息Cookie,该值只能是true,不需要则不设置
c.Header("Access-Control-Allow-Credentials", "true")
c.AbortWithStatus(http.StatusNoContent)
return
}
header.Set("Access-Control-Allow-Origin", origin)
// 必须,设置服务器支持的所有跨域请求的方法
c.Header("Access-Control-Allow-Methods", "POST, GET, PUT, DELETE, OPTIONS")
// 服务器支持的所有头信息字段,不限于浏览器在"预检"中请求的字段
c.Header("Access-Control-Allow-Headers", "Content-Type, Content-Length, Token")
// 可选,设置XMLHttpRequest的响应对象能拿到的额外字段
c.Header("Access-Control-Expose-Headers", "Access-Control-Allow-Headers, Token")
// 可选,是否允许后续请求携带认证信息Cookie,该值只能是true,不需要则不设置
c.Header("Access-Control-Allow-Credentials", "true")
c.Next()
}
}
效果:
Nginx
直接利用反向代理进行处理
server{
# 监听8080端口
listen 8080;
# 域名是localhost
server_name localhost;
location / {
index index.html index.htm;
}
#凡是localhost:8080/api这个样子的,都转发到真正的服务端地址http://localhost:9871
location ^~ /api/ {
proxy_pass http://localhost:4399;
}
}
参考资料
浏览器跨域问题与服务器中的 CORS - 掘金 (juejin.cn)