3.9.1 JWT 认证机制 #
JSON Web Token(JWT)是一种开放标准(RFC 7519),用于在各方之间安全地传输信息。JWT 是无状态的,这使得它特别适合分布式系统和微服务架构。
JWT 基础概念 #
JWT 结构 #
JWT 由三部分组成,用点(.)分隔:
Header.Payload.Signature
Header(头部):包含令牌类型和签名算法
{
"alg": "HS256",
"typ": "JWT"
}
Payload(载荷):包含声明(claims)
{
"sub": "1234567890",
"name": "John Doe",
"iat": 1516239022,
"exp": 1516242622
}
Signature(签名):用于验证令牌的完整性
HMACSHA256(
base64UrlEncode(header) + "." +
base64UrlEncode(payload),
secret
)
标准声明 #
JWT 定义了一些标准声明:
- iss(Issuer):令牌发行者
- sub(Subject):令牌主题
- aud(Audience):令牌受众
- exp(Expiration Time):过期时间
- nbf(Not Before):生效时间
- iat(Issued At):发行时间
- jti(JWT ID):令牌唯一标识
Go 中的 JWT 实现 #
安装依赖 #
go mod init jwt-demo
go get github.com/golang-jwt/jwt/v5
go get github.com/gin-gonic/gin
JWT 工具包 #
首先创建一个 JWT 工具包:
// pkg/jwt/jwt.go
package jwt
import (
"errors"
"time"
"github.com/golang-jwt/jwt/v5"
)
var (
ErrTokenExpired = errors.New("token已过期")
ErrTokenNotValidYet = errors.New("token尚未生效")
ErrTokenMalformed = errors.New("token格式错误")
ErrTokenInvalid = errors.New("token无效")
)
// Claims 自定义声明
type Claims struct {
UserID uint `json:"user_id"`
Username string `json:"username"`
Role string `json:"role"`
jwt.RegisteredClaims
}
// JWTManager JWT管理器
type JWTManager struct {
secretKey []byte
tokenDuration time.Duration
}
// NewJWTManager 创建JWT管理器
func NewJWTManager(secretKey string, tokenDuration time.Duration) *JWTManager {
return &JWTManager{
secretKey: []byte(secretKey),
tokenDuration: tokenDuration,
}
}
// GenerateToken 生成JWT令牌
func (manager *JWTManager) GenerateToken(userID uint, username, role string) (string, error) {
now := time.Now()
claims := Claims{
UserID: userID,
Username: username,
Role: role,
RegisteredClaims: jwt.RegisteredClaims{
Issuer: "jwt-demo",
Subject: username,
Audience: []string{"web"},
ExpiresAt: jwt.NewNumericDate(now.Add(manager.tokenDuration)),
NotBefore: jwt.NewNumericDate(now),
IssuedAt: jwt.NewNumericDate(now),
},
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
return token.SignedString(manager.secretKey)
}
// VerifyToken 验证JWT令牌
func (manager *JWTManager) VerifyToken(tokenString string) (*Claims, error) {
token, err := jwt.ParseWithClaims(
tokenString,
&Claims{},
func(token *jwt.Token) (interface{}, error) {
return manager.secretKey, nil
},
)
if err != nil {
if ve, ok := err.(*jwt.ValidationError); ok {
switch {
case ve.Errors&jwt.ValidationErrorMalformed != 0:
return nil, ErrTokenMalformed
case ve.Errors&jwt.ValidationErrorExpired != 0:
return nil, ErrTokenExpired
case ve.Errors&jwt.ValidationErrorNotValidYet != 0:
return nil, ErrTokenNotValidYet
default:
return nil, ErrTokenInvalid
}
}
return nil, err
}
if claims, ok := token.Claims.(*Claims); ok && token.Valid {
return claims, nil
}
return nil, ErrTokenInvalid
}
// RefreshToken 刷新令牌
func (manager *JWTManager) RefreshToken(tokenString string) (string, error) {
claims, err := manager.VerifyToken(tokenString)
if err != nil {
return "", err
}
// 检查令牌是否即将过期(在过期前30分钟内可以刷新)
if time.Until(claims.ExpiresAt.Time) > 30*time.Minute {
return "", errors.New("令牌尚未到刷新时间")
}
return manager.GenerateToken(claims.UserID, claims.Username, claims.Role)
}
用户模型和服务 #
// models/user.go
package models
import (
"golang.org/x/crypto/bcrypt"
"gorm.io/gorm"
)
type User struct {
ID uint `json:"id" gorm:"primaryKey"`
Username string `json:"username" gorm:"unique;not null"`
Email string `json:"email" gorm:"unique;not null"`
Password string `json:"-" gorm:"not null"`
Role string `json:"role" gorm:"default:user"`
IsActive bool `json:"is_active" gorm:"default:true"`
gorm.Model
}
// HashPassword 加密密码
func (u *User) HashPassword() error {
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(u.Password), bcrypt.DefaultCost)
if err != nil {
return err
}
u.Password = string(hashedPassword)
return nil
}
// CheckPassword 验证密码
func (u *User) CheckPassword(password string) bool {
err := bcrypt.CompareHashAndPassword([]byte(u.Password), []byte(password))
return err == nil
}
// UserService 用户服务
type UserService struct {
db *gorm.DB
}
func NewUserService(db *gorm.DB) *UserService {
return &UserService{db: db}
}
// CreateUser 创建用户
func (s *UserService) CreateUser(user *User) error {
if err := user.HashPassword(); err != nil {
return err
}
return s.db.Create(user).Error
}
// GetUserByUsername 根据用户名获取用户
func (s *UserService) GetUserByUsername(username string) (*User, error) {
var user User
err := s.db.Where("username = ? AND is_active = ?", username, true).First(&user).Error
return &user, err
}
// GetUserByID 根据ID获取用户
func (s *UserService) GetUserByID(id uint) (*User, error) {
var user User
err := s.db.Where("id = ? AND is_active = ?", id, true).First(&user).Error
return &user, err
}
认证中间件 #
// middleware/auth.go
package middleware
import (
"net/http"
"strings"
"github.com/gin-gonic/gin"
"your-project/pkg/jwt"
)
// AuthMiddleware JWT认证中间件
func AuthMiddleware(jwtManager *jwt.JWTManager) gin.HandlerFunc {
return func(c *gin.Context) {
// 从Header中获取Authorization
authHeader := c.GetHeader("Authorization")
if authHeader == "" {
c.JSON(http.StatusUnauthorized, gin.H{
"error": "缺少Authorization头",
})
c.Abort()
return
}
// 检查Bearer前缀
parts := strings.SplitN(authHeader, " ", 2)
if len(parts) != 2 || parts[0] != "Bearer" {
c.JSON(http.StatusUnauthorized, gin.H{
"error": "Authorization头格式错误",
})
c.Abort()
return
}
// 验证令牌
claims, err := jwtManager.VerifyToken(parts[1])
if err != nil {
c.JSON(http.StatusUnauthorized, gin.H{
"error": err.Error(),
})
c.Abort()
return
}
// 将用户信息存储到上下文
c.Set("user_id", claims.UserID)
c.Set("username", claims.Username)
c.Set("role", claims.Role)
c.Set("claims", claims)
c.Next()
}
}
// RequireRole 角色验证中间件
func RequireRole(roles ...string) gin.HandlerFunc {
return func(c *gin.Context) {
userRole, exists := c.Get("role")
if !exists {
c.JSON(http.StatusForbidden, gin.H{
"error": "无法获取用户角色",
})
c.Abort()
return
}
role := userRole.(string)
for _, requiredRole := range roles {
if role == requiredRole {
c.Next()
return
}
}
c.JSON(http.StatusForbidden, gin.H{
"error": "权限不足",
})
c.Abort()
}
}
认证控制器 #
// controllers/auth.go
package controllers
import (
"net/http"
"time"
"github.com/gin-gonic/gin"
"your-project/models"
"your-project/pkg/jwt"
)
type AuthController struct {
userService *models.UserService
jwtManager *jwt.JWTManager
}
func NewAuthController(userService *models.UserService, jwtManager *jwt.JWTManager) *AuthController {
return &AuthController{
userService: userService,
jwtManager: jwtManager,
}
}
// LoginRequest 登录请求
type LoginRequest struct {
Username string `json:"username" binding:"required"`
Password string `json:"password" binding:"required"`
}
// RegisterRequest 注册请求
type RegisterRequest struct {
Username string `json:"username" binding:"required,min=3,max=20"`
Email string `json:"email" binding:"required,email"`
Password string `json:"password" binding:"required,min=6"`
}
// LoginResponse 登录响应
type LoginResponse struct {
Token string `json:"token"`
ExpiresAt time.Time `json:"expires_at"`
User *models.User `json:"user"`
}
// Register 用户注册
func (ctrl *AuthController) Register(c *gin.Context) {
var req RegisterRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"error": err.Error(),
})
return
}
user := &models.User{
Username: req.Username,
Email: req.Email,
Password: req.Password,
Role: "user",
}
if err := ctrl.userService.CreateUser(user); err != nil {
c.JSON(http.StatusConflict, gin.H{
"error": "用户名或邮箱已存在",
})
return
}
c.JSON(http.StatusCreated, gin.H{
"message": "注册成功",
"user": user,
})
}
// Login 用户登录
func (ctrl *AuthController) Login(c *gin.Context) {
var req LoginRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"error": err.Error(),
})
return
}
// 验证用户
user, err := ctrl.userService.GetUserByUsername(req.Username)
if err != nil {
c.JSON(http.StatusUnauthorized, gin.H{
"error": "用户名或密码错误",
})
return
}
if !user.CheckPassword(req.Password) {
c.JSON(http.StatusUnauthorized, gin.H{
"error": "用户名或密码错误",
})
return
}
// 生成JWT令牌
token, err := ctrl.jwtManager.GenerateToken(user.ID, user.Username, user.Role)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"error": "生成令牌失败",
})
return
}
c.JSON(http.StatusOK, LoginResponse{
Token: token,
ExpiresAt: time.Now().Add(24 * time.Hour),
User: user,
})
}
// RefreshToken 刷新令牌
func (ctrl *AuthController) RefreshToken(c *gin.Context) {
authHeader := c.GetHeader("Authorization")
if authHeader == "" {
c.JSON(http.StatusBadRequest, gin.H{
"error": "缺少Authorization头",
})
return
}
parts := strings.SplitN(authHeader, " ", 2)
if len(parts) != 2 || parts[0] != "Bearer" {
c.JSON(http.StatusBadRequest, gin.H{
"error": "Authorization头格式错误",
})
return
}
newToken, err := ctrl.jwtManager.RefreshToken(parts[1])
if err != nil {
c.JSON(http.StatusUnauthorized, gin.H{
"error": err.Error(),
})
return
}
c.JSON(http.StatusOK, gin.H{
"token": newToken,
"expires_at": time.Now().Add(24 * time.Hour),
})
}
// GetProfile 获取用户信息
func (ctrl *AuthController) GetProfile(c *gin.Context) {
userID, _ := c.Get("user_id")
user, err := ctrl.userService.GetUserByID(userID.(uint))
if err != nil {
c.JSON(http.StatusNotFound, gin.H{
"error": "用户不存在",
})
return
}
c.JSON(http.StatusOK, gin.H{
"user": user,
})
}
主程序 #
// main.go
package main
import (
"log"
"time"
"github.com/gin-gonic/gin"
"gorm.io/driver/sqlite"
"gorm.io/gorm"
"your-project/controllers"
"your-project/middleware"
"your-project/models"
"your-project/pkg/jwt"
)
func main() {
// 初始化数据库
db, err := gorm.Open(sqlite.Open("test.db"), &gorm.Config{})
if err != nil {
log.Fatal("数据库连接失败:", err)
}
// 自动迁移
db.AutoMigrate(&models.User{})
// 初始化服务
userService := models.NewUserService(db)
jwtManager := jwt.NewJWTManager("your-secret-key", 24*time.Hour)
// 初始化控制器
authController := controllers.NewAuthController(userService, jwtManager)
// 初始化路由
r := gin.Default()
// 公开路由
public := r.Group("/api/v1")
{
public.POST("/register", authController.Register)
public.POST("/login", authController.Login)
public.POST("/refresh", authController.RefreshToken)
}
// 需要认证的路由
protected := r.Group("/api/v1")
protected.Use(middleware.AuthMiddleware(jwtManager))
{
protected.GET("/profile", authController.GetProfile)
// 需要管理员权限的路由
admin := protected.Group("/admin")
admin.Use(middleware.RequireRole("admin"))
{
admin.GET("/users", func(c *gin.Context) {
c.JSON(200, gin.H{"message": "管理员专用接口"})
})
}
}
log.Println("服务器启动在 :8080")
r.Run(":8080")
}
JWT 最佳实践 #
1. 安全考虑 #
// 使用强密钥
const SecretKey = "your-very-long-and-random-secret-key-here"
// 设置合理的过期时间
const TokenDuration = 15 * time.Minute // 访问令牌15分钟
const RefreshDuration = 7 * 24 * time.Hour // 刷新令牌7天
2. 令牌黑名单 #
// pkg/jwt/blacklist.go
package jwt
import (
"sync"
"time"
)
// TokenBlacklist 令牌黑名单
type TokenBlacklist struct {
tokens map[string]time.Time
mutex sync.RWMutex
}
func NewTokenBlacklist() *TokenBlacklist {
bl := &TokenBlacklist{
tokens: make(map[string]time.Time),
}
// 定期清理过期令牌
go bl.cleanup()
return bl
}
// Add 添加令牌到黑名单
func (bl *TokenBlacklist) Add(tokenID string, expiry time.Time) {
bl.mutex.Lock()
defer bl.mutex.Unlock()
bl.tokens[tokenID] = expiry
}
// IsBlacklisted 检查令牌是否在黑名单中
func (bl *TokenBlacklist) IsBlacklisted(tokenID string) bool {
bl.mutex.RLock()
defer bl.mutex.RUnlock()
expiry, exists := bl.tokens[tokenID]
if !exists {
return false
}
// 如果令牌已过期,从黑名单中移除
if time.Now().After(expiry) {
delete(bl.tokens, tokenID)
return false
}
return true
}
// cleanup 清理过期令牌
func (bl *TokenBlacklist) cleanup() {
ticker := time.NewTicker(1 * time.Hour)
defer ticker.Stop()
for range ticker.C {
bl.mutex.Lock()
now := time.Now()
for tokenID, expiry := range bl.tokens {
if now.After(expiry) {
delete(bl.tokens, tokenID)
}
}
bl.mutex.Unlock()
}
}
3. 双令牌机制 #
// TokenPair 令牌对
type TokenPair struct {
AccessToken string `json:"access_token"`
RefreshToken string `json:"refresh_token"`
ExpiresAt time.Time `json:"expires_at"`
}
// GenerateTokenPair 生成令牌对
func (manager *JWTManager) GenerateTokenPair(userID uint, username, role string) (*TokenPair, error) {
// 生成访问令牌(短期)
accessToken, err := manager.generateToken(userID, username, role, 15*time.Minute, "access")
if err != nil {
return nil, err
}
// 生成刷新令牌(长期)
refreshToken, err := manager.generateToken(userID, username, role, 7*24*time.Hour, "refresh")
if err != nil {
return nil, err
}
return &TokenPair{
AccessToken: accessToken,
RefreshToken: refreshToken,
ExpiresAt: time.Now().Add(15 * time.Minute),
}, nil
}
测试示例 #
# 注册用户
curl -X POST http://localhost:8080/api/v1/register \
-H "Content-Type: application/json" \
-d '{
"username": "testuser",
"email": "[email protected]",
"password": "password123"
}'
# 用户登录
curl -X POST http://localhost:8080/api/v1/login \
-H "Content-Type: application/json" \
-d '{
"username": "testuser",
"password": "password123"
}'
# 访问受保护的接口
curl -X GET http://localhost:8080/api/v1/profile \
-H "Authorization: Bearer YOUR_JWT_TOKEN"
JWT 认证机制为现代 Web 应用提供了一种无状态、可扩展的认证解决方案。通过合理的设计和安全实践,可以构建出既安全又高效的认证系统。