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