五、跨域资源共享
跨域资源共享(CORS,Cross-Origin Resource Sharing)是一种机制,它允许来自不同源的请求访问资源。默认情况下,浏览器出于安全原因会阻止跨域 HTTP 请求。Gin 框架本身没有内置的 CORS 支持,但可以通过中间件轻松实现。
-
同源策略:浏览器的安全策略,只允许同源请求访问资源。同源指的是协议、主机和端口都相同。
-
CORS 头:服务器通过设置特定的 HTTP 响应头来告诉浏览器它允许哪些来源可以访问其资源。
5.1 不配置CORS
假设你有一个前端应用运行在 http://localhost:3000
,而后端服务是用 Gin 开发的,运行在 http://localhost:8080
。前端需要通过 AJAX 请求调用后端的 API。
如果没有配置 CORS,当浏览器尝试从 http://localhost:3000
发起请求到 http://localhost:8080
时,会因为跨域限制而被阻止。
5.1.1 后端代码
启动服务
func Test1(t *testing.T) {r := gin.Default()r.GET("/test", func(c *gin.Context) {c.JSON(200, gin.H{"message": "成功进入test路由!",})})r.Run()
}
5.1.2 前端代码(发起跨域请求)
创建一个名为 index.html
的文件,并将以下内容粘贴进去:
<!DOCTYPE html>
<html lang="en">
<head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>Fetch API Example</title>
</head>
<body><h1>Fetch API Example</h1><script>// 使用 Fetch API 发起请求到后端接口fetch('http://localhost:8080/test').then(response => response.json()).then(data => console.log(data)).catch(error => console.error('Error:', error));
</script></body>
</html>
5.1.3 测试
方式一:用浏览器打开index.html
方式二(我用的这种方式):为了更方便地查看和调试前端代码,可以使用 Python 提供的简易 HTTP 服务器。在终端中导航到项目目录,然后运行
python -m http.server 3000
这会在 http://localhost:3000
启动一个简单的 HTTP 静态文件服务器。
然后再在浏览器访问 http://localhost:3000/index.html
5.1.4 测试结果
打开浏览器开发者工具(通常按 F12 或右键点击页面选择“检查”),切换到“console”标签页
这是因为浏览器默认出于安全考虑,不允许从不同源(不同协议、域名或端口)的页面访问资源。如果服务器未正确设置允许跨域访问的响应头,就会触发这个错误。
Access to fetch at 'http://localhost:8080/test' from origin 'http://localhost:3000' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource. If an opaque response serves your needs, set the request's mode to 'no-cors' to fetch the resource with CORS disabled.
5.2 配置CORS
5.2.1 安装 CORS 中间件库
github.com/gin-contrib/cors
是一个常用的 Gin 中间件库。
安装命令:
go get github.com/gin-contrib/cors
5.2.2 配置和使用中间件
最简单的,使用默认的配置
// 使用默认选项r.Use(cors.Default())
或者自定义配置
func Test3(t *testing.T) {r := gin.Default()// 或者自定义配置r.Use(cors.New(cors.Config{AllowOrigins: []string{"http://localhost:3000"},AllowMethods: []string{"GET", "POST", "PUT"},AllowHeaders: []string{"Origin", "Content-Type"},ExposeHeaders: []string{"Content-Length"},AllowCredentials: true,MaxAge: 12 * time.Hour,}))r.GET("/test", func(c *gin.Context) {c.JSON(200, gin.H{"message": "成功进入test路由!",})})r.Run()
}
配置选项解释
-
AllowOrigins
: 指定哪些域名可以进行跨域请求。如果要允许所有域名,可以使用"*"
。 -
AllowMethods
: 指定哪些 HTTP 方法被允许用于跨域请求,如 GET、POST 等。 -
AllowHeaders
: 指定客户端在预检请求(preflight request)时能发送哪些自定义头部字段。 -
ExposeHeaders
: 列出哪些响应头部信息可以暴露给外部 JavaScript 程序。 -
AllowCredentials
: 是否允许发送 Cookie 信息。如果设置为 true,则不能将 AllowOrigins 设置为"*"
,而必须指定具体的 URL。 -
MaxAge
: 指示预检请求结果能够被缓存多长时间,以减少客户端与服务器之间不必要的通信。
5.2.3 再次测试结果
六、授权与认证
在Gin框架中,实现授权和认证通常涉及到用户身份验证(Authentication)和权限控制(Authorization)。这两者是确保应用程序安全性的重要组成部分。
6.1 认证(Authentication)
认证是指验证用户身份的过程。常见的方法包括使用用户名和密码、OAuth、JWT等。在Gin中,通常通过中间件来处理认证逻辑。
这里我们使用JWT(JSON Web Token),它是一种用于在各方之间作为JSON对象安全地传输信息的紧凑、URL安全的方式。广泛应用于Web应用程序中,用于实现用户认证和授权。
- 用户登录:接收用户名和密码,验证后生成并返回一个JWT。
- 请求保护资源:客户端在请求头或Cookie中携带JWT。
- 解析与验证JWT:服务器端通过解析该令牌以确认其有效性,并提取其中的声明信息。
6.1.1 安装JWT
官方文档
go get -u github.com/golang-jwt/jwt/v5
6.1.2 使用
6.1.2.1 生成新的token
1、首先定义一个登录接口
type LoginInfo struct {Username string `json:"username" binding:"required"`Password string `json:"password" binding:"required"`
}func Test4(t *testing.T) {r := gin.Default()// 模拟用户登录操作r.POST("login", func(c *gin.Context) {var loginInfo LoginInfoif err := c.ShouldBindJSON(&loginInfo); loginInfo.Password != "123123" || err != nil {// 简单模拟校验密码是否正确c.JSON(http.StatusBadRequest, gin.H{"msg": "用户名或密码错误",})return}c.JSON(http.StatusOK, gin.H{"msg": "登录成功",})})r.Run()
}
2、然后实现登录验证成功后生成并返回一个JWT token的方法
// 定义一个密钥
var jwtKey = []byte("这是个密钥 private key")// token
type Claims struct {Username stringjwt.RegisteredClaims // 官方提供的实现了Claims接口的类
}// 登录成功后生成一个有效时间为5分钟的token
func LoginSuccess(username string) string {expirationTime := time.Now().Add(time.Minute * 5)claims := &Claims{Username: username,RegisteredClaims: jwt.RegisteredClaims{ExpiresAt: jwt.NewNumericDate(expirationTime),},}// 注册一个新的token,需要传入注册方法和实现Claims接口的类token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)tokenString, _ := token.SignedString(jwtKey)fmt.Println(tokenString)return tokenString
}
3、应用在login接口中
...// 登录成功后,注册一个token并通过cookie返回给客户tokenStr := LoginSuccess(loginInfo.Username)c.SetCookie("token", tokenStr, 300, "/", "", false, true)c.JSON(http.StatusOK, gin.H{"msg": "登录成功",})
}
4、调用login接口成功后,应该在cookie中能看到token
6.1.2.2 验证cookie中的token
定义一个gin的中间件,用于校验token,如果校验成功则进行c.Next(),否则不通过
// 定义校验token的中间件
func CheckToken() gin.HandlerFunc {return func(c *gin.Context) {// 获取cookie中的tokentokenStr, err := c.Cookie("token")if err != nil {c.Abort()c.JSON(http.StatusUnauthorized, gin.H{"msg": "请重新登录",})}// 校验tokenclaims := &Claims{}tkn, err := jwt.ParseWithClaims(tokenStr, claims,func(token *jwt.Token) (interface{}, error) { return jwtKey, nil })if !tkn.Valid || err != nil {c.JSON(http.StatusUnauthorized, gin.H{"msg": "请重新登录",})return}// 可以将用户信息放进context中,方便后续使用c.Set("username", claims.Username)c.Next()}
}
定义一个新的接口,并应用这个中间件
r.Use(CheckToken())r.GET("/homepage", func(c *gin.Context) {c.JSON(http.StatusOK, gin.H{"msg": "欢迎你," + c.GetString("username"),})})
将之前的cookie中的token复制到新的窗口中调用homepage接口
6.2 授权(Authorization)
授权是在确认用户身份之后,决定其是否有权访问特定资源或执行某些操作的过程。常见的方法包括基于角色的访问控制(RBAC)和基于属性的访问控制(ABAC)。
6.2.1 基于角色的访问控制
RBAC根据用户所属角色来授予权限。例如,一个管理员可能有权访问所有资源,而普通用户只能查看自己的数据。
func CheckRole() gin.HandlerFunc {return func(c *gin.Context) {username := c.GetString("username")if role := getRole(username); role != "admin" {c.Abort()c.JSON(http.StatusForbidden, gin.H{"msg": "抱歉,没有权限访问",})return}// 如果有权限,则继续处理请求c.Next()}
}func getRole(username string) string {// 模拟从数据库查询该用户的角色if username == "admin" {return "admin"} else {return "user"}
}
然后定义一个新的路由组,应用这个中间件
group := r.Group("menu")group.Use(CheckRole())group.GET("list", func(c *gin.Context) {c.JSON(http.StatusOK, gin.H{"msg": "成功访问菜单",})})