Gin+axios跨域问题详解
文章目录
- Gin+axios跨域问题详解
- 前言
- 一 为什么会跨域
- 1. 什么是源
- 2. URL结构
- 3. 同源不同源举🌰
- 同源例子
- 不同源例子
- 4. 浏览器为什么需要同源策略
- 5. 常规前端请求跨域
- 二 前端解决方案
- 三 后端解决方案
- Http 协议CORS头
- 四 设置跨域,依旧报错
- 跨域回顾
- 简单请求
- 非简单请求
- 前端axios 的处理
- 后端修改代码,增加针对预检请求的处理
前言
跨域问题一直是前端的一大难题
标准的前端跨域报错:
Access to XMLHttpRequest at '…' from origin '…' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource
一 为什么会跨域
说到跨域不得不谈的就是浏览器的同源策略,跨域也是因为浏览器这个机制引起的,这个机制的存在还是在于安全。
1. 什么是源
Web内容的源由用于访问它的URL 的方案(协议),主机(域名)和端口定义。只有当方案,主机和端口都匹配时,两个对象具有相同的起源。
同源不同源一句话就可以判断:就是url中 scheme host port 都相同即为同源。
下面认识下url 结构中的这三个部分。
2. URL结构
URL 代表着是统一资源定位符(Uniform Resource Locator)。URL 无非就是一个给定的独特资源在 Web 上的地址。
URL有如下结构组成:
- Schme 或者 Protocol
- Domain Name 也叫做host域名
- port 端口号
- Parameters参数
- Anchor 锚点,一般用于定位位置
3. 同源不同源举🌰
举一下同源不同源的例子,便于
同源例子
例子 | 原因 |
http://example.com/app1/index.html http://example.com/app2/index.html | 相同的 scheme http 和host |
http://Example.com:80 http://example.com | http 默认80端口所以同源 |
不同源例子
例子 | 原因 |
http://example.com/app1 https://example.com/app2 | 不同的协议 |
http://example.com http://myapp.example.com | 不同的host |
http://example.com http://example.com:8080 | 不同的端口 |
4. 浏览器为什么需要同源策略
同源策略是一个重要的安全策略,它用于限制一个origin的文档或者它加载的脚本如何能与另一个源的资源进行交互。它能帮助阻隔恶意文档,减少可能被攻击的媒介。
5. 常规前端请求跨域
在没有前后端分离的时候,跨域问题往往是很少的。因为前后端都部署到一起。现在前后端分离不管vue /react 面临跨域请求的问题。
下面是引用官网描述的一张图来解释跨域:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-9ghiwNeq-1680414983802)(D:\25683\zhang\myBlog\firstblog\source\img\7000)]
跨源域资源共享(CORS)机制允许 Web 应用服务器进行跨源访问控制,从而使跨源数据传输得以安全进行。现代浏览器支持在 API 容器中(例如 XMLHttpRequest 或 Fetch)使用 CORS,以降低跨源 HTTP 请求所带来的风险。
二 前端解决方案
// 在http.js中引入axios
import Vue from "vue";
import axios from "axios"; // 引入axios
axios.defaults.timeout = 15000;
axios.defaults.headers.post["Content-Type"] = "application/json;charset=UTF-8";
axios.defaults.wheaders.post["Access-Control-Allow-Origin"] = "*";
axios.defaults.withCredentials=true; // 允许携带cookie
三 后端解决方案
Http 协议CORS头
跨域其实也是http层面上可以解决的问题,后端解决也是比较简单的,也是项目常见的解决手法。
CORS (Cross-Origin Resource Sharing,跨域资源共享)是一个系统,它由一系列传输的HTTP头组成,这些HTTP头决定浏览器是否阻止前端 JavaScript 代码获取跨域请求的响应。
同源安全策略 默认阻止“跨域”获取资源。但是 CORS 给了web服务器这样的权限,即服务器可以选择,允许跨域请求访问到它们的资源。
- Access-Control-Allow-Origin 指示请求的资源能共享给哪些域。
- Access-Control-Allow-Credentials 指示当请求的凭证标记为 true 时,是否响应该请求。
- Access-Control-Allow-Headers 用在对预请求的响应中,指示实际的请求中可以使用哪些 HTTP 头。
- Access-Control-Allow-Methods 指定对预请求的响应中,哪些 HTTP 方法允许访问请求的资源。
- Access-Control-Expose-Headers 指示哪些 HTTP 头的名称能在响应中列出。
- Access-Control-Max-Age 指示预请求的结果能被缓存多久。
- Access-Control-Request-Headers 用于发起一个预请求,告知服务器正式请求会使用那些 HTTP 头。
- Access-Control-Request-Method 用于发起一个预请求,告知服务器正式请求会使用哪一种 HTTP 请求方法。
- Origin 指示获取资源的请求是从什么域发起的。
跨域设置可以使用官网推荐的插件
go get github.com/gin-contrib/cors
也可以自己写一个中间件
func Cors() gin.HandlerFunc {
return func(c *gin.Context) {
if origin != "" {
c.Header("Access-Control-Allow-Origin", "*") // 可将 * 替换为指定的域名
c.Header("Access-Control-Allow-Methods", "POST, GET, OPTIONS, PUT, DELETE, UPDATE")
c.Header("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept, Authorization")
c.Header("Access-Control-Expose-Headers", "Content-Length, Access-Control-Allow-Origin, Access-Control-Allow-Headers, Cache-Control, Content-Language, Content-Type")
c.Header("Access-Control-Allow-Credentials", "true")
}
c.Next()
}
}
当然上述的可以进行基本的请求操作。如果修改了header,使用浏览器进行 Get Post Header等请求的话,会默认进行OPTIONS的请求进行试探,要做下处理。
func Cors() gin.HandlerFunc {
return func(c *gin.Context) {
method := c.Request.Method
if origin != "" {
c.Header("Access-Control-Allow-Origin", "*") // 可将 * 替换为指定的域名
c.Header("Access-Control-Allow-Methods", "POST, GET, OPTIONS, PUT, DELETE, UPDATE")
c.Header("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept, Authorization")
c.Header("Access-Control-Expose-Headers", "Content-Length, Access-Control-Allow-Origin, Access-Control-Allow-Headers, Cache-Control, Content-Language, Content-Type")
c.Header("Access-Control-Allow-Credentials", "true")
}
if method == "OPTIONS" {
c.AbortWithStatus(http.StatusNoContent)
}
c.Next()
}
}
注意的是Access-Control-Allow-Headers和Access-Control-Allow-Methods 值 如果设置不当也会造成跨域问题无法进行访问,可以去掉也可以进行加 * 操作
如果需要用到跨域携带cookie,session校验的话,“Access-Control-Allow-Origin”就不能设置为“*”,要设置为具体的源地址,否则会报安全错误
func Cors() gin.HandlerFunc {
return func(c *gin.Context) {
method := c.Request.Method
if origin != "" {
c.Header("Access-Control-Allow-Origin", "*") // 可将 * 替换为指定的域名 加*即代表允许所有的跨域请求
c.Header("Access-Control-Allow-Methods", "*") // 允许所有的请求方法类型
//c.Header("Access-Control-Allow-Headers", "*") // 允许的请求头信息
c.Header("Access-Control-Expose-Headers", "Content-Length, Access-Control-Allow-Origin, Access-Control-Allow-Headers, Cache-Control, Content-Language, Content-Type")
c.Header("Access-Control-Allow-Credentials", "true") // 允许校验携带的cookie,session。
}
if method == "OPTIONS" {
c.AbortWithStatus(http.StatusNoContent)
}
c.Next()
}
然后我们只需在gin中的相应的路由中加入中间件就行了
import (
"github.com/gin-contrib/cors"
"github.com/gin-gonic/gin"
)
func main() {
app := gin.New()
app.Use(cors.Default())
}
四 设置跨域,依旧报错
前后端均做跨域处理,但是却依旧请求失败
跨域回顾
浏览器的请求分为,简单请求,非简单请求。简单请求浏览器不会预检,而非简单请求会预检。
简单请求
同时满足下列三大条件,就属于简单请求,否则属于非简单请求
1. 请求方式只能是:GET、POST、HEAD
- HTTP请求头限制这几种字段:Accept、Accept-Language、Content-Language、Content-Type、Last-Event-ID
3. Content-type只能取:application/x-www-form-urlencoded、multipart/form-data、text/plain
对于简单请求,浏览器直接请求,会在请求头信息中,增加一个origin字段,来说明本次请求来自哪个源(协议+域名+端口)。服务器根据这个值,来决定是否同意该请求。服务器返回的响应会多几个和跨域相关的头信息字段
- Access-Control-Allow-Origin:该字段是必须的,* 表示接受任意域名的请求,还可以指定域名
- Access-Control-Allow-Credentials:该字段可选,是个布尔值,表示是否可以携带cookie,(注意:如果Access-Control-Allow-Origin字段设置*,此字段设为true无效)
- Access-Control-Allow-Headers:表明服务器允许请求中携带字段 ,如Cache-Control、Content-Type、Expires等
- Access-Control-Max-Age:有效时间,在有效时间内,浏览器无须为同一请求再次发起预检请求
非简单请求
非简单请求是对那种对服务器有特殊要求的请求,比如请求方式是PUT或者DELETE,或者Content-Type字段类型是application/json。都会在正式通信之前,增加一次HTTP请求,称之为预检。
浏览器会先询问服务器,当前网页所在域名是否在服务器的许可名单之中,服务器允许之后,浏览器会发出正式的XMLHttpRequest请求,否则会报错。(备注:之前碰到预检请求后端没有通过,就不会发正式请求)
针对预检请求的报错,我们可以采取以下两种处理方法
前端axios 的处理
经过不断尝试,发现在axios代码中如果去掉 axios.defaults.headers.post["Access-Control-Allow-Origin"] = "*";
这行代码的话,请求可以了。但这样并不满足我们的需要,倘若前端不是我们自己开发的,后端如何处理才能解决这种问题呢?
后端修改代码,增加针对预检请求的处理
func Cors() gin.HandlerFunc {
return func(c *gin.Context) {
method := c.Request.Method
origin := c.Request.Header.Get("Origin")
if origin != "" {
c.Header("Access-Control-Allow-Origin", "*")
c.Header("Access-Control-Allow-Methods", "POST, GET, OPTIONS, PUT, DELETE, UPDATE")
c.Header("Access-Control-Allow-Headers", "Content-Type,AccessToken,X-CSRF-Token, Authorization")
c.Header("Access-Control-Allow-Credentials", "true")
c.Header("Access-Control-Expose-Headers", "Content-Length, Access-Control-Allow-Origin, Access-Control-Allow-Headers,Cache-Control,Content-Language,Content-Type,Expires,Last-Modified,Pragma,FooBar")
}
if method == "OPTIONS" {
/* 添加 */
c.Header("Access-Control-Allow-Origin", "*")
c.Header("Access-Control-Allow-Methods", "OPTIONS")
c.Header("Access-Control-Allow-Headers", "*")
/* 添加 */
c.AbortWithStatus(http.StatusNoContent)
}
c.Next()
}
}
在请求是OPTIONS
的时候,需要对请求头进行重新赋值,经过测试 c.Header("Access-Control-Allow-Headers", "*")
问题出现在这里,必须填* 否则都会出现上述问题
猜测可能axios或者浏览器内部的bug,前端不设置"Access-Control-Allow-Origin"就没有问题,或者前端设置,但是后端不对"Access-Control-Allow-Origin"做处理,具体不知道他们的交互逻辑是什么,也有人提出类似的问题,但是都没有说明原因,笔者这里也仅仅是猜测。