golang+gin+jwt实现用户登录认证与 Token 鉴权
在众多认证方案中,JWT (JSON Web Token) 凭借其简洁、无状态和跨域友好的特性,成为了构建 RESTful API 时的热门选择。而 Golang 语言以其出色的性能和并发处理能力,搭配轻量级的 Gin 框架,正是实现这一方案的理想组合。
这篇文章将带你从零开始,一步步用 Golang + Gin + JWT 构建一个完整的用户登录认证系统
一、为什么选择 JWT?
我们先简单了解一下 JWT 的优势:
- 无状态:JWT 自身包含了用户的身份信息,服务器不需要在数据库或缓存中存储会话状态。这使得服务可以轻松地水平扩展,因为任何一个服务器都可以验证 Token 的有效性。
- 紧凑且自包含:Token 体积小,可以通过 URL、POST 参数或 HTTP Header 轻松传输。并且它包含了所有必要的信息,避免了多次查询数据库。
- 跨域友好:由于其无状态特性和基于标准的格式,JWT 在构建跨域应用时非常方便。
- 易于实现:在 Golang 中有非常成熟的库(如
golang-jwt/jwt)可以帮助我们快速实现 JWT 的生成与验证。
二、准备工作
在开始编写代码之前,请确保你的开发环境已经准备好了以下工具:
- Golang:建议使用 1.16+ 版本。
- 一个代码编辑器:如 VS Code、GoLand 等。
- 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 文件中,我们将实现以下核心功能:
- 模拟用户数据库:在真实项目中,这部分会替换为数据库操作。
- 密码加密:使用 bcrypt 对用户密码进行哈希处理,永远不要在数据库中存储明文密码。
- 生成 JWT:用户登录成功后,根据用户信息生成并返回一个 JWT Token。
- JWT 中间件:创建一个 Gin 中间件,用于验证 API 请求中携带的 Token 是否有效。
- 定义路由:实现
/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 的错误响应。
六、生产环境
这篇教程提供了一个基础且完整的实现,但在将其应用到生产环境之前,你还需要考虑以下几点:
- 密钥管理:
jwtSecret绝对不能硬编码在代码中。应该使用环境变量(如os.Getenv("JWT_SECRET"))或安全的配置管理服务来注入。 - Token 过期时间:根据你的应用场景,设置合理的 Token 过期时间。对于安全性要求高的应用,可以设置得短一些(如 15-60 分钟),并实现刷新 Token(Refresh Token)的机制。
- HTTPS:始终使用 HTTPS 来传输 Token,以防止 Token 在网络中被窃听或篡改。
- 密码策略:强制用户使用强密码,并对密码尝试次数进行限制,以防止暴力破解。
- 数据库:将模拟的用户数据替换为真实的数据库(如 PostgreSQL, MySQL)操作,并使用 ORM 框架(如 GORM)来简化开发。
- 日志和监控:添加完善的日志记录和监控,以便追踪认证相关的事件和问题。
总结
我们从理论到实践,一步步实现了用户登录、Token 生成、Token 验证等关键功能。这个模式是构建现代 Web 应用和 API 的基石,希望能对你的项目开发有所帮助。
如果你有任何疑问或者想了解更多(比如 Refresh Token 的实现),欢迎在评论区留言讨论。
版权声明
未经授权,禁止转载本文章。
如需转载请保留原文链接并注明出处。即视为默认获得授权。
未保留原文链接未注明出处或删除链接将视为侵权,必追究法律责任!
本文原文链接: https://fiveyoboy.com/articles/go-gin-jwt-token/
备用原文链接: https://blog.fiveyoboy.com/articles/go-gin-jwt-token/