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))
}
}
}
核心逻辑
-
获取 JWT:
- 从请求头中获取 JWT。如果没有找到 JWT,则返回 "未登录或非法访问" 的响应,并中止请求处理。
-
检查黑名单:
- 检查 JWT 是否在黑名单中。如果在黑名单中,返回 "您的帐户异地登陆或令牌失效" 的响应,并清除 JWT。
-
解析 JWT:
- 使用
utils.NewJWT()创建一个新的 JWT 实例,并解析 token。如果解析失败,检查错误类型:- 如果是
TokenExpired错误,返回 "授权已过期" 的响应,并清除 JWT。 - 其他错误,返回错误信息,并清除 JWT。
- 如果是
- 使用
-
可选:检查用户状态:
- 可以选择检查用户是否被禁用或删除。如果用户被禁用或删除,将 JWT 添加到黑名单,并返回错误响应。
-
刷新 JWT:
- 检查 JWT 的剩余有效时间是否小于缓冲时间。如果是,生成一个新的 JWT 并将其设置到响应头中。如果启用了多点登录(
UseMultipoint),则更新 Redis 中的 JWT 信息。
- 检查 JWT 的剩余有效时间是否小于缓冲时间。如果是,生成一个新的 JWT 并将其设置到响应头中。如果启用了多点登录(
-
继续处理请求:
- 调用
c.Next()继续处理请求。
- 调用
-
设置新的 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 黑名单和多点登录信息不会丢失。这样可以提高系统的可靠性和安全性,同时保持高效的鉴权和会话管理。