Gin JWT认证与鉴权
JWT(JSON Web Token)是无状态认证的标准方案,适合分布式系统和 API 认证。
JWT 结构
JWT 由三部分组成:Header.Payload.Signature
Bash
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoxLCJleHAiOjE2ODAwMDAwMDB9.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
| 部分 | 说明 | 内容 |
|---|---|---|
| Header | 算法信息 | {"alg":"HS256","typ":"JWT"} |
| Payload | 用户数据 | {"user_id":1,"exp":1680000000} |
| Signature | 签名 | 防篡改验证 |
安装依赖
Go
go get github.com/golang-jwt/jwt/v5
JWT 生成与验证
基础实现
Go
import (
"github.com/golang-jwt/jwt/v5"
"time"
)
var jwtSecret = []byte("your-secret-key")
type Claims struct {
UserID uint `json:"user_id"`
Username string `json:"username"`
jwt.RegisteredClaims
}
// 生成 Token
func GenerateToken(userID uint, username string) (string, error) {
claims := Claims{
UserID: userID,
Username: username,
RegisteredClaims: jwt.RegisteredClaims{
ExpiresAt: jwt.NewNumericDate(time.Now().Add(24 * time.Hour)),
IssuedAt: jwt.NewNumericDate(time.Now()),
Issuer: "gin-app",
},
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
return token.SignedString(jwtSecret)
}
// 验证 Token
func ParseToken(tokenString string) (*Claims, error) {
token, err := jwt.ParseWithClaims(tokenString, &Claims{}, func(token *jwt.Token) (interface{}, error) {
return jwtSecret, nil
})
if err != nil {
return nil, err
}
if claims, ok := token.Claims.(*Claims); ok && token.Valid {
return claims, nil
}
return nil, jwt.ErrSignatureInvalid
}
Gin 中间件实现
JWT 认证中间件
Go
func JWTAuthMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
// 从 Header 获取 Token
authHeader := c.GetHeader("Authorization")
if authHeader == "" {
c.JSON(401, gin.H{"error": "未提供认证令牌"})
c.Abort()
return
}
// Bearer Token 格式
parts := strings.SplitN(authHeader, " ", 2)
if !(len(parts) == 2 && parts[0] == "Bearer") {
c.JSON(401, gin.H{"error": "认证格式错误"})
c.Abort()
return
}
// 验证 Token
claims, err := ParseToken(parts[1])
if err != nil {
c.JSON(401, gin.H{"error": "无效的认证令牌"})
c.Abort()
return
}
// 存储用户信息到上下文
c.Set("user_id", claims.UserID)
c.Set("username", claims.Username)
c.Next()
}
}
路由使用
Go
func main() {
r := gin.Default()
// 公开路由
r.POST("/login", LoginHandler)
r.POST("/register", RegisterHandler)
// 需要认证的路由
authorized := r.Group("/api")
authorized.Use(JWTAuthMiddleware())
{
authorized.GET("/profile", ProfileHandler)
authorized.PUT("/profile", UpdateProfileHandler)
}
r.Run(":8080")
}
登录流程实现
Go
type LoginRequest struct {
Username string `json:"username" binding:"required"`
Password string `json:"password" binding:"required"`
}
func LoginHandler(c *gin.Context) {
var req LoginRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(400, gin.H{"error": "参数错误"})
return
}
// 验证用户名密码
user, err := authenticateUser(req.Username, req.Password)
if err != nil {
c.JSON(401, gin.H{"error": "用户名或密码错误"})
return
}
// 生成 Token
token, err := GenerateToken(user.ID, user.Username)
if err != nil {
c.JSON(500, gin.H{"error": "生成令牌失败"})
return
}
c.JSON(200, gin.H{
"token": token,
"user": gin.H{
"id": user.ID,
"username": user.Username,
},
})
}
Token 刷新机制
Go
// 双 Token 方案:Access Token + Refresh Token
func GenerateTokenPair(userID uint, username string) (accessToken, refreshToken string, err error) {
// 短期 Access Token (15分钟)
accessClaims := Claims{
UserID: userID,
Username: username,
RegisteredClaims: jwt.RegisteredClaims{
ExpiresAt: jwt.NewNumericDate(time.Now().Add(15 * time.Minute)),
IssuedAt: jwt.NewNumericDate(time.Now()),
},
}
accessToken, err = jwt.NewWithClaims(jwt.SigningMethodHS256, accessClaims).SignedString(jwtSecret)
if err != nil {
return "", "", err
}
// 长期 Refresh Token (7天)
refreshClaims := jwt.RegisteredClaims{
Subject: string(rune(userID)),
ExpiresAt: jwt.NewNumericDate(time.Now().Add(7 * 24 * time.Hour)),
IssuedAt: jwt.NewNumericDate(time.Now()),
}
refreshToken, err = jwt.NewWithClaims(jwt.SigningMethodHS256, refreshClaims).SignedString(jwtSecret)
return accessToken, refreshToken, err
}
// 刷新接口
func RefreshTokenHandler(c *gin.Context) {
refreshToken := c.PostForm("refresh_token")
if refreshToken == "" {
c.JSON(400, gin.H{"error": "缺少 refresh token"})
return
}
token, err := jwt.ParseWithClaims(refreshToken, &jwt.RegisteredClaims{}, func(token *jwt.Token) (interface{}, error) {
return jwtSecret, nil
})
if err != nil || !token.Valid {
c.JSON(401, gin.H{"error": "无效的 refresh token"})
return
}
// 生成新的 Access Token
claims := token.Claims.(*jwt.RegisteredClaims)
userID := uint([]rune(claims.Subject)[0])
newAccessToken, err := GenerateToken(userID, "")
if err != nil {
c.JSON(500, gin.H{"error": "生成令牌失败"})
return
}
c.JSON(200, gin.H{
"access_token": newAccessToken,
})
}
安全配置
Go
import (
"crypto/sha256"
"encoding/base64"
)
// 使用更强的签名密钥
func generateSecureSecret() []byte {
// 生产环境应从配置或环境变量读取
secret := os.Getenv("JWT_SECRET")
if secret == "" {
panic("JWT_SECRET 环境变量未设置")
}
hash := sha256.Sum256([]byte(secret))
return hash[:]
}
// Token 黑名单(登出失效)
type TokenBlacklist struct {
tokens map[string]time.Time
mu sync.RWMutex
}
var blacklist = &TokenBlacklist{
tokens: make(map[string]time.Time),
}
func (b *TokenBlacklist) Add(token string, exp time.Time) {
b.mu.Lock()
defer b.mu.Unlock()
b.tokens[token] = exp
}
func (b *TokenBlacklist) Contains(token string) bool {
b.mu.RLock()
defer b.mu.RUnlock()
_, exists := b.tokens[token]
return exists
}
func (b *TokenBlacklist) Cleanup() {
b.mu.Lock()
defer b.mu.Unlock()
now := time.Now()
for token, exp := range b.tokens {
if exp.Before(now) {
delete(b.tokens, token)
}
}
}
// 登出处理
func LogoutHandler(c *gin.Context) {
authHeader := c.GetHeader("Authorization")
if authHeader != "" {
parts := strings.SplitN(authHeader, " ", 2)
if len(parts) == 2 {
blacklist.Add(parts[1], time.Now().Add(24*time.Hour))
}
}
c.JSON(200, gin.H{"message": "登出成功"})
}
最佳实践
text
func SecurityMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
// 检查黑名单
authHeader := c.GetHeader("Authorization")
if authHeader != "" {
parts := strings.SplitN(authHeader, " ", 2)
if len(parts) == 2 && blacklist.Contains(parts[1]) {
c.JSON(401, gin.H{"error": "令牌已失效"})
c.Abort()
return
}
}
c.Next()
}
}
JWT 配置对比
| 配置项 | 推荐值 | 说明 |
|---|---|---|
| 签名算法 | HS256/RS256 | RS256 安全性更高 |
| Access Token 有效期 | 15-30分钟 | 短期有效降低风险 |
| Refresh Token 有效期 | 7天-30天 | 便于用户使用 |
| 密钥长度 | 256位+ | 防止暴力破解 |
注意:生产环境密钥应从环境变量读取,禁止硬编码。
要点总结
- Token 生成:包含用户 ID、过期时间,使用安全密钥签名
- 中间件验证:解析 Token 并将用户信息存入上下文
- 双 Token 方案:Access Token 短期 + Refresh Token 长期
- 安全措施:黑名单机制、安全密钥、HTTPS 传输
- 密钥管理:使用环境变量,禁止代码硬编码
📝 发现内容有误?点击此处直接编辑