Skip to content

24|认证与 JWT

go-gateway 的管理后台暴露在 HTTP 接口上,任何人都能调用的话,路由配置可能被恶意修改。认证解决"你是谁"的问题,鉴权解决"你能做什么"的问题。JWT(JSON Web Token)是目前最流行的无状态认证方案,服务端签发一个签名令牌,客户端后续请求带上这个令牌,服务端验证签名即可确认身份,不需要在服务端保存会话状态。

一、Session vs Token

方案存储位置优点缺点
Session服务端内存/Redis安全性高,可随时销毁有状态,分布式部署需要共享存储
JWT客户端(Header/Cookie)无状态,天然适合分布式令牌签发后无法提前失效(除非加黑名单)

网关的管理后台适合用 JWT:管理节点可能多实例部署,无状态设计更简洁。

二、JWT 结构

JWT 由三部分组成,用点号分隔:

Header.Payload.Signature
  • Header:算法类型(如 HS256)
  • Payload:声明(claims),如用户 ID、过期时间、签发时间
  • Signature:用密钥对前两部分签名,防止篡改

Payload 里的 claims:

字段含义
sub主题(用户ID)
iat签发时间
exp过期时间
jti令牌唯一标识

三、签发与验证

bash
go get github.com/golang-jwt/jwt/v5

签发 Token:

go
package main

import (
	"time"
	"github.com/golang-jwt/jwt/v5"
)

var secretKey = []byte("your-secret-key")

func generateToken(userID string) (string, error) {
	token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
		"sub": userID,
		"iat": time.Now().Unix(),
		"exp": time.Now().Add(24 * time.Hour).Unix(),
	})
	return token.SignedString(secretKey)
}

验证 Token:

go
func parseToken(tokenString string) (*jwt.Token, error) {
	return jwt.Parse(tokenString, 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 secretKey, nil
	})
}

验证时检查签名算法,防止算法混淆攻击(攻击者把算法改成 none 绕过签名)。

四、Gin 鉴权中间件

管理 API 路由组加 JWT 鉴权中间件:

go
func AuthMiddleware() gin.HandlerFunc {
	return func(c *gin.Context) {
		authHeader := c.GetHeader("Authorization")
		if authHeader == "" {
			c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "missing token"})
			return
		}

		// Bearer <token>
		parts := strings.SplitN(authHeader, " ", 2)
		if len(parts) != 2 || parts[0] != "Bearer" {
			c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "invalid token format"})
			return
		}

		token, err := parseToken(parts[1])
		if err != nil || !token.Valid {
			c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "invalid token"})
			return
		}

		claims, _ := token.Claims.(jwt.MapClaims)
		c.Set("user_id", claims["sub"])
		c.Next()
	}
}

注册到管理路由组:

go
admin := r.Group("/admin")
admin.Use(AuthMiddleware())
{
	admin.GET("/routes", listRoutes)
	admin.POST("/routes", addRoute)
}

未携带有效 Token 的请求会被 401 拒绝。

五、Refresh Token

Access Token 过期时间通常很短(15 分钟到 1 小时),减少被盗后的风险。Refresh Token 过期时间长(7 天到 30 天),用于换取新的 Access Token。

登录接口返回两个令牌:

go
func login(c *gin.Context) {
	// 验证用户名密码...
	accessToken, _ := generateToken(userID, 1*time.Hour)
	refreshToken, _ := generateToken(userID, 7*24*time.Hour)

	c.JSON(http.StatusOK, gin.H{
		"access_token":  accessToken,
		"refresh_token": refreshToken,
		"expires_in":    3600,
	})
}

刷新接口:

go
func refresh(c *gin.Context) {
	var req struct {
		RefreshToken string `json:"refresh_token"`
	}
	c.ShouldBindJSON(&req)

	token, _ := parseToken(req.RefreshToken)
	claims, _ := token.Claims.(jwt.MapClaims)
	userID := claims["sub"].(string)

	newAccessToken, _ := generateToken(userID, 1*time.Hour)
	c.JSON(http.StatusOK, gin.H{"access_token": newAccessToken})
}

Refresh Token 被盗风险更高,通常存到 HttpOnly Cookie 里,或绑定设备指纹。

六、JWT 黑名单

JWT 一旦签发,在过期前无法提前失效。需要登出或发现令牌泄漏时,把 Token 的 jti 加入 Redis 黑名单:

go
func revokeToken(jti string, exp time.Time) error {
	ttl := time.Until(exp)
	return redisClient.Set(ctx, "blacklist:"+jti, "1", ttl).Err()
}

func isRevoked(jti string) bool {
	exists, _ := redisClient.Exists(ctx, "blacklist:"+jti).Result()
	return exists > 0
}

黑名单只在 Token 过期前有效,过期后自动清理。

七、常见错误

密钥硬编码

go
var secretKey = []byte("123456") // 错误:密钥太简单,容易被破解

生产环境密钥应该从环境变量或密钥管理服务读取,长度至少 256 位。

不验证签名算法

go
token, _ := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
	return secretKey, nil // 错误:没有检查算法
})

攻击者可以把 alg 改成 none,服务端不检查就会通过验证。

在客户端存储敏感信息

JWT Payload 只是 Base64 编码,没有加密。不要把密码、权限列表等敏感信息放进 Payload,任何人都能解码看到。

Token 过期时间太长

Access Token 过期时间设成 30 天,被盗后可以长期滥用。建议 Access Token 1 小时以内,配合 Refresh Token 机制。