diff --git a/config/config_jwt.go b/config/config_jwt.go new file mode 100644 index 0000000..c95d30d --- /dev/null +++ b/config/config_jwt.go @@ -0,0 +1,8 @@ +package config + +type JWT struct { + SigningKey string `mapstructure:"signing-key" json:"signing-key" yaml:"signing-key"` // jwt签名 + ExpiresTime string `mapstructure:"expires-time" json:"expires-time" yaml:"expires-time"` // 过期时间 + BufferTime string `mapstructure:"buffer-time" json:"buffer-time" yaml:"buffer-time"` // 缓冲时间 + Issuer string `mapstructure:"issuer" json:"issuer" yaml:"issuer"` // 签发者 +} diff --git a/model/system/request/jwt.go b/model/system/request/jwt.go new file mode 100644 index 0000000..6f09007 --- /dev/null +++ b/model/system/request/jwt.go @@ -0,0 +1,18 @@ +package request + +import ( + "github.com/golang-jwt/jwt/v5" + "github.com/google/uuid" +) + +type CustomClaims struct { + BaseClaims + BufferTime int64 + jwt.RegisteredClaims +} + +type BaseClaims struct { + UUID uuid.UUID + ID uint + Account string +} diff --git a/utils/claims.go b/utils/claims.go new file mode 100644 index 0000000..e9ea23a --- /dev/null +++ b/utils/claims.go @@ -0,0 +1,156 @@ +package utils + +import ( + "github.com/gin-gonic/gin" + "net" + "sundynix-go/global" + "sundynix-go/model/system" + systemReq "sundynix-go/model/system/request" + "time" +) + +// GetLoginToken 获取登录token +func GetLoginToken(user system.Login) (token string, claims systemReq.CustomClaims, err error) { + j := NewJWT() + claims = j.CreateClaims(systemReq.BaseClaims{ + Account: user.GetAccount(), + ID: user.GetUserId(), + }) + token, err = j.CreateToken(claims) + return +} + +// SetToken 设置一个名为 "x-token" 的 Cookie,并根据请求的主机名和 IP 地址来设置 Cookie 的域。 +// +// 参数: +// - c: *gin.Context, Gin 框架的上下文对象,用于处理 HTTP 请求和响应。 +// - token: string, 要设置的 token 值。 +// - maxAge: int, Cookie 的最大生存时间(以秒为单位)。 +// +// 该函数首先从请求的主机名中提取出主机部分(去除端口号),然后判断该主机名是否为 IP 地址。 +// 如果是 IP 地址,则设置 Cookie 的域为空;否则,将 Cookie 的域设置为提取出的主机名。 +func SetToken(c *gin.Context, token string, maxAge int) { + // 从请求的主机名中提取主机部分,忽略端口号 + host, _, err := net.SplitHostPort(c.Request.Host) + if err != nil { + host = c.Request.Host + } + + // 判断主机名是否为 IP 地址,并根据结果设置 Cookie 的域 + if net.ParseIP(host) != nil { + c.SetCookie("x-token", token, maxAge, "/", "", false, false) + } else { + c.SetCookie("x-token", token, maxAge, "/", host, false, false) + } +} + +// GetToken 从请求头或Cookie中获取JWT token,并确保其有效性。 +// 如果请求头中没有Authorization字段,则尝试从Cookie中获取token,并解析验证其有效性。 +// 如果token有效,则将其重新写入Cookie,并设置过期时间。 +// +// 参数: +// - c: *gin.Context, Gin框架的上下文对象,用于获取请求信息和设置响应。 +// +// 返回值: +// - string: 获取到的JWT token,如果获取失败则返回空字符串。 +func GetToken(c *gin.Context) string { + // 从请求头中获取Authorization字段的值 + token := c.Request.Header.Get("Authorization") + + // 如果请求头中没有Authorization字段,则尝试从Cookie中获取token + if token == "" { + j := NewJWT() + token, _ = c.Cookie("x-token") + + // 解析并验证token的有效性 + claims, err := j.ParseToken(token) + if err != nil { + // 如果解析失败,记录错误日志并返回当前token + global.Logger.Error("重新写入cookie token失败,未能成功解析token,请检查请求头是否存在x-token且claims是否为规定结构") + return token + } + + // 如果token有效,则将其重新写入Cookie,并设置过期时间 + SetToken(c, token, int((claims.ExpiresAt.Unix()-time.Now().Unix())/60)) + } + + // 返回获取到的token + return token +} + +// GetUserInfo 从 gin.Context 中获取用户信息,并返回 CustomClaims 类型的指针。 +// 该函数首先尝试从上下文中获取已存在的 claims,如果不存在,则调用 GetClaims 函数获取 claims。 +// 如果获取 claims 失败,则返回 nil。 +// +// 参数: +// - c: *gin.Context, gin 框架的上下文对象,用于获取请求相关的信息。 +// +// 返回值: +// - *systemReq.CustomClaims: 返回用户的自定义 claims 信息,如果获取失败则返回 nil。 +func GetUserInfo(c *gin.Context) *systemReq.CustomClaims { + // 尝试从上下文中获取已存在的 claims + if claims, exists := c.Get("claims"); !exists { + // 如果 claims 不存在,则调用 GetClaims 函数获取 claims + if cl, err := GetClaims(c); err != nil { + // 如果获取 claims 失败,返回 nil + return nil + } else { + // 成功获取 claims,返回 claims + return cl + } + } else { + // 如果 claims 存在,将其转换为 CustomClaims 类型并返回 + waitUse := claims.(*systemReq.CustomClaims) + return waitUse + } +} + +// GetUserId 从 gin.Context 中获取用户 ID,并返回 uint 类型的 ID。 +// 该函数首先尝试从上下文中获取已存在的 claims,如果不存在,则调用 GetClaims 函数获取 claims。 +// 如果获取 claims 失败,则返回 0。 +// +// 参数: +// - c: *gin.Context, gin 框架的上下文对象,用于获取请求相关的信息。 +// +// 返回值: +// - uint: 返回用户的 ID,如果获取失败则返回 0。 +func GetUserId(c *gin.Context) uint { + if claims, exists := c.Get("claims"); !exists { + if cl, err := GetClaims(c); err != nil { + return 0 + } else { + return cl.BaseClaims.ID + } + } else { + waitUse := claims.(*systemReq.CustomClaims) + return waitUse.BaseClaims.ID + } +} + +// GetClaims 从 Gin 上下文中提取并解析 JWT 令牌,返回自定义的 claims 信息。 +// 该函数首先从请求头中获取 JWT 令牌,然后使用 JWT 解析器解析令牌并返回 claims。 +// 如果解析过程中发生错误,函数会记录错误日志并返回错误信息。 +// +// 参数: +// - c: *gin.Context, Gin 上下文对象,用于获取请求头中的 JWT 令牌。 +// +// 返回值: +// - *systemReq.CustomClaims: 解析后的自定义 claims 信息。 +// - error: 解析过程中发生的错误,如果解析成功则为 nil。 +func GetClaims(c *gin.Context) (*systemReq.CustomClaims, error) { + // 从 Gin 上下文中获取 JWT 令牌 + token := GetToken(c) + + // 创建新的 JWT 解析器 + j := NewJWT() + + // 解析 JWT 令牌并获取 claims 信息 + claims, err := j.ParseToken(token) + if err != nil { + // 如果解析失败,记录错误日志 + global.Logger.Error("获取用户信息失败,请检查请求头是否存在x-token且claims是否为规定结构") + } + + // 返回解析后的 claims 信息和可能的错误 + return claims, err +} diff --git a/utils/jwt.go b/utils/jwt.go new file mode 100644 index 0000000..f7b225c --- /dev/null +++ b/utils/jwt.go @@ -0,0 +1,87 @@ +package utils + +import ( + "errors" + "github.com/golang-jwt/jwt/v5" + "sundynix-go/global" + "sundynix-go/model/system/request" + "time" +) + +type JWT struct { + SigningKey []byte +} + +var ( + TokenValid = errors.New("未知错误") + TokenExpired = errors.New("token已过期") + TokenNotValidYet = errors.New("token尚未激活") + TokenMalformed = errors.New("这不是一个token") + TokenSignatureInvalid = errors.New("无效签名") + TokenInvalid = errors.New("无法处理此token") +) + +// NewJWT 初始化JWT +func NewJWT() *JWT { + return &JWT{ + SigningKey: []byte("gin-blog-key"), + } +} + +// CreateClaims 创建Claims +func (j *JWT) CreateClaims(baseClaims request.BaseClaims) request.CustomClaims { + bf, _ := ParseDuration(global.Config.JWT.BufferTime) + ep, _ := ParseDuration(global.Config.JWT.ExpiresTime) + claims := request.CustomClaims{ + BaseClaims: baseClaims, + BufferTime: int64(bf / time.Second), // 缓冲时间1天 缓冲时间内会获得新的token刷新令牌 此时一个用户会存在两个有效令牌 但是前端只留一个 另一个会丢失 + RegisteredClaims: jwt.RegisteredClaims{ + Audience: jwt.ClaimStrings{"sundynix"}, + NotBefore: jwt.NewNumericDate(time.Now().Add(-1000)), // 签名生效时间 + ExpiresAt: jwt.NewNumericDate(time.Now().Add(ep)), // 过期时间 7天 配置文件 + Issuer: global.Config.JWT.Issuer, + }, + } + return claims +} + +// CreateToken 创建一个token +func (j *JWT) CreateToken(claims request.CustomClaims) (string, error) { + token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) + return token.SignedString(j.SigningKey) +} + +// RefreshToken 刷新token +func (j *JWT) RefreshToken(oldTokenString string, claims request.CustomClaims) (string, error) { + v, err, _ := global.ConcurrencyControl.Do("JWT:"+oldTokenString, func() (interface{}, error) { + return j.CreateToken(claims) + }) + return v.(string), err +} + +// ParseToken 解析token +func (j *JWT) ParseToken(tokenString string) (*request.CustomClaims, error) { + token, err := jwt.ParseWithClaims(tokenString, &request.CustomClaims{}, func(token *jwt.Token) (i interface{}, e error) { + return j.SigningKey, nil + }) + if err != nil { + switch { + case errors.Is(err, jwt.ErrTokenExpired): + return nil, TokenExpired + case errors.Is(err, jwt.ErrTokenNotValidYet): + return nil, TokenNotValidYet + case errors.Is(err, jwt.ErrTokenMalformed): + return nil, TokenMalformed + case errors.Is(err, jwt.ErrTokenSignatureInvalid): + return nil, TokenSignatureInvalid + default: + return nil, TokenInvalid + } + } + if token != nil { + if claims, ok := token.Claims.(*request.CustomClaims); ok && token.Valid { + return claims, nil + } + } + return nil, TokenInvalid +}