目 录CONTENT

文章目录
Go

Go设计一个高效jwt认证

过客
2025-11-26 / 0 评论 / 1 点赞 / 112 阅读 / 0 字

JWT(JSON Web Token)是一种开放标准(RFC 7519),用于在各方之间安全地以 JSON 对象的形式传输信息。这些信息可以被验证和信任,因为它们是经过数字签名的(可以使用 HMAC 算法或 RSA/ECDSA 公钥加密算法)。


一、JWT 的基本结构

一个 JWT 通常由三部分组成,用点(.)分隔:

  • ​Header(头部) ​:包含令牌类型(通常是 JWT)和所使用的签名算法(如 HS256 或 RS256)。
  • ​Payload(载荷) ​:包含声明(claims),即实际要传递的数据(如用户 ID、角色、过期时间等)。
  • ​Signature(签名) ​:用于验证消息在传输过程中没有被篡改,并验证发送方身份。

例如:

xxxxx.yyyyy.zzzzz

二、JWT 的优势 vs Session 与 普通 Token

特性 Session 普通 Token JWT
是否有状态 有状态(服务端存) 有状态(需查库) 无状态
扩展性 差(需共享 session) 一般 好(天然分布式)
性能 每次查 session 每次查 token 只需验签,不查库
跨域支持 弱(依赖 Cookie) 好 好
安全性 依赖 Cookie 安全 取决于实现 签名防篡改
撤销难度 容易(删 session) 容易(删 token) 难(需黑名单机制)

三、JWT高效设计

主要思路是使用 双令牌(访问令牌 + 刷新令牌)+ Redis黑名单机制 来实现高效设计,大致流程如下:

sequenceDiagram participant User as 用户 participant FE as 前端 (html+js) participant BE as 后端 (Go) Note over User,FE: 1. 用户输入账号密码 User->>FE: 提交登录表单 FE->>BE: POST /auth/login {username, password} Note over BE: 验证凭证<br/>生成 Access Token (15分钟)<br/>生成 Refresh Token (7天) BE-->>FE: { accessToken, refreshToken } FE->>FE: 存储 accessToken (内存)<br/>存储 refreshToken (HttpOnly Cookie) Note over User,FE: 2. 正常请求受保护资源 User->>FE: 访问网站页面 FE->>BE: GET /api/players<br/>Authorization: Bearer <accessToken> BE-->>FE: 返回网站数据 Note over FE,BE: 3. Access Token 过期 FE->>BE: GET /api/players<br/>Authorization: Bearer <expiredToken> BE-->>FE: 401 Unauthorized<br/>{ code: "TOKEN_EXPIRED" } Note over FE: 检测到 TOKEN_EXPIRED<br/>自动触发刷新流程 FE->>BE: POST /auth/refresh<br/>(携带 HttpOnly Cookie) BE->>BE: 验证 Refresh Token<br/>检查是否有效/未吊销 alt Refresh Token 有效 BE->>BE: 生成新的 Access Token BE-->>FE: { accessToken: "new_token" } FE->>FE: 更新内存中的 accessToken FE->>BE: 重试原请求<br/>GET /api/players<br/>Authorization: Bearer <newToken> BE-->>FE: 返回玩家数据 else Refresh Token 无效/过期 BE-->>FE: 401 Unauthorized<br/>{ code: "INVALID_REFRESH_TOKEN" } FE->>FE: 清除所有认证状态<br/>跳转到登录页 FE->>User: 跳转到登录页面 end

📋 核心概念

令牌类型 有效期 存储位置 用途 验证
Access Token 15-30分钟 内存 API请求认证 仅验证jwt数据合法性
Refresh Token 7-30天 HttpOnly Cookie 刷新Access Token 验证Redis中Refresh Token

四、代码实现

go后端实现

话不多说直接上代码,下面是完整的go demo代码

package main

import (
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"log"
	"net/http"
	"strings"
	"time"

	"github.com/golang-jwt/jwt/v5"
	"github.com/redis/go-redis/v9"
)

// 配置常量
const (
	AccessTokenExpiry   = 15 * time.Minute                                  // 访问令牌失效时间
	RefreshTokenExpiry  = 7 * 24 * time.Hour                                // 刷新令牌失效时间
	JWTAccessSecretKey  = "your-super-secret-key-change-this-in-production" // 访问令牌密钥
	JWTRefreshSecretKey = "your-super-secret-key-change-this-in-production" // 刷新令牌密钥
)

// Redis 连接
var redisClient *redis.Client

// User 用户结构
type User struct {
	ID       int    `json:"id"`
	Username string `json:"username"`
	Password string `json:"password"` // hashed
}

// TokenClaims JWT声明结构
type TokenClaims struct {
	UserID int `json:"user_id"`
	jwt.RegisteredClaims
}

func createJWTToken(userID int, secretKey string) (string, error) {
	claims := TokenClaims{
		UserID: userID,
		RegisteredClaims: jwt.RegisteredClaims{
			ExpiresAt: jwt.NewNumericDate(time.Now().Add(AccessTokenExpiry)),
			IssuedAt:  jwt.NewNumericDate(time.Now()),
			Issuer:    "game-backend",
		},
	}

	token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
	return token.SignedString([]byte(secretKey))
}

// validateJWTToken 验证JWT令牌
func validateJWTToken(tokenString string, secretKey string) (*TokenClaims, error) {
	token, err := jwt.ParseWithClaims(tokenString, &TokenClaims{}, func(token *jwt.Token) (interface{}, error) {
		if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
			return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
		}
		return []byte(secretKey), nil
	})

	if err != nil {
		return nil, err
	}

	if claims, ok := token.Claims.(*TokenClaims); ok && token.Valid {
		return claims, nil
	}

	return nil, fmt.Errorf("invalid token")
}

// createAccessToken 创建访问令牌
func createAccessToken(userID int) (string, error) {
	return createJWTToken(userID, JWTAccessSecretKey)
}

// createRefreshToken 创建刷新令牌
func createRefreshToken(userID int) (string, error) {
	token, err := createJWTToken(userID, JWTRefreshSecretKey)
	if err != nil {
		return "", err
	}

	// 将刷新令牌存储到Redis,设置过期时间
	ctx := context.Background()
	err = redisClient.SetEx(ctx, fmt.Sprintf("refresh_token:%d", userID), token, RefreshTokenExpiry).Err()
	if err != nil {
		return "", err
	}

	return token, nil
}

// validateAccessToken 验证访问JWT令牌
func validateAccessToken(tokenString string) (*TokenClaims, error) {
	return validateJWTToken(tokenString, JWTAccessSecretKey)
}

// validateRefreshToken 检查刷新令牌是否有效
func validateRefreshToken(token string) (*TokenClaims, error) {
	claims, err := validateJWTToken(token, JWTAccessSecretKey)
	if err != nil {
		return nil, err
	}

	// 从Redis获取存储的刷新令牌
	ctx := context.Background()
	storedRefreshToken, err := redisClient.Get(ctx, fmt.Sprintf("refresh_token:%d", claims.UserID)).Result()
	if errors.Is(err, redis.Nil) {
		return nil, err
	} else if err != nil {
		return nil, err
	} else if token != storedRefreshToken {
		return nil, fmt.Errorf("invalid refresh token")
	}

	return claims, nil
}

// revokeRefreshToken 吊销刷新令牌
func revokeRefreshToken(userID int) {
	redisClient.Del(context.Background(), fmt.Sprintf("refresh_token:%d", userID))
}

// AuthMiddleware 认证中间件
func AuthMiddleware(next http.HandlerFunc) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		authHeader := r.Header.Get("Authorization")
		if authHeader == "" {
			http.Error(w, `{"error": "Authorization header required"}`, http.StatusUnauthorized)
			return
		}

		// 检查是否为Bearer格式
		tokenString := ""
		if strings.HasPrefix(authHeader, "Bearer ") {
			tokenString = strings.TrimPrefix(authHeader, "Bearer ")
		} else {
			http.Error(w, `{"error": "Bearer token required"}`, http.StatusUnauthorized)
			return
		}

		claims, err := validateAccessToken(tokenString)
		if err != nil {
			http.Error(w, fmt.Sprintf(`{"error": "Invalid token: %v"}`, err), http.StatusUnauthorized)
			return
		}

		// 将用户ID添加到请求上下文
		ctx := context.WithValue(r.Context(), "userID", claims.UserID)
		next(w, r.WithContext(ctx))
	}
}

// LoginHandler 登录处理
func LoginHandler(w http.ResponseWriter, r *http.Request) {
	if r.Method != http.MethodPost {
		http.Error(w, `{"error": "Method not allowed"}`, http.StatusMethodNotAllowed)
		return
	}

	var req struct {
		Username string `json:"username"`
		Password string `json:"password"`
	}

	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
		http.Error(w, `{"error": "Invalid JSON"}`, http.StatusBadRequest)
		return
	}

	// TODO 从数据库读取 用户数据,并验证密码
	user := User{
		ID:       1,
		Username: req.Username,
		Password: req.Password,
	}

	// 创建访问令牌
	accessToken, err := createAccessToken(user.ID)
	if err != nil {
		http.Error(w, `{"error": "Failed to create access token"}`, http.StatusInternalServerError)
		return
	}

	// 创建刷新令牌
	refreshToken, err := createRefreshToken(user.ID)
	if err != nil {
		http.Error(w, `{"error": "Failed to create refresh token"}`, http.StatusInternalServerError)
		return
	}

	// 设置刷新令牌为HttpOnly Cookie
	http.SetCookie(w, &http.Cookie{
		Name:     "refresh_token",
		Value:    refreshToken,
		HttpOnly: true,
		Secure:   false, // 在生产环境中应为true(需要HTTPS)
		SameSite: http.SameSiteStrictMode,
		MaxAge:   int(RefreshTokenExpiry.Seconds()),
		Path:     "/",
	})

	// 返回访问令牌
	w.Header().Set("Content-Type", "application/json")
	json.NewEncoder(w).Encode(map[string]string{
		"accessToken": accessToken,
		"message":     "Login successful",
	})
}

// RefreshHandler 刷新令牌处理
func RefreshHandler(w http.ResponseWriter, r *http.Request) {
	if r.Method != http.MethodPost {
		http.Error(w, `{"error": "Method not allowed"}`, http.StatusMethodNotAllowed)
		return
	}

	// 从Cookie获取刷新令牌
	cookie, err := r.Cookie("refresh_token")
	if err != nil {
		http.Error(w, `{"error": "No refresh token in cookie"}`, http.StatusUnauthorized)
		return
	}

	refreshToken := cookie.Value

	// 检查刷新令牌是否有效
	claims, err := validateRefreshToken(refreshToken)
	if err != nil {
		http.Error(w, fmt.Sprintf(`{"error": "Invalid refresh token: %v"}`, err), http.StatusUnauthorized)
		return
	}

	// 创建新的访问令牌
	newAccessToken, err := createAccessToken(claims.UserID)
	if err != nil {
		http.Error(w, `{"error": "Failed to create new access token"}`, http.StatusInternalServerError)
		return
	}

	// 生成新的刷新令牌(令牌轮换)
	newRefreshToken, err := createRefreshToken(claims.UserID)
	if err != nil {
		http.Error(w, `{"error": "Failed to create new refresh token"}`, http.StatusInternalServerError)
		return
	}

	// 吊销旧的刷新令牌
	revokeRefreshToken(claims.UserID)

	// 设置新的刷新令牌Cookie
	http.SetCookie(w, &http.Cookie{
		Name:     "refresh_token",
		Value:    newRefreshToken,
		HttpOnly: true,
		Secure:   false, // 在生产环境中应为true
		SameSite: http.SameSiteStrictMode,
		MaxAge:   int(RefreshTokenExpiry.Seconds()),
		Path:     "/",
	})

	// 返回新的访问令牌
	w.Header().Set("Content-Type", "application/json")
	json.NewEncoder(w).Encode(map[string]string{
		"accessToken": newAccessToken,
		"message":     "Token refreshed successfully",
	})
}

// LogoutHandler 登出处理
func LogoutHandler(w http.ResponseWriter, r *http.Request) {
	if r.Method != http.MethodPost {
		http.Error(w, `{"error": "Method not allowed"}`, http.StatusMethodNotAllowed)
		return
	}

	// 从上下文获取用户ID
	userID := r.Context().Value("userID").(int)

	// 吊销刷新令牌
	revokeRefreshToken(userID)

	// 清除刷新令牌Cookie
	http.SetCookie(w, &http.Cookie{
		Name:     "refresh_token",
		Value:    "",
		HttpOnly: true,
		MaxAge:   -1,
		Path:     "/",
	})

	// 返回成功响应
	w.Header().Set("Content-Type", "application/json")
	json.NewEncoder(w).Encode(map[string]string{
		"message": "Logged out successfully",
	})
}

// PlayersHandler 受保护的API接口 - 玩家列表
func PlayersHandler(w http.ResponseWriter, r *http.Request) {
	if r.Method != http.MethodGet {
		http.Error(w, `{"error": "Method not allowed"}`, http.StatusMethodNotAllowed)
		return
	}

	// 从上下文获取用户ID
	userID := r.Context().Value("userID").(int)

	// 模拟玩家数据
	players := []map[string]interface{}{
		{"id": 1, "name": "Player1", "level": 10, "coins": 1000},
		{"id": 2, "name": "Player2", "level": 25, "coins": 2500},
		{"id": 3, "name": "Player3", "level": 5, "coins": 500},
	}

	// 添加访问者信息
	response := map[string]interface{}{
		"players":     players,
		"accessed_by": userID,
		"timestamp":   time.Now().Format(time.RFC3339),
	}

	w.Header().Set("Content-Type", "application/json")
	json.NewEncoder(w).Encode(response)
}

func main() {
	// 连接Redis
	opt, err := redis.ParseURL("redis://:123456@127.0.0.1:6379/0")
	if err != nil {
		return
	}
	redisClient = redis.NewClient(opt)

	// TODO 连接数据库

	// 无验证路由
	http.HandleFunc("/auth/login", LoginHandler)
	http.HandleFunc("/auth/refresh", RefreshHandler)

	// 验证路由
	http.HandleFunc("/auth/logout", AuthMiddleware(LogoutHandler))
	http.HandleFunc("/api/players", AuthMiddleware(PlayersHandler))

	// 启动服务器
	fmt.Println("Server starting on :8080")
	log.Fatal(http.ListenAndServe(":8080", nil))
}

前端部分代码

  • 前端登录请求
const login = async (credentials) => {
  const response = await fetch('/auth/login', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify(credentials)
  });
  
  const { accessToken } = await response.json();
  
  // 只存储 accessToken 到内存
  setAccessToken(accessToken);
  // refreshToken 自动存储在 HttpOnly Cookie 中
};
  • API请求拦截,访问Token失效,刷新Token
// Axios 请求拦截器
axios.interceptors.request.use(config => {
  if (accessToken) {
    config.headers.Authorization = `Bearer ${accessToken}`;
  }
  return config;
});

// 响应拦截器 - 处理令牌刷新
axios.interceptors.response.use(
  response => response,
  async error => {
    const originalRequest = error.config;
    
    if (error.response?.status === 401 && 
        error.response.data.code === "TOKEN_EXPIRED" && 
        !originalRequest._retry) {
      
      originalRequest._retry = true;
      
      try {
        // 刷新令牌
        const response = await axios.post('/auth/refresh');
        const { accessToken: newToken } = response.data;
        
        // 更新内存中的令牌
        setAccessToken(newToken);
        
        // 重试原请求
        originalRequest.headers.Authorization = `Bearer ${newToken}`;
        return axios(originalRequest);
      } catch (refreshError) {
        // 刷新失败,跳转登录
        logout();
        window.location.href = '/login';
        return Promise.reject(refreshError);
      }
    }
    
    return Promise.reject(error);
  }
);
1
Go
  1. 支付宝打赏

    qrcode alipay
  2. 微信打赏

    qrcode weixin

评论区