Skip to main content

gin jwt

这段代码实现了一个 JWT 鉴权中间件 JWTAuth,用于在 Gin 框架中验证 JWT(JSON Web Token)。这个中间件会检查请求头中的 JWT,验证其有效性,并在必要时刷新令牌。以下是代码的详细分析:

引入的包

import (
"errors"
"github.com/flipped-aurora/gin-vue-admin/server/global"
"github.com/flipped-aurora/gin-vue-admin/server/model/system"
"github.com/flipped-aurora/gin-vue-admin/server/utils"
"github.com/golang-jwt/jwt/v4"
"go.uber.org/zap"
"strconv"
"time"

"github.com/flipped-aurora/gin-vue-admin/server/model/common/response"
"github.com/flipped-aurora/gin-vue-admin/server/service"

"github.com/gin-gonic/gin"
)

全局变量

var jwtService = service.ServiceGroupApp.SystemServiceGroup.JwtService

JWTAuth 函数

func JWTAuth() gin.HandlerFunc {
return func(c *gin.Context) {
// 从请求头中获取 JWT
token := utils.GetToken(c)
if token == "" {
response.NoAuth("未登录或非法访问", c)
c.Abort()
return
}
// 检查 JWT 是否在黑名单中
if jwtService.IsBlacklist(token) {
response.NoAuth("您的帐户异地登陆或令牌失效", c)
utils.ClearToken(c)
c.Abort()
return
}
j := utils.NewJWT()
// 解析 JWT
claims, err := j.ParseToken(token)
if err != nil {
if errors.Is(err, utils.TokenExpired) {
response.NoAuth("授权已过期", c)
utils.ClearToken(c)
c.Abort()
return
}
response.NoAuth(err.Error(), c)
utils.ClearToken(c)
c.Abort()
return
}

// 可选:检查用户是否被禁用或删除
// if user, err := userService.FindUserByUuid(claims.UUID.String()); err != nil || user.Enable == 2 {
// _ = jwtService.JsonInBlacklist(system.JwtBlacklist{Jwt: token})
// response.FailWithDetailed(gin.H{"reload": true}, err.Error(), c)
// c.Abort()
// }

// 将解析后的 claims 存储到上下文中
c.Set("claims", claims)
// 检查 JWT 的剩余有效时间是否小于缓冲时间
if claims.ExpiresAt.Unix()-time.Now().Unix() < claims.BufferTime {
dr, _ := utils.ParseDuration(global.GVA_CONFIG.JWT.ExpiresTime)
claims.ExpiresAt = jwt.NewNumericDate(time.Now().Add(dr))
newToken, _ := j.CreateTokenByOldToken(token, *claims)
newClaims, _ := j.ParseToken(newToken)
c.Header("new-token", newToken)
c.Header("new-expires-at", strconv.FormatInt(newClaims.ExpiresAt.Unix(), 10))
utils.SetToken(c, newToken, int(dr.Seconds()))
if global.GVA_CONFIG.System.UseMultipoint {
RedisJwtToken, err := jwtService.GetRedisJWT(newClaims.Username)
if err != nil {
global.GVA_LOG.Error("get redis jwt failed", zap.Error(err))
} else {
_ = jwtService.JsonInBlacklist(system.JwtBlacklist{Jwt: RedisJwtToken})
}
_ = jwtService.SetRedisJWT(newToken, newClaims.Username)
}
}
c.Next()

// 将新生成的 token 信息设置到响应头中
if newToken, exists := c.Get("new-token"); exists {
c.Header("new-token", newToken.(string))
}
if newExpiresAt, exists := c.Get("new-expires-at"); exists {
c.Header("new-expires-at", newExpiresAt.(string))
}
}
}

核心逻辑

  1. 获取 JWT

    • 从请求头中获取 JWT。如果没有找到 JWT,则返回 "未登录或非法访问" 的响应,并中止请求处理。
  2. 检查黑名单

    • 检查 JWT 是否在黑名单中。如果在黑名单中,返回 "您的帐户异地登陆或令牌失效" 的响应,并清除 JWT。
  3. 解析 JWT

    • 使用 utils.NewJWT() 创建一个新的 JWT 实例,并解析 token。如果解析失败,检查错误类型:
      • 如果是 TokenExpired 错误,返回 "授权已过期" 的响应,并清除 JWT。
      • 其他错误,返回错误信息,并清除 JWT。
  4. 可选:检查用户状态

    • 可以选择检查用户是否被禁用或删除。如果用户被禁用或删除,将 JWT 添加到黑名单,并返回错误响应。
  5. 刷新 JWT

    • 检查 JWT 的剩余有效时间是否小于缓冲时间。如果是,生成一个新的 JWT 并将其设置到响应头中。如果启用了多点登录(UseMultipoint),则更新 Redis 中的 JWT 信息。
  6. 继续处理请求

    • 调用 c.Next() 继续处理请求。
  7. 设置新的 JWT 信息到响应头

    • 将新生成的 JWT 信息(如果有)设置到响应头中。

使用示例

要在 Gin 应用中使用这个中间件,可以在路由设置时添加:

r := gin.Default()
r.Use(JWTAuth())

总结

这个中间件通过验证和管理 JWT,确保只有合法的请求才能访问受保护的资源。它还处理了 JWT 的刷新逻辑,以确保用户在令牌即将过期时自动获取新的令牌,从而提高用户体验和安全性。

内存中,重启程序就没了

你提到的一个重要问题是:如果使用内存中的数据结构(如内存缓存)来存储黑名单或其他关键数据,当程序重启时,这些数据将会丢失。为了应对这种情况,通常会使用持久化存储解决方案,如 Redis 或数据库。以下是一些改进建议和示例代码,以确保数据在程序重启后仍然存在。

使用 Redis 进行持久化存储

Redis 是一个内存数据库,支持数据持久化,可以在程序重启后保留数据。以下是如何在你的代码中使用 Redis 来管理 JWT 黑名单和多点登录信息的示例。

1. 安装 Redis 和 Redis 客户端

首先,确保你已经安装了 Redis 服务器,并在你的 Go 项目中引入 Redis 客户端库。

go get github.com/go-redis/redis/v8

2. 配置 Redis 客户端

在你的项目中配置 Redis 客户端连接。

package global

import (
"github.com/go-redis/redis/v8"
"context"
)

var (
RedisClient *redis.Client
Ctx = context.Background()
)

func InitRedis() {
RedisClient = redis.NewClient(&redis.Options{
Addr: "localhost:6379", // Redis 服务器地址
Password: "", // Redis 服务器密码
DB: 0, // 使用默认数据库
})
}

在程序启动时初始化 Redis 客户端:

func main() {
global.InitRedis()
// 其他初始化代码
}

3. 更新 JWT 服务以使用 Redis

修改 jwtService 以使用 Redis 存储黑名单和多点登录信息。

package service

import (
"github.com/go-redis/redis/v8"
"github.com/flipped-aurora/gin-vue-admin/server/global"
"time"
)

type JwtService struct{}

func (jwtService *JwtService) IsBlacklist(token string) bool {
_, err := global.RedisClient.Get(global.Ctx, token).Result()
return err == nil
}

func (jwtService *JwtService) JsonInBlacklist(token string) error {
err := global.RedisClient.Set(global.Ctx, token, "blacklist", 0).Err()
return err
}

func (jwtService *JwtService) GetRedisJWT(username string) (string, error) {
return global.RedisClient.Get(global.Ctx, username).Result()
}

func (jwtService *JwtService) SetRedisJWT(token, username string) error {
err := global.RedisClient.Set(global.Ctx, username, token, time.Hour*24).Err() // 设置有效时间为 24 小时
return err
}

4. 更新中间件代码

在中间件中调用更新后的 jwtService 方法。

package middleware

import (
"errors"
"github.com/flipped-aurora/gin-vue-admin/server/global"
"github.com/flipped-aurora/gin-vue-admin/server/model/system"
"github.com/flipped-aurora/gin-vue-admin/server/utils"
"github.com/golang-jwt/jwt/v4"
"go.uber.org/zap"
"strconv"
"time"

"github.com/flipped-aurora/gin-vue-admin/server/model/common/response"
"github.com/flipped-aurora/gin-vue-admin/server/service"

"github.com/gin-gonic/gin"
)

var jwtService = service.ServiceGroupApp.SystemServiceGroup.JwtService

func JWTAuth() gin.HandlerFunc {
return func(c *gin.Context) {
token := utils.GetToken(c)
if token == "" {
response.NoAuth("未登录或非法访问", c)
c.Abort()
return
}
if jwtService.IsBlacklist(token) {
response.NoAuth("您的帐户异地登陆或令牌失效", c)
utils.ClearToken(c)
c.Abort()
return
}
j := utils.NewJWT()
claims, err := j.ParseToken(token)
if err != nil {
if errors.Is(err, utils.TokenExpired) {
response.NoAuth("授权已过期", c)
utils.ClearToken(c)
c.Abort()
return
}
response.NoAuth(err.Error(), c)
utils.ClearToken(c)
c.Abort()
return
}

c.Set("claims", claims)
if claims.ExpiresAt.Unix()-time.Now().Unix() < claims.BufferTime {
dr, _ := utils.ParseDuration(global.GVA_CONFIG.JWT.ExpiresTime)
claims.ExpiresAt = jwt.NewNumericDate(time.Now().Add(dr))
newToken, _ := j.CreateTokenByOldToken(token, *claims)
newClaims, _ := j.ParseToken(newToken)
c.Header("new-token", newToken)
c.Header("new-expires-at", strconv.FormatInt(newClaims.ExpiresAt.Unix(), 10))
utils.SetToken(c, newToken, int(dr.Seconds()))
if global.GVA_CONFIG.System.UseMultipoint {
RedisJwtToken, err := jwtService.GetRedisJWT(newClaims.Username)
if err != nil {
global.GVA_LOG.Error("get redis jwt failed", zap.Error(err))
} else {
_ = jwtService.JsonInBlacklist(RedisJwtToken)
}
_ = jwtService.SetRedisJWT(newToken, newClaims.Username)
}
}
c.Next()

if newToken, exists := c.Get("new-token"); exists {
c.Header("new-token", newToken.(string))
}
if newExpiresAt, exists := c.Get("new-expires-at"); exists {
c.Header("new-expires-at", newExpiresAt.(string))
}
}
}

总结

通过使用 Redis 进行持久化存储,可以确保在程序重启后,JWT 黑名单和多点登录信息不会丢失。这样可以提高系统的可靠性和安全性,同时保持高效的鉴权和会话管理。