package utils import ( "errors" "fmt" "github.com/gin-gonic/gin" jwt "github.com/golang-jwt/jwt/v5" "golang.org/x/sync/singleflight" "log" "net" "qnc-server/config" model "qnc-server/model/common" "strconv" "strings" "time" ) type JWT struct { SigningKey []byte } var ( TokenExpired = errors.New("Token is expired") TokenNotValidYet = errors.New("Token not active yet") TokenMalformed = errors.New("That's not even a token") TokenInvalid = errors.New("Couldn't handle this token:") ) func NewJWT() *JWT { return &JWT{ []byte(config.ConfigData.JWT.SigningKey), } } func (j *JWT) CreateClaims(baseClaims model.BaseClaims) model.CustomClaims { bf, _ := ParseDuration(config.ConfigData.JWT.BufferTime) ep, _ := ParseDuration(config.ConfigData.JWT.ExpiresTime) claims := model.CustomClaims{ BaseClaims: baseClaims, BufferTime: int64(bf / time.Second), // 缓冲时间1天 缓冲时间内会获得新的token刷新令牌 此时一个用户会存在两个有效令牌 但是前端只留一个 另一个会丢失 RegisteredClaims: jwt.RegisteredClaims{ Audience: jwt.ClaimStrings{"FM"}, // 受众 NotBefore: jwt.NewNumericDate(time.Now().Add(-1000)), // 签名生效时间 ExpiresAt: jwt.NewNumericDate(time.Now().Add(ep)), // 过期时间 7天 配置文件 Issuer: config.ConfigData.JWT.Issuer, // 签名的发行者 }, } return claims } // 创建一个token func (j *JWT) CreateToken(claims model.CustomClaims) (string, error) { token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) return token.SignedString(j.SigningKey) } var concurrencyControl = &singleflight.Group{} // CreateTokenByOldToken 旧token 换新token 使用归并回源避免并发问题 func (j *JWT) CreateTokenByOldToken(oldToken string, claims model.CustomClaims) (string, error) { v, err, _ := concurrencyControl.Do("JWT:"+oldToken, func() (interface{}, error) { return j.CreateToken(claims) }) return v.(string), err } // ParseToken 解析并验证 JWT 字符串 func (j *JWT) ParseToken(tokenString string) (*model.CustomClaims, error) { token, err := jwt.ParseWithClaims(tokenString, &model.CustomClaims{}, func(token *jwt.Token) (interface{}, error) { // 检查签名方法 if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"]) } return j.SigningKey, nil }) if err != nil { return nil, err } if claims, ok := token.Claims.(*model.CustomClaims); ok && token.Valid { return claims, nil } else { return nil, errors.New("invalid token") } } // 解析时间字符 func ParseDuration(d string) (time.Duration, error) { d = strings.TrimSpace(d) dr, err := time.ParseDuration(d) if err == nil { return dr, nil } if strings.Contains(d, "d") { index := strings.Index(d, "d") hour, _ := strconv.Atoi(d[:index]) dr = time.Hour * 24 * time.Duration(hour) ndr, err := time.ParseDuration(d[index+1:]) if err != nil { return dr, nil } return dr + ndr, nil } dv, err := strconv.ParseInt(d, 10, 64) return time.Duration(dv), err } func SetToken(c *gin.Context, token string, maxAge int) { // 增加cookie x-token 向来源的web添加 host, _, err := net.SplitHostPort(c.Request.Host) if err != nil { host = c.Request.Host } if net.ParseIP(host) != nil { c.SetCookie("x-token", token, maxAge, "/", "", false, false) } else { c.SetCookie("x-token", token, maxAge, "/", host, false, false) } } func ClearToken(c *gin.Context) { // 增加cookie x-token 向来源的web添加 host, _, err := net.SplitHostPort(c.Request.Host) if err != nil { host = c.Request.Host } if net.ParseIP(host) != nil { c.SetCookie("x-token", "", -1, "/", "", false, false) } else { c.SetCookie("x-token", "", -1, "/", host, false, false) } } func GetToken(c *gin.Context) string { token, _ := c.Cookie("x-token") if token == "" { token = c.Request.Header.Get("x-token") } return token } func GetClaims(c *gin.Context) (*model.CustomClaims, error) { token := GetToken(c) j := NewJWT() claims, err := j.ParseToken(token) if err != nil { log.Println("从Gin的Context中获取从jwt解析信息失败, 请检查请求头是否存在x-token且claims是否为规定结构") } return claims, err } 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.Userid } } else { waitUse := claims.(*model.CustomClaims) return waitUse.BaseClaims.Userid } }