前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >手把手,带你从零封装Gin框架(八):引入Redis & 解决 JWT 注销问题(黑名单策略)

手把手,带你从零封装Gin框架(八):引入Redis & 解决 JWT 注销问题(黑名单策略)

作者头像
玖柒的小窝
修改2021-10-25 10:34:54
2.2K0
修改2021-10-25 10:34:54
举报
文章被收录于专栏:各类技术文章~各类技术文章~

前言

由于 JWT 是无状态的,只能等到它的有效期过了才会失效,服务端无法主动让一个 token 失效,为了解决这个问题,我这里使用黑名单策略来解决 JWT 的注销问题,简单来说就将用户主动注销的 token 加入到黑名单(Redis)中,并且必须设置有效期,否则将导致黑名单巨大的问题,然后在 Jwt 中间件鉴权时判断 token 是否在黑名单中

安装

代码语言:javascript
复制
go get -u github.com/go-redis/redis/v8
复制代码

定义配置项

新建 config/redis.go 文件,编写配置

代码语言:javascript
复制
package config

type Redis struct {
    Host string `mapstructure:"host" json:"host" yaml:"host"`
    Port int `mapstructure:"port" json:"port" yaml:"port"`
    DB int `mapstructure:"db" json:"db" yaml:"db"`
    Password string `mapstructure:"password" json:"password" yaml:"password"`
}
复制代码

config/config.go 中,添加 Redis 属性

代码语言:javascript
复制
package config

type Configuration struct {
    App App `mapstructure:"app" json:"app" yaml:"app"`
    Log Log `mapstructure:"log" json:"log" yaml:"log"`
    Database Database `mapstructure:"database" json:"database" yaml:"database"`
    Jwt Jwt `mapstructure:"jwt" json:"jwt" yaml:"jwt"`
    Redis Redis `mapstructure:"redis" json:"redis" yaml:"redis"`
}
复制代码

config/jwt.go 中,添加 JwtBlacklistGracePeriod 属性

代码语言:javascript
复制
package config

type Jwt struct {
    Secret string `mapstructure:"secret" json:"secret" yaml:"secret"`
    JwtTtl int64 `mapstructure:"jwt_ttl" json:"jwt_ttl" yaml:"jwt_ttl"` // token 有效期(秒)
    JwtBlacklistGracePeriod int64 `mapstructure:"jwt_blacklist_grace_period" json:"jwt_blacklist_grace_period" yaml:"jwt_blacklist_grace_period"` // 黑名单宽限时间(秒)
}
复制代码

config.yaml 添加对应配置

代码语言:javascript
复制
redis:
  host: 127.0.0.1
  port: 6379
  db: 0
  password:

jwt:
  jwt_blacklist_grace_period: 10
复制代码

初始化 Redis

新建 bootstrap/redis.go 文件,编写

代码语言:javascript
复制
package bootstrap

import (
    "context"
    "github.com/go-redis/redis/v8"
    "go.uber.org/zap"
    "jassue-gin/global"
)

func InitializeRedis() *redis.Client {
    client := redis.NewClient(&redis.Options{
        Addr:     global.App.Config.Redis.Host + ":" + global.App.Config.Redis.Port,
        Password: global.App.Config.Redis.Password, // no password set
        DB:       global.App.Config.Redis.DB,       // use default DB
    })
    _, err := client.Ping(context.Background()).Result()
    if err != nil {
        global.App.Log.Error("Redis connect ping failed, err:", zap.Any("err", err))
        return nil
    }
    return client
}
复制代码

global/app.go 中,Application 结构体添加 Redis 属性

代码语言:javascript
复制
type Application struct {
    // ...
    Redis *redis.Client
}
复制代码

main.go 中,调用 InitializeRedis()

代码语言:javascript
复制
func main() {
    // ...

    // 初始化验证器
    bootstrap.InitializeValidator()

    // 初始化Redis
    global.App.Redis = bootstrap.InitializeRedis()

    // 启动服务器
    bootstrap.RunServer()
}
复制代码

编写黑名单相关逻辑

新建 utils/md5.go 文件,编写 MD5() 用于 token 编码

代码语言:javascript
复制
package utils

import (
    "crypto/md5"
    "encoding/hex"
)

func MD5(str []byte, b ...byte) string {
    h := md5.New()
    h.Write(str)
    return hex.EncodeToString(h.Sum(b))
}
复制代码

app/services/jwt.go 中,编写:

代码语言:javascript
复制
// 获取黑名单缓存 key
func (jwtService *jwtService) getBlackListKey(tokenStr string) string {
    return "jwt_black_list:" + utils.MD5([]byte(tokenStr))
}

// JoinBlackList token 加入黑名单
func (jwtService *jwtService) JoinBlackList(token *jwt.Token) (err error) {
    nowUnix := time.Now().Unix()
    timer := time.Duration(token.Claims.(*CustomClaims).ExpiresAt - nowUnix) * time.Second
    // 将 token 剩余时间设置为缓存有效期,并将当前时间作为缓存 value 值
    err = global.App.Redis.SetNX(context.Background(), jwtService.getBlackListKey(token.Raw), nowUnix, timer).Err()
    return
}

// IsInBlacklist token 是否在黑名单中
func (jwtService *jwtService) IsInBlacklist(tokenStr string) bool {
    joinUnixStr, err := global.App.Redis.Get(context.Background(), jwtService.getBlackListKey(tokenStr)).Result()
    joinUnix, err := strconv.ParseInt(joinUnixStr, 10, 64)
    if joinUnixStr == "" || err != nil {
        return false
    }
    // JwtBlacklistGracePeriod 为黑名单宽限时间,避免并发请求失效
    if time.Now().Unix()-joinUnix < global.App.Config.Jwt.JwtBlacklistGracePeriod {
        return false
    }
    return true
}
复制代码

app/middleware/jwt.go 中,增加黑名单校验

代码语言:javascript
复制
func JWTAuth(GuardName string) gin.HandlerFunc {
    return func(c *gin.Context) {
        // ...
        token, err := jwt.ParseWithClaims(tokenStr, &services.CustomClaims{}, func(token *jwt.Token) (interface{}, error) {
            return []byte(global.App.Config.Jwt.Secret), nil
        })
        if err != nil || services.JwtService.IsInBlacklist(tokenStr) {
            response.TokenFail(c)
            c.Abort()
            return
        }

        claims := token.Claims.(*services.CustomClaims)
        // ...
    }
}
复制代码

实现登出接口

添加路由

代码语言:javascript
复制
func SetApiGroupRoutes(router *gin.RouterGroup) {
    // ...
    authRouter := router.Group("").Use(middleware.JWTAuth(app.GuardName))
    {
        authRouter.POST("/auth/info", app.Info)
        authRouter.POST("/auth/logout", app.Logout)
    }
}
复制代码

app/controllers/app/auth.go 中,编写

代码语言:javascript
复制
func Logout(c *gin.Context) {
    err := services.JwtService.JoinBlackList(c.Keys["token"].(*jwt.Token))
    if err != nil {
        response.BusinessFail(c, "登出失败")
        return
    }
    response.Success(c, nil)
}
复制代码

测试

调用登录接口获取 token

image-20211024181646999.png
image-20211024181646999.png

将 token 加入 Authorization 头,调用登出接口 http://localhost:8888/api/auth/logout

image-20211024181827834.png
image-20211024181827834.png

JwtBlacklistGracePeriod 黑名单宽限时间结束之后,继续调用登出接口将无法成功响应

image-20211024182847212.png
image-20211024182847212.png

本文系转载,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文系转载前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 前言
  • 安装
  • 定义配置项
  • 初始化 Redis
  • 编写黑名单相关逻辑
  • 实现登出接口
  • 测试
相关产品与服务
云数据库 Redis
腾讯云数据库 Redis(TencentDB for Redis)是腾讯云打造的兼容 Redis 协议的缓存和存储服务。丰富的数据结构能帮助您完成不同类型的业务场景开发。支持主从热备,提供自动容灾切换、数据备份、故障迁移、实例监控、在线扩容、数据回档等全套的数据库服务。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档