// Package jwt implements HS256 JWT with Go standard crypto primitives. package jwt import ( "context" "crypto/hmac" "crypto/rand" "crypto/sha256" "encoding/base64" "encoding/json" "strings" "time" "github.com/gogf/gf/v2/errors/gerror" "github.com/gogf/gf/v2/frame/g" ) type Claims struct { Subject uint64 `json:"sub"` Scope string `json:"scope"` Terminal string `json:"terminal,omitempty"` Type string `json:"typ"` ExpireAt int64 `json:"exp"` IssuedAt int64 `json:"iat"` JTI string `json:"jti,omitempty"` // 刷新令牌的唯一编号,用于数据库会话校验。 } type Service struct { secret []byte accessTTL, refreshTTL time.Duration } func New(ctx context.Context) *Service { // 密钥和有效期由配置中心统一提供,生产环境必须通过环境变量覆盖。 return &Service{secret: []byte(g.Cfg().MustGet(ctx, "jwt.secret").String()), accessTTL: g.Cfg().MustGet(ctx, "jwt.accessExpire").Duration(), refreshTTL: g.Cfg().MustGet(ctx, "jwt.refreshExpire").Duration()} } func (s *Service) Issue(id uint64, scope, terminal string) (access, refresh string, expires int64, err error) { // access token 只负责短期访问;refresh token 带唯一 JTI,用于可撤销的长期会话。 now := time.Now().Unix() expires = now + int64(s.accessTTL.Seconds()) access, err = s.sign(Claims{Subject: id, Scope: scope, Terminal: terminal, Type: "access", ExpireAt: expires, IssuedAt: now}) if err != nil { return } jti, err := newJTI() if err != nil { return "", "", 0, gerror.Wrap(err, "生成刷新令牌标识失败") } refresh, err = s.sign(Claims{Subject: id, Scope: scope, Terminal: terminal, Type: "refresh", ExpireAt: now + int64(s.refreshTTL.Seconds()), IssuedAt: now, JTI: jti}) return } func (s *Service) Parse(token, typ, scope string) (*Claims, error) { // 先使用常量时间比较校验签名,再解析声明,避免伪造令牌进入业务层。 parts := strings.Split(token, ".") if len(parts) != 3 || !hmac.Equal([]byte(parts[2]), []byte(s.signature(parts[0]+"."+parts[1]))) { return nil, gerror.New("invalid token") } raw, err := base64.RawURLEncoding.DecodeString(parts[1]) if err != nil { return nil, gerror.Wrap(err, "decode token") } var c Claims if err = json.Unmarshal(raw, &c); err != nil { return nil, gerror.Wrap(err, "decode claims") } if c.ExpireAt < time.Now().Unix() || c.Type != typ || c.Scope != scope { return nil, gerror.New("token expired or scope mismatch") } return &c, nil } // newJTI 生成 256 位随机标识,编码后正好适配数据库 CHAR(43) 字段。 func newJTI() (string, error) { b := make([]byte, 32) if _, err := rand.Read(b); err != nil { return "", err } return base64.RawURLEncoding.EncodeToString(b), nil } func (s *Service) sign(c Claims) (string, error) { h := base64.RawURLEncoding.EncodeToString([]byte(`{"alg":"HS256","typ":"JWT"}`)) b, err := json.Marshal(c) if err != nil { return "", gerror.Wrap(err, "encode claims") } p := h + "." + base64.RawURLEncoding.EncodeToString(b) return p + "." + s.signature(p), nil } func (s *Service) signature(input string) string { mac := hmac.New(sha256.New, s.secret) _, _ = mac.Write([]byte(input)) return base64.RawURLEncoding.EncodeToString(mac.Sum(nil)) }