基于Gin、Golang与JWT的登出功能实现方案问询
Gin + JWT 实现登出功能方案
JWT属于无状态令牌,一旦签发无法主动作废,因此登出功能需要通过令牌黑名单机制实现:将已登出的令牌存入黑名单,在后续请求中拦截这些令牌。以下是具体实现步骤:
1. 准备黑名单存储
生产环境推荐使用Redis(支持自动过期清理),开发阶段可使用内存sync.Map(重启后失效)。这里以Redis为例:
先安装Redis依赖:
go get github.com/go-redis/redis/v8
初始化Redis客户端(示例在main.go中):
import ( "context" "log" "github.com/go-redis/redis/v8" ) var ctx = context.Background() var rdb *redis.Client func initRedis() { rdb = redis.NewClient(&redis.Options{ Addr: "localhost:6379", // Redis服务地址 Password: "", // 无密码则留空 DB: 0, // 使用默认数据库 }) // 测试连接有效性 _, err := rdb.Ping(ctx).Result() if err != nil { log.Fatalf("Redis连接失败: %v", err) } }
2. 实现登出GET接口
从请求头获取令牌,解析出过期时间后将令牌存入Redis,设置与令牌剩余有效期一致的过期时间:
func Logout(c *gin.Context) { authHeader := c.GetHeader("Authorization") if authHeader == "" { c.JSON(http.StatusBadRequest, gin.H{"erro": "令牌不能为空"}) return } // 处理Bearer前缀 tokenString := "" if len(authHeader) > 7 && authHeader[:7] == "Bearer " { tokenString = authHeader[7:] } else { tokenString = authHeader } // 解析令牌(仅提取exp声明,不验证签名) token, _, err := new(jwt.Parser).ParseUnverified(tokenString, jwt.MapClaims{}) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"erro": "无效的令牌"}) return } claims, ok := token.Claims.(jwt.MapClaims) if !ok { c.JSON(http.StatusBadRequest, gin.H{"erro": "无法解析令牌声明"}) return } exp, ok := claims["exp"].(float64) if !ok { c.JSON(http.StatusBadRequest, gin.H{"erro": "令牌缺少过期声明"}) return } // 计算令牌剩余有效期 remainingTime := time.Until(time.Unix(int64(exp), 0)) if remainingTime <= 0 { c.JSON(http.StatusOK, gin.H{"message": "令牌已过期"}) return } // 将令牌存入黑名单,设置自动过期 err = rdb.Set(ctx, "blacklist:"+tokenString, "invalid", remainingTime).Err() if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"erro": "登出失败"}) log.Println("Redis存入黑名单失败:", err) return } c.JSON(http.StatusOK, gin.H{"message": "登出成功"}) }
3. 修改认证中间件,增加黑名单校验
在原有令牌验证逻辑前,先检查令牌是否在黑名单中:
package middleware import ( "context" "log" "net/http" "os" "strings" "github.com/dgrijalva/jwt-go" "github.com/gin-gonic/gin" "github.com/go-redis/redis/v8" "github.com/joho/godotenv" ) var ( ctx = context.Background() rdb *redis.Client ) // 注入Redis客户端,避免循环依赖 func SetRedisClient(client *redis.Client) { rdb = client } func MiddlewareAutentication() gin.HandlerFunc { return func(c *gin.Context) { err := godotenv.Load("./.env") if err != nil { c.JSON(http.StatusInternalServerError, gin.H{ "erro ao carregar variavel de ambiente para middleware": err, }) log.Println("erro ao carregar variavel de ambiente para middleware", err) return } secret := os.Getenv("SECRET") authHeader := c.GetHeader("Authorization") if authHeader == "" { c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"message": "缺少授权令牌"}) return } // 处理Bearer前缀 tokenString := "" if len(authHeader) > 7 && strings.HasPrefix(authHeader, "Bearer ") { tokenString = authHeader[7:] } else { tokenString = authHeader } // 第一步:检查令牌是否在黑名单 exists, err := rdb.Exists(ctx, "blacklist:"+tokenString).Result() if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"message": "令牌验证失败"}) log.Println("黑名单检查失败:", err) return } if exists == 1 { c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"message": "令牌已失效"}) return } // 第二步:验证令牌签名与有效性 token, err := jwt.Parse(tokenString, func(t *jwt.Token) (interface{}, error) { // 验证签名算法是否合法 if _, ok := t.Method.(*jwt.SigningMethodHMAC); !ok { return nil, jwt.NewValidationError("无效的签名算法", jwt.ValidationErrorSignatureInvalid) } return []byte(secret), nil }) if err != nil || !token.Valid { c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"message": "Token invalido ou expirado"}) return } // 可选:将用户ID存入上下文,供后续接口使用 if claims, ok := token.Claims.(jwt.MapClaims); ok { c.Set("user_id", claims["sub"]) } c.Next() } }
4. 路由配置
在main.go中注册登出接口,并确保只有已认证用户可访问:
func main() { initRedis() middleware.SetRedisClient(rdb) r := gin.Default() // 登录接口(无需认证) r.POST("/login", Login) // 需认证的路由组 authGroup := r.Group("/api") authGroup.Use(middleware.MiddlewareAutentication()) { authGroup.GET("/logout", Logout) // 其他需认证的接口... } r.Run(":8080") }
注意事项
- 若使用内存
sync.Map替代Redis,需注意并发安全,但重启服务后黑名单会清空,仅适合开发测试。 - 必须保证JWT包含
exp过期声明,否则无法自动清理黑名单中的令牌。 - 原中间件未处理Bearer前缀的问题已在修改后的代码中修复。
内容的提问来源于stack exchange,提问作者Guilherme Rodrigues
相关产品推荐
相关产品推荐

