目录

golang+gin+jwt实现用户登录认证与 Token 鉴权

在众多认证方案中,JWT (JSON Web Token) 凭借其简洁、无状态和跨域友好的特性,成为了构建 RESTful API 时的热门选择。而 Golang 语言以其出色的性能和并发处理能力,搭配轻量级的 Gin 框架,正是实现这一方案的理想组合。

这篇文章将带你从零开始,一步步用 Golang + Gin + JWT 构建一个完整的用户登录认证系统

一、为什么选择 JWT?

我们先简单了解一下 JWT 的优势:

  1. 无状态:JWT 自身包含了用户的身份信息,服务器不需要在数据库或缓存中存储会话状态。这使得服务可以轻松地水平扩展,因为任何一个服务器都可以验证 Token 的有效性。
  2. 紧凑且自包含:Token 体积小,可以通过 URL、POST 参数或 HTTP Header 轻松传输。并且它包含了所有必要的信息,避免了多次查询数据库。
  3. 跨域友好:由于其无状态特性和基于标准的格式,JWT 在构建跨域应用时非常方便。
  4. 易于实现:在 Golang 中有非常成熟的库(如 golang-jwt/jwt)可以帮助我们快速实现 JWT 的生成与验证。

二、准备工作

在开始编写代码之前,请确保你的开发环境已经准备好了以下工具:

  1. Golang:建议使用 1.16+ 版本。
  2. 一个代码编辑器:如 VS Code、GoLand 等。
  3. Go Modules:Golang 1.11+ 内置的依赖管理工具。

首先,我们创建一个新的项目并初始化 Go Modules:

mkdir -p ~/go-projects/gin-jwt-auth && cd ~/go-projects/gin-jwt-auth
go mod init github.com/your-username/gin-jwt-auth

然后,我们需要安装本次实战所需的核心依赖库:

  • Gin 框架github.com/gin-gonic/gin
  • JWT 库github.com/golang-jwt/jwt/v5
  • 用于密码加密golang.org/x/crypto/bcrypt
go get github.com/gin-gonic/gin
go get github.com/golang-jwt/jwt/v5
go get golang.org/x/crypto/bcrypt

三、项目结构与核心逻辑

我们将采用一个简单清晰的结构来组织代码:

gin-jwt-auth/
├── go.mod
├── go.sum
└── main.go

在 main.go 文件中,我们将实现以下核心功能:

  1. 模拟用户数据库:在真实项目中,这部分会替换为数据库操作。
  2. 密码加密:使用 bcrypt 对用户密码进行哈希处理,永远不要在数据库中存储明文密码。
  3. 生成 JWT:用户登录成功后,根据用户信息生成并返回一个 JWT Token。
  4. JWT 中间件:创建一个 Gin 中间件,用于验证 API 请求中携带的 Token 是否有效。
  5. 定义路由:实现 /login 登录接口和一个需要认证的 /profile 接口。

四、完整代码实现

下面是 main.go 的完整代码,其中包含了详细的注释来解释每一部分的功能。

package main

import (
	"net/http"
	"time"

	"github.com/gin-gonic/gin"
	"github.com/golang-jwt/jwt/v5"
	"golang.org/x/crypto/bcrypt"
)

// 定义一个结构体来表示用户
type User struct {
	ID       uint   `json:"id"`
	Username string `json:"username"`
	Password string `json:"-"` // 密码字段在返回给前端时应隐藏
}

// 模拟一个用户数据库
var users = []User{
	// 密码是 "password123"
	{ID: 1, Username: "admin", Password: "$2a$10$xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"}, // 这里应该是一个真实的 bcrypt 哈希值
}

// 定义一个结构体来接收登录请求的 JSON 数据
type LoginRequest struct {
	Username string `json:"username" binding:"required"`
	Password string `json:"password" binding:"required"`
}

// JWT 的密钥,在生产环境中,这应该从环境变量或配置文件中读取,并且要保证其安全性
var jwtSecret = []byte("your-secret-key-keep-it-safe-and-long")

// generateToken 生成一个 JWT Token
func generateToken(userID uint, username string) (string, error) {
	// 设置 Token 的过期时间,例如 24 小时
	expirationTime := time.Now().Add(24 * time.Hour)

	// 创建一个新的 JWT claims,包含用户 ID 和用户名,并设置过期时间
	claims := &jwt.MapClaims{
		"sub":  userID,
		"name": username,
		"exp":  expirationTime.Unix(),
	}

	// 使用 HS256 算法和我们的密钥创建一个新的 Token
	token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)

	// 将 Token 签名并转换为字符串
	tokenString, err := token.SignedString(jwtSecret)
	if err != nil {
		return "", err
	}

	return tokenString, nil
}

// authMiddleware 是一个 Gin 中间件,用于验证 JWT Token
func authMiddleware() gin.HandlerFunc {
	return func(c *gin.Context) {
		// 从 HTTP Header 的 "Authorization" 字段获取 Token
		authHeader := c.GetHeader("Authorization")
		if authHeader == "" {
			c.JSON(http.StatusUnauthorized, gin.H{"error": "Authorization header is required"})
			c.Abort() // 终止请求链
			return
		}

		// 通常 Token 的格式是 "Bearer <token>"
		var tokenString string
		// 简单检查格式
		if len(authHeader) > 7 && authHeader[:7] == "Bearer " {
			tokenString = authHeader[7:]
		} else {
			c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid authorization header format. Use Bearer <token>"})
			c.Abort()
			return
		}

		// 解析 Token
		token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
			// 验证签名方法是否为我们预期的 HS256
			if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
				return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
			}
			return jwtSecret, nil
		})

		if err != nil {
			c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid or expired token"})
			c.Abort()
			return
		}

		// 检查 Token 是否有效
		if claims, ok := token.Claims.(jwt.MapClaims); ok && token.Valid {
			// 将用户信息存储在 Gin 的上下文 (Context) 中,以便后续的处理函数可以使用
			c.Set("userID", int(claims["sub"].(float64))) // JWT claims 中的数字默认是 float64
			c.Set("username", claims["name"].(string))
			c.Next() // 继续处理下一个中间件或路由处理函数
		} else {
			c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid token claims"})
			c.Abort()
			return
		}
	}
}

func main() {
	// 创建一个默认的 Gin 路由器
	r := gin.Default()

	// 公开的登录接口
	r.POST("/login", func(c *gin.Context) {
		var req LoginRequest
		// 绑定并验证请求体中的 JSON 数据
		if err := c.ShouldBindJSON(&req); err != nil {
			c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
			return
		}

		// 在真实场景中,这里应该是查询数据库
		var foundUser *User
		for i := range users {
			if users[i].Username == req.Username {
				foundUser = &users[i]
				break
			}
		}

		// 检查用户是否存在以及密码是否正确
		if foundUser == nil {
			c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid username or password"})
			return
		}

		// 使用 bcrypt 比较明文密码和数据库中存储的哈希值
		err := bcrypt.CompareHashAndPassword([]byte(foundUser.Password), []byte(req.Password))
		if err != nil {
			c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid username or password"})
			return
		}

		// 登录成功,生成 Token
		token, err := generateToken(foundUser.ID, foundUser.Username)
		if err != nil {
			c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to generate token"})
			return
		}

		// 返回 Token 给客户端
		c.JSON(http.StatusOK, gin.H{
			"message": "Login successful",
			"token":   token,
		})
	})

	// 需要认证的路由组
	authorized := r.Group("/")
	authorized.Use(authMiddleware()) // 应用我们的 JWT 中间件
	{
		// 用户个人资料接口,只有携带有效 Token 的请求才能访问
		authorized.GET("/profile", func(c *gin.Context) {
			// 从上下文中获取用户信息
			userID, _ := c.Get("userID")
			username, _ := c.Get("username")

			// 返回用户信息
			c.JSON(http.StatusOK, gin.H{
				"userID":   userID,
				"username": username,
			})
		})
	}

	// 启动服务器,默认在 8080 端口
	r.Run(":8080")
}

如何获取一个真实的 bcrypt 哈希值?

在上面的代码中,users 切片里的密码是一个占位符。你可以在你的终端里运行以下简单的 Go 代码来生成一个真实的哈希值:

package main

import (
	"fmt"

	"golang.org/x/crypto/bcrypt"
)

func main() {
	password := []byte("password123") // 你想要加密的密码
	hashedPassword, err := bcrypt.GenerateFromPassword(password, bcrypt.DefaultCost)
	if err != nil {
		panic(err)
	}
	fmt.Println(string(hashedPassword))
}

将运行后输出的哈希字符串替换掉代码中的占位符即可。

五、测试验证

现在,让我们启动服务并测试一下我们的 API。

启动服务,然后调用:

curl -X POST http://localhost:8080/login \
-H "Content-Type: application/json" \
-d '{"username": "admin", "password": "password123"}'
{"message":"Login successful","token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."}

第二步:使用 Token 访问受保护的接口

将上一步获取的 token 替换到下面的命令中。

curl -X GET http://localhost:8080/profile \
-H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
{"userID":1,"username":"admin"}

如果使用无效或不提供 Token,你将收到 401 Unauthorized 的错误响应。

六、生产环境

这篇教程提供了一个基础且完整的实现,但在将其应用到生产环境之前,你还需要考虑以下几点:

  1. 密钥管理jwtSecret 绝对不能硬编码在代码中。应该使用环境变量(如 os.Getenv("JWT_SECRET"))或安全的配置管理服务来注入。
  2. Token 过期时间:根据你的应用场景,设置合理的 Token 过期时间。对于安全性要求高的应用,可以设置得短一些(如 15-60 分钟),并实现刷新 Token(Refresh Token)的机制。
  3. HTTPS:始终使用 HTTPS 来传输 Token,以防止 Token 在网络中被窃听或篡改。
  4. 密码策略:强制用户使用强密码,并对密码尝试次数进行限制,以防止暴力破解。
  5. 数据库:将模拟的用户数据替换为真实的数据库(如 PostgreSQL, MySQL)操作,并使用 ORM 框架(如 GORM)来简化开发。
  6. 日志和监控:添加完善的日志记录和监控,以便追踪认证相关的事件和问题。

总结

我们从理论到实践,一步步实现了用户登录、Token 生成、Token 验证等关键功能。这个模式是构建现代 Web 应用和 API 的基石,希望能对你的项目开发有所帮助。

如果你有任何疑问或者想了解更多(比如 Refresh Token 的实现),欢迎在评论区留言讨论。

版权声明

未经授权,禁止转载本文章。
如需转载请保留原文链接并注明出处。即视为默认获得授权。
未保留原文链接未注明出处或删除链接将视为侵权,必追究法律责任!

本文原文链接: https://fiveyoboy.com/articles/go-gin-jwt-token/

备用原文链接: https://blog.fiveyoboy.com/articles/go-gin-jwt-token/