Gin CORS 跨域请求资源共享与中间件

一、同源策略

1.1 什么是浏览器的同源策略?

  • 同源策略(Same origin policy)是一种约定,它是浏览器最核心也最基本的安全功能,如果缺少了同源策略,则浏览器的正常功能可能都会受到影响。可以说Web是构建在同源策略基础之上的,浏览器只是针对同源策略的一种实现
  • 浏览器最基本的安全策略
  • 浏览器只能接收相同域(IP地址+端口)返回的数据

1.2 同源策略判依据

请求的url地址,必须与浏览器上的url地址处于同域上,也就是域名,端口,协议相同只要协议、域名和端口任意一个不同,都是跨域请求

  1. 比如: 我在本地上的域名是127.0.0.1:8000,请求另外一个域名:127.0.0.1:8001一段数据
  2. 浏览器上就会报错,这个就是同源策略的保护,如果浏览器对javascript没有同源策略的保护,那么一些重要的机密网站将会很危险
  3. 已拦截跨源请求:同源策略禁止读取位于http://127.0.0.1:8001/SendAjax/的远程资源。(原因:CORS 头缺少 'Access-Control-Allow-Origin')
  4. 但是注意,项目2中的访问已经发生了,说明是浏览器对非同源请求返回的结果做了拦截

所以就导致了向不同域发请求,就会出现跨域问题(被浏览器阻止了),正常来说,如果我们不做额外处理,是没办法这么发请求的。

1.3 跨域问题三种解决方案

  • CORS(跨域资源共享:后端技术),主流采用的方案,使用第三方插件
  • 前端代理(只能在测试阶段使用):node起了一个服务,正向代理
  • jsonp:只能解决get请求跨域,本质原理使用了某些标签不限制跨域(img,script)

二、CORS:跨域资源共享简介(后端技术)

CORS需要浏览器和服务器同时支持。目前,所有浏览器都支持该功能,IE浏览器不能低于IE10。

整个CORS通信过程,都是浏览器自动完成,不需要用户参与。对于开发者来说,CORS通信与同源的AJAX通信没有差别,代码完全一样。浏览器一旦发现AJAX请求跨源,就会自动添加一些附加的头信息,有时还会多出一次附加的请求,但用户不会有感觉。

因此,实现CORS通信的关键是服务器。只要服务器实现了CORS接口,就可以跨源通信。

三 CORS基本流程

1.CORS请求分类

  • 简单请求 (simple request):简单请求只发一次

  • 非简单请求 (not-so-simple request):发送两次,第一次是options请求,第二次是真正的请求

2.基本流程

  • 浏览器发出CORS简单请求只需要在头信息之中增加一个Origin字段。
  • 浏览器发出CORS非简单请求会在正式通信之前,增加一次HTTP查询请求,称为"预检"请求(preflight)。浏览器先询问服务器,当前网页所在的域名是否在服务器的许可名单之中,以及可以使用哪些HTTP动词和头信息字段。只有得到肯定答复,浏览器才会发出正式的XMLHttpRequest请求,否则就报错。

四、CORS两种请求详解

1.两种请求详解

只要同时满足以下两大条件,就属于简单请求

  1. 请求方法是以下三种方法之一:
    • HEAD
    • GET
    • POST
  2. HTTP的头信息不超出以下几种字段:
    • Accept
    • Accept-Language
    • Content-Language
    • Last-Event-ID
    • Content-Type:只限于三个值application/x-www-form-urlencodedmultipart/form-data、text/plain

凡是不同时满足上面两个条件,就属于非简单请求

浏览器对这两种请求的处理,是不一样的。

简单请求和非简单请求的区别

  • 简单请求: 一次请求
  • 非简单请求:两次请求,在发送数据之前会先发一次请求用于做"预检",只有"预检"通过后才再发送一次请求用于数据传输。

关于"预检"

  • 请求方式:OPTIONS
  • "预检"其实做检查,检查如果通过则允许传输数据,检查不通过则不再发送真正想要发送的消息

如何"预检" ?

  • 如果复杂请求是PUT等请求,则服务端需要设置允许某请求,否则"预检"不通过Access-Control-Allow-Methods

  • 如果复杂请求设置了请求头,则服务端需要设置允许某请求头,否则"预检"不通过Access-Control-Allow-Headers

2.解决跨域问题:浏览器对于这两种请求的处理

支持跨域,简单请求

  • 服务器设置响应头:Access-Control-Allow-Origin = '域名' 或 '*'

支持跨域,复杂请求

非简单请求需要判断是否是options请求

由于复杂请求时,首先会发送"预检"请求,如果"预检"成功,则发送真实数据。

  • "预检"请求时,允许请求方式则需服务器设置响应头:Access-Control-Allow-Methods
  • "预检"请求时,允许请求头则需服务器设置响应头: Access-Control-Allow-Headers

五、Gin 中间件

在Gin框架中,中间件(Middleware)是一种允许在请求到达处理程序之前或之后执行一些逻辑的机制。中间件允许你在处理请求的过程中插入一些代码,例如验证请求、记录日志、处理跨域等。

5.1 中间件介绍

中间件是Gin框架的一个关键概念。它是一个函数,接受gin.Context作为参数,可以在请求到达处理程序之前或之后执行一些逻辑。中间件允许你在请求处理过程中执行预处理或后处理的操作。

5.2 初识中间件

在Gin框架中,使用Use方法可以注册一个全局中间件,它将应用于所有路由。例如:

go 复制代码
package main

import (
	"fmt"
	"github.com/gin-gonic/gin"
	"net/http"
)

func LoggerMiddleware(c *gin.Context) {
	// 在请求处理之前执行的逻辑
	fmt.Println("Start Logging")

	// 将请求传递给下一个处理程序
	c.Next()

	// 在请求处理之后执行的逻辑
	fmt.Println("End Logging")
}

func main() {
	r := gin.Default()

	// 注册全局中间件
	r.Use(LoggerMiddleware)

	// 定义路由
	r.GET("/", func(c *gin.Context) {
		c.String(http.StatusOK, "Hello, Gin!")
	})

	r.Run(":8080")
}

在上述例子中,LoggerMiddleware是一个简单的中间件,用于记录请求日志。通过使用Use方法,我们将这个中间件注册为全局中间件,它将应用于所有的路由。

查看Use方法源码如下:

综上,所以中间件必须是一个 gin.HandlerFunc 类型,配置路由的时候可以传递多个 func 回调函数。

5.3 c.Next()

在中间件中,通过调用c.Next()可以将请求传递给下一个处理程序。这是一个重要的步骤,如果你忘记调用c.Next(),那么请求将不会继续传递给后续的中间件或路由处理程序。

go 复制代码
package main

import (
	"fmt"
	"github.com/gin-gonic/gin"
	"time"
)

// 写一个中间件,统计视图函数运行时间
func totalTime(c *gin.Context) {
	start := time.Now()

	c.Next() //继续往后执行

	end := time.Now()
	fmt.Println("视图函数运行时间为:", end.Sub(start))
}
func main() {
	r := gin.Default()
	// 把中间件函数加在视图函数之前
	r.GET("/", totalTime, func(c *gin.Context) {
		time.Sleep(time.Second * 2)
		c.String(200, "hello,Gin!")
	})

	r.Run()
}

5.4 多个中间件执行顺序

如果你有多个中间件,它们将按照注册的顺序执行。在上述例子中,如果我们有多个全局中间件,它们将按照注册的顺序依次执行。

go 复制代码
func Middleware1(c *gin.Context) {
    fmt.Println("Middleware 1 - Start")
    c.Next()
    fmt.Println("Middleware 1 - End")
}

func Middleware2(c *gin.Context) {
    fmt.Println("Middleware 2 - Start")
    c.Next()
    fmt.Println("Middleware 2 - End")
}

func main() {
    r := gin.Default()
    
    // 注册全局中间件,按照注册顺序执行
    r.Use(Middleware1)
    r.Use(Middleware2)
    
    r.GET("/hello", func(c *gin.Context) {
        c.String(http.StatusOK, "Hello, Gin!")
    })
    
    r.Run(":8080")
}

输出结果如下:

go 复制代码
Middleware 1 - Start
Middleware 2 - Start
Middleware 2 - End
Middleware 1 - End

5.5 c.Abort()

在中间件中,通过调用c.Abort()可以终止请求链,不再执行后续的中间件或路由处理程序。这通常是在中间件中检测到错误或条件不满足时使用的。

go 复制代码
package main

import (
	"github.com/gin-gonic/gin"
	"net/http"
)

func isValidAuth(authorizationHeader string) bool {
	// 在这里实现你的身份验证逻辑
	// 例如,你可能验证一个令牌或检查凭证
	// 如果身份验证成功,返回 true;否则返回 false
	return true
}

func AuthMiddleware(c *gin.Context) {
	// 检查是否有有效的 Authorization 头
	if authorizationHeader := c.GetHeader("Authorization"); authorizationHeader == "" {
		// 如果 Authorization 头缺失,返回未授权状态
		c.AbortWithStatus(http.StatusUnauthorized)
		return
	}

	// 检查使用你的自定义逻辑提供的身份验证是否有效
	if !isValidAuth(c.GetHeader("Authorization")) {
		// 如果身份验证失败,返回未授权状态
		c.AbortWithStatus(http.StatusUnauthorized)
		return
	}

	// 如果身份验证成功,继续执行下一个中间件或路由处理程序
	c.Next()
}

func main() {
	r := gin.Default()

	// 应用 AuthMiddleware 以保护 "/protected" 路由
	r.GET("/protected", AuthMiddleware, func(c *gin.Context) {
		// 只有在 AuthMiddleware 允许请求继续时,才会执行此处理程序
		c.String(http.StatusOK, "你有权访问受保护的路由!")
	})

	r.Run(":8080")
}

5.6 全局中间件与局部中间件

全局中间件是通过Use方法注册的,它将应用于所有路由。局部中间件是在定义单个路由时附加的。

go 复制代码
package main

import (
	"fmt"
	"github.com/gin-gonic/gin"
	"net/http"
)

// GlobalMiddleware1 全局中间件1
func GlobalMiddleware1(c *gin.Context) {
	c.Header("X-Global-Middleware-1", "Applied")
	fmt.Printf("GlobalMiddleware1\n")
	c.Next()
}

// GlobalMiddleware2 全局中间件2
func GlobalMiddleware2(c *gin.Context) {
	c.Header("X-Global-Middleware-2", "Applied")
	fmt.Printf("GlobalMiddleware2\n")
	c.Next()
}

// LocalMiddleware3 局部中间件3
func LocalMiddleware3(c *gin.Context) {
	c.Header("X-Local-Middleware-3", "Applied")
	fmt.Printf("LocalMiddleware3\n")
	c.Next()
}

func main() {
	// 创建一个新的 Gin 引擎
	r := gin.New()

	// 使用全局中间件1
	r.Use(GlobalMiddleware1)

	// 使用全局中间件2
	r.Use(GlobalMiddleware2)

	r.GET("/", func(c *gin.Context) {
		c.String(http.StatusOK, "Hello,Gin!")
	})

	// 定义一个路由组,并在路由组中使用局部中间件3
	apiGroup := r.Group("/api")
	apiGroup.Use(LocalMiddleware3)

	// 路由1,将同时应用全局中间件1、全局中间件2以及局部中间件3
	apiGroup.GET("/endpoint1", func(c *gin.Context) {
		c.String(http.StatusOK, "Endpoint 1")
	})

	// 路由2,将同时应用全局中间件1、全局中间件2
	r.GET("/endpoint2", func(c *gin.Context) {
		c.String(http.StatusOK, "Endpoint 2")
	})

	// 启动服务器
	r.Run(":8080")
}

5.7 中间件和视图函数之间共享数据

在中间件和视图函数之间共享数据可以使用c.Setc.Get方法。

go 复制代码
package main

import (
	"fmt"
	"github.com/gin-gonic/gin"
	"net/http"
)

func CustomMiddleware(c *gin.Context) {
	// 在中间件中设置数据
	c.Set("userID", 123)

	// 继续执行下一个中间件或路由处理程序
	c.Next()
}

func ProtectedRouteHandler(c *gin.Context) {
	// 从上一个中间件中获取数据
	userID, exists := c.Get("userID")
	if !exists {
		// 如果数据不存在,返回错误响应
		c.String(http.StatusInternalServerError, "无法获取用户信息")
		return
	}

	// 数据存在,继续处理
	c.String(http.StatusOK, fmt.Sprintf("用户ID:%v,你有权访问受保护的路由!", userID))
}

func main() {
	r := gin.Default()

	// 应用 CustomMiddleware 中间件以设置数据
	r.Use(CustomMiddleware)

	// 设置保护路由
	r.GET("/protected", ProtectedRouteHandler)

	r.Run(":8080")
}

5.8 在路由分组中配置中间件

在Gin框架中,你可以使用路由分组将中间件应用于一组相关的路由。这样,你可以更清晰地组织你的中间件和路由。

go 复制代码
func LoggerMiddleware(c *gin.Context) {
    fmt.Println("Request Log")
    c.Next()
}

func AuthMiddleware(c *gin.Context) {
    // 检查是否有有效的身份验证信息
    if !isValidAuth(c.GetHeader("Authorization")) {
        c.AbortWithStatus(http.StatusUnauthorized)
        return
    }
    c.Next()
}

func main() {
    r := gin.Default()
    
    // 创建一个路由分组,并将中间件应用于该分组中的所有路由
    apiGroup := r.Group("/api", LoggerMiddleware, AuthMiddleware)
    
    apiGroup.GET("/users", func(c *gin.Context) {
        c.String(http.StatusOK, "List of Users")
    })
    
    apiGroup.GET("/products", func(c *gin.Context) {
        c.String(http.StatusOK, "List of Products")
    })
    
    r.Run(":8080")
}

5.9 中间件解决跨域

下面是一个简单的例子:

go 复制代码
package main

import (
	"github.com/gin-gonic/gin"
	"net/http"
)

func main() {
	r := gin.Default()

	// 使用中间件处理跨域问题
	r.Use(CORSMiddleware())

	// 其他路由注册
	r.GET("/hello", func(c *gin.Context) {
		c.JSON(http.StatusOK, gin.H{"message": "Hello, CORS is enabled!"})
	})

	// 启动 Gin 服务器
	r.Run(":8080")
}

// CORSMiddleware 中间件处理跨域问题
func CORSMiddleware() gin.HandlerFunc {
	return func(c *gin.Context) {
		c.Writer.Header().Set("Access-Control-Allow-Origin", "*")
		c.Writer.Header().Set("Access-Control-Allow-Credentials", "true")
		c.Writer.Header().Set("Access-Control-Allow-Methods", "POST, GET, OPTIONS, PUT, DELETE")
		c.Writer.Header().Set("Access-Control-Allow-Headers", "Origin, Content-Type, Content-Length, Accept-Encoding, X-CSRF-Token, Authorization")

		if c.Request.Method == "OPTIONS" {
			c.AbortWithStatus(204)
			return
		}

		c.Next()
	}
}

在上述例子中,CORSMiddleware函数返回一个用于处理跨域的中间件。通过将该中间件注册到Gin框架中,可以轻松地解决跨域问题。

5.10 中间件注意事项

5.10.1 Gin 默认中间件

gin.Default()默认使用了 LoggerRecovery 中间件,其中:

  • Logger 中间件将日志写入 gin.DefaultWriter,即使配置了 GIN_MODE=release

  • Recovery 中间件会 recover 任何 panic。如果有 panic 的话,会写入 500 响应码。

    如果不想使用上面两个默认的中间件,可以使用 gin.New()新建一个没有任何默认中间件的 路由。

5.10.2 gin中间件中使用 goroutine

当在中间件或 handler 中启动新的 goroutine 时,不能使用原始的上下文(c *gin.Context), 必须使用其只读副本(c.Copy())

go 复制代码
r.GET("/", func(c *gin.Context) {
		cCp := c.Copy()
		go func() {
			// simulate a long task with time.Sleep(). 5 seconds time.Sleep(5 * time.Second)
			// 这里使用你创建的副本
			fmt.Println("Done! in path " + cCp.Request.URL.Path)
		}()
		c.String(200, "首页")

	})

六、Gin 框架跨域问题解决

目前大多数的 Web 框架,都提供了 CORS 的解决方案。Gin 也不例外,Gin 里面也提供了一个 middleware 实现来解决跨域问题,打开如下Gin 中间件插件库合集:

go 复制代码
https://github.com/gin-gonic/contrib

找到cors 文件夹:

进入后会提示:此软件包已于2016-12-07放弃。请改用gin-contrib/cors。点击进入最新的即可。

5.1 安装

go 复制代码
go get github.com/gin-contrib/cors

5.2 导入

go 复制代码
import "github.com/gin-contrib/cors"

5.3 直接设置跨域参数(一般用这个)

go 复制代码
package main

import (
	"github.com/gin-contrib/cors"
	"github.com/gin-gonic/gin"
	"strings"
	"time"
)

func main() {
	// 创建一个默认的 Gin 实例
	server := gin.Default()

	// 使用 CORS 中间件处理跨域问题,配置 CORS 参数
	server.Use(cors.New(cors.Config{
		// 允许的源地址(CORS中的Access-Control-Allow-Origin)
		// AllowOrigins: []string{"https://foo.com"},
		// 允许的 HTTP 方法(CORS中的Access-Control-Allow-Methods)
		AllowMethods: []string{"PUT", "PATCH"},
		// 允许的 HTTP 头部(CORS中的Access-Control-Allow-Headers)
		AllowHeaders: []string{"Origin"},
		// 暴露的 HTTP 头部(CORS中的Access-Control-Expose-Headers)
		ExposeHeaders: []string{"Content-Length"},
		// 是否允许携带身份凭证(CORS中的Access-Control-Allow-Credentials)
		AllowCredentials: true,
		// 允许源的自定义判断函数,返回true表示允许,false表示不允许
		AllowOriginFunc: func(origin string) bool {
			if strings.HasPrefix(origin, "http://localhost") {
				// 允许你的开发环境
				return true
			}
			// 允许包含 "yourcompany.com" 的源
			return strings.Contains(origin, "yourcompany.com")
		},
		// 用于缓存预检请求结果的最大时间(CORS中的Access-Control-Max-Age)
		MaxAge: 12 * time.Hour,
	}))

	// 启动 Gin 服务器,监听在 0.0.0.0:8080 上
	server.Run(":8080")
}

5.4 使用DefaultConfig作为起点

go 复制代码
func main() {
  router := gin.Default()
  // - No origin allowed by default
  // - GET,POST, PUT, HEAD methods
  // - Credentials share disabled
  // - Preflight requests cached for 12 hours
  config := cors.DefaultConfig()
  config.AllowOrigins = []string{"http://google.com"}
  // config.AllowOrigins = []string{"http://google.com", "http://facebook.com"}
  // config.AllowAllOrigins = true

  router.Use(cors.New(config))
  router.Run()
}

**注意:**虽然 Default() 允许所有来源,但 DefaultConfig() 不允许,您仍然必须使用 AllowAllOriins

5.5 Default()允许所有来源

go 复制代码
func main() {
  router := gin.Default()
  // same as
  // config := cors.DefaultConfig()
  // config.AllowAllOrigins = true
  // router.Use(cors.New(config))
  router.Use(cors.Default())
  router.Run()
}

使用所有来源会禁用 Gin 为客户端设置 cookie 的能力。处理凭据时,不要允许所有来源。

相关推荐
煎鱼eddycjy1 天前
新提案:由迭代器启发的 Go 错误函数处理
go
煎鱼eddycjy1 天前
Go 语言十五周年!权力交接、回顾与展望
go
不爱说话郭德纲2 天前
聚焦 Go 语言框架,探索创新实践过程
go·编程语言
0x派大星3 天前
【Golang】——Gin 框架中的 API 请求处理与 JSON 数据绑定
开发语言·后端·golang·go·json·gin
IT书架3 天前
golang高频面试真题
面试·go
郝同学的测开笔记3 天前
云原生探索系列(十四):Go 语言panic、defer以及recover函数
后端·云原生·go
秋落风声4 天前
【滑动窗口入门篇】
java·算法·leetcode·go·哈希表
0x派大星6 天前
【Golang】——Gin 框架中的模板渲染详解
开发语言·后端·golang·go·gin
0x派大星6 天前
【Golang】——Gin 框架中的表单处理与数据绑定
开发语言·后端·golang·go·gin
三里清风_7 天前
如何使用Casbin设计后台权限管理系统
golang·go·casbin