service.xpcool.com/internal/library/jwt/jwt.go
夏犀麟 4aca0c7f6f
Some checks failed
Build and Deploy (service.xpcool.com) / build-and-deploy (push) Failing after 5m7s
feat(house): 实现看房模块后端功能
- 新增 9 张房屋相关数据表(社区/楼宇/房源/价格快照/交易/设施/社区设施/学区/偏好)
- 添加菜单权限种子数据并绑定超级管理员角色
- 生成 DAO 层代码和实体对象
- 实现房屋模块 API 接口(社区/房源/看板)和控制器服务层
- 支持多平台软关联匹配、笋盘标记和低可信度标记功能
- 更新超级管理员账号为 xxcool/xxCool@2026
- 调整 RBAC 菜单结构,移除管理员管理功能,新增日志管理菜单
- 修复 RBAC 安全漏洞,确保禁用角色权限失效
- 重构认证模块,将登录相关接口迁移到统一包结构下
- 移除废弃的管理模块和工具类接口定义
- 为通用工具包添加中文注释和文档说明
2026-08-26 23:42:38 +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("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))
}