目 录CONTENT

文章目录
Go

Go设计一个高效jwt认证

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

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


一、JWT 的基本结构

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

  • Header(头部) ​:包含令牌类型(通常是 JWT)和所使用的签名算法(如 HS256RS256)。
  • 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

评论区