service.xpcool.com/internal/library/jwt/jwt.go
夏犀麟 4b3c00270f
Some checks failed
Build and Deploy (service.xpcool.com) / build-and-deploy (push) Failing after 31s
feat(i18n): 中文化校验提示与服务层错误信息
将 api 层校验规则提示语、service 层 gerror.Wrap 与 response.Error
错误信息、panic 未注册提示统一改为中文,并同步中文化 cmd 路由注释
与变更记录。仅涉及注释、文档与字符串改动,无业务逻辑变更。
2026-09-13 23:22:46 +08:00

93 lines
3.2 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// Package jwt 使用 Go 标准加密库实现 HS256 JWT。
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("令牌无效")
}
raw, err := base64.RawURLEncoding.DecodeString(parts[1])
if err != nil {
return nil, gerror.Wrap(err, "解码令牌失败")
}
var c Claims
if err = json.Unmarshal(raw, &c); err != nil {
return nil, gerror.Wrap(err, "解析令牌声明失败")
}
if c.ExpireAt < time.Now().Unix() || c.Type != typ || c.Scope != scope {
return nil, gerror.New("令牌已过期或作用域不匹配")
}
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, "编码令牌声明失败")
}
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))
}