在现代 API 接口开发中,JWT(JSON Web Token) 已成为实现用户认证和授权的一种广泛使用的机制。相比传统的基于会话的认证方式,JWT 的无状态设计更适合分布式系统和微服务架构。接下来将和大家一起探讨下 JWT 的原理与安全风险,并通过 Gin 框架实现从登录到鉴权、续期,以及黑名单与 Token 吊销的完整方案。
1. JWT原理与安全风险
1.1 JWT 是什么
JWT 是一种基于 JSON 的轻量级开放标准(RFC 7519),用于在客户端与服务器之间传递加密的信息。
一个典型的 JWT 由三部分组成,用 .
分隔:
- Header :定义算法类型(如
HS256
)。 - Payload:包含用户的声明(如用户 ID、角色等)。
- Signature:通过 Header 和 Payload 使用密钥签名,确保数据未被篡改。
JWT 示例:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3NDI2MzcwNjksInVzZXJfaWQiOiJhZG1pbiJ9.fdtzvufDPITmCAO4yDalPHa3ZcSvPNgWJeK8iAH3La4
1.2 JWT 的优缺点
优点:
- 无状态:JWT 是自包含的,服务器无需存储会话数据,易于扩展。
- 跨平台:JWT 格式是标准化的,可以在不同语言和系统中使用。
- 灵活性:Payload 可自定义,便于存储额外的用户数据。
缺点:
- 不可撤销:JWT 一旦签发,直到过期前都有效,默认情况下无法吊销。
- Payload 明文存储:尽管签名保证了完整性,但 Payload 数据是 Base64 编码的,容易泄露敏感信息。
- 长度较长:JWT 比传统的会话 ID 更大,会增加传输和存储开销。
1.3 安全风险与解决方案
-
敏感数据泄漏:
- 风险:Payload 可被解码,敏感信息可能泄露。
- 解决:避免在 Payload 中存储敏感信息,改用标识符(如用户 ID)。
-
Token 篡改:
- 风险:攻击者可能伪造 Token。
- 解决:使用强随机密钥签名(如 HMAC 或 RSA),并定期更换密钥。
-
Token 重放攻击:
- 风险:攻击者窃取合法 Token 后进行重放。
- 解决:使用 HTTPS,必要时添加 Token 黑名单。
2. 登录接口实现(签发Token)
在 JWT 中,登录接口的核心功能是验证用户身份并签发 Token。
2.1 安装 JWT 库
我们使用 github.com/golang-jwt/... 来处理 JWT。
安装命令:
bash
go get -u github.com/golang-jwt/jwt/v5
当然平常使用更多的是将代码引入,然后使用go mod tidy拉取jwt库。
2.2 生成 Token
下面是在gin框架中使用jwt在登录接口中做token签发功能的完整实现:
go
package main
import (
"github.com/gin-gonic/gin"
"github.com/golang-jwt/jwt/v5"
"time"
)
var secretKey = []byte("your_secret_key") // 用于签名的密钥
// 登录请求结构体
type LoginRequest struct {
Username string `json:"username" binding:"required"`
Password string `json:"password" binding:"required"`
}
// 登录接口
func login(c *gin.Context) {
var req LoginRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(400, gin.H{"error": "Invalid request"})
return
}
// 模拟用户名和密码验证
if req.Username != "admin" || req.Password != "password" {
c.JSON(401, gin.H{"error": "Invalid credentials"})
return
}
// 签发 Token
token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
"user_id": req.Username,
"exp": time.Now().Add(time.Hour * 2).Unix(), // 过期时间
})
// 生成 Token 字符串
tokenString, err := token.SignedString(secretKey)
if err != nil {
c.JSON(500, gin.H{"error": "Failed to generate token"})
return
}
c.JSON(200, gin.H{"token": tokenString})
}
3. 鉴权中间件设计(Token解析与续期)
在上面的示例中我们已经将token签发给了登录的用户,现在就要做根据token来进行鉴权了,我们先来设计一个中间件,这个中间件会从请求头中获取token,然后解析token,如果token有效,则继续执行请求,否则返回401错误。 下面将给出两个示例说明,如何通过鉴权中间件,解析 Token 并校验用户身份。
3.1 鉴权中间件
示例代码:
go
func authMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
// 获取请求头中的 Token
authHeader := c.GetHeader("Authorization")
if authHeader == "" {
c.JSON(401, gin.H{"error": "Authorization token required"})
c.Abort()
return
}
// 解析 Token
token, err := jwt.Parse(authHeader, func(token *jwt.Token) (interface{}, error) {
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, jwt.NewValidationError("invalid signing method", jwt.ValidationErrorSignatureInvalid)
}
return secretKey, nil
})
if err != nil || !token.Valid {
c.JSON(401, gin.H{"error": "Invalid or expired token"})
c.Abort()
return
}
// 将 Token 数据存储到上下文中
if claims, ok := token.Claims.(jwt.MapClaims); ok {
c.Set("user_id", claims["user_id"])
} else {
c.JSON(401, gin.H{"error": "Invalid token payload"})
c.Abort()
return
}
c.Next()
}
}
3.2 路由保护示例
go
r := gin.Default()
// 登录接口
r.POST("/login", login)
// 需要鉴权的路由组
authorized := r.Group("/")
authorized.Use(authMiddleware())
{
authorized.GET("/protected", func(c *gin.Context) {
userID := c.MustGet("user_id").(string)
c.JSON(200, gin.H{"message": "Welcome!", "user_id": userID})
})
}
r.Run(":8080")
4. 黑名单与 Token 吊销
当用户退出登录、注销账号、被禁用等场景下,它的token还能用吗?答案是不可以。那如何来限制呢 由于 JWT 是无状态的,吊销已签发的 Token 需要额外的机制,比如黑名单、或者token版本管理。
4.1 使用 Redis 实现黑名单
当用户登出或管理员强制下线时,将 Token 加入 Redis 黑名单,并设置过期时间。
黑名单示例: 这里用全局map来模拟redis使用
go
var revokedTokens = make(map[string]bool)
// 将 Token 添加到黑名单
func revokeToken(tokenString string) {
revokedTokens[tokenString] = true
}
// 检查 Token 是否被吊销
func isTokenRevoked(tokenString string) bool {
return revokedTokens[tokenString]
}
4.2 集成到中间件
每次鉴权时,检查 Token 是否在黑名单中,若存在则拒绝请求。
go
func authMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
authHeader := c.GetHeader("Authorization")
if authHeader == "" {
c.JSON(401, gin.H{"error": "Authorization token required"})
c.Abort()
return
}
// 检查 Token 是否被吊销
if isTokenRevoked(authHeader) {
c.JSON(401, gin.H{"error": "Token has been revoked"})
c.Abort()
return
}
// 解析和验证 Token ...
}
}
5. 最佳实践
-
设置适当的 Token 有效期:
- 根据业务场景设置合适的有效期,避免过长的 Token 使用窗口。
-
敏感操作要求重新认证:
- 对于支付等高敏感操作,可强制用户重新登录。
-
黑名单的清理机制:
- 使用 Redis 的 TTL 功能自动清理过期的吊销记录。
-
HTTPS 加密:
- 确保所有请求通过 HTTPS,防止 Token 在传输中被截获。
通过本篇文章,你已经掌握了在 Gin 框架中实现 JWT 认证的完整方案。从基础原理到登录接口的设计,再到鉴权中间件、Token 续期以及吊销机制,构建一个安全、灵活的认证系统不再是难题。