Appearance
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 机制。