五、跨域资源共享
跨域资源共享(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 后端代码
启动服务
go
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
的文件,并将以下内容粘贴进去:
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
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 中间件库。
安装命令:
bash
go get github.com/gin-contrib/cors
5.2.2 配置和使用中间件
最简单的,使用默认的配置
go
// 使用默认选项
r.Use(cors.Default())
或者自定义配置
go
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
shell
go get -u github.com/golang-jwt/jwt/v5
6.1.2 使用
6.1.2.1 生成新的token
1、首先定义一个登录接口
go
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 LoginInfo
if 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的方法
go
// 定义一个密钥
var jwtKey = []byte("这是个密钥 private key")
// token
type Claims struct {
Username string
jwt.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接口中
go
...
// 登录成功后,注册一个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(),否则不通过
go
// 定义校验token的中间件
func CheckToken() gin.HandlerFunc {
return func(c *gin.Context) {
// 获取cookie中的token
tokenStr, err := c.Cookie("token")
if err != nil {
c.Abort()
c.JSON(http.StatusUnauthorized, gin.H{
"msg": "请重新登录",
})
}
// 校验token
claims := &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()
}
}
定义一个新的接口,并应用这个中间件
go
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根据用户所属角色来授予权限。例如,一个管理员可能有权访问所有资源,而普通用户只能查看自己的数据。
go
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"
}
}
然后定义一个新的路由组,应用这个中间件
go
group := r.Group("menu")
group.Use(CheckRole())
group.GET("list", func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{
"msg": "成功访问菜单",
})
})