Some checks failed
Build and Deploy (service.xpcool.com) / build-and-deploy (push) Failing after 31s
将 api 层校验规则提示语、service 层 gerror.Wrap 与 response.Error 错误信息、panic 未注册提示统一改为中文,并同步中文化 cmd 路由注释 与变更记录。仅涉及注释、文档与字符串改动,无业务逻辑变更。
93 lines
3.2 KiB
Go
93 lines
3.2 KiB
Go
// 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))
|
||
}
|