You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

基于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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.30 09:10:49