- users 加 token_version(schema v4):改密/登出递增,该用户全部已发 access/refresh 立即作废;refresh 改轮换式换发新 cookie - 邀请码改精确匹配 + 注册成功后消费(一码一用),修复子串匹配让任意后缀放行、可无限薅体验金 - 生产校验拦下 .env.example 占位密钥;OT_ENV 枚举校验(拼错不再静默跳过断言);生产强制 COOKIE_SECURE;非 development 拒绝默认密码播种管理员 - WebAuthn 会话启用过期校验 + 4096 容量上限(未认证 login/begin 可无限撑爆内存),begin/complete 挂登录级限速 - /api/v1 挂 1MB 请求体上限;login 先限 IP 桶再解析 body;全局安全响应头 + CSP - 连接池配置(PG 30/SQLite 8);SQLite DSN 自动追加 WAL + busy_timeout(驱动只认 _pragma,旧注释的 _journal_mode 写法不生效)
76 lines
2.1 KiB
Go
76 lines
2.1 KiB
Go
// Package jwt 访问令牌(短时,存内存)与刷新令牌(HttpOnly Cookie)签发/校验。
|
||
package jwt
|
||
|
||
import (
|
||
"errors"
|
||
"time"
|
||
|
||
"github.com/golang-jwt/jwt/v5"
|
||
)
|
||
|
||
// Claims 用户声明;Subject 字段区分 "access" / "refresh"。
|
||
// Ver 是用户级会话版本号:改密/登出时递增,旧 token 立即失效(可撤销性)。
|
||
type Claims struct {
|
||
UserID uint64 `json:"uid"`
|
||
Username string `json:"uname"`
|
||
Role string `json:"role"`
|
||
Ver int `json:"ver"`
|
||
jwt.RegisteredClaims
|
||
}
|
||
|
||
type Manager struct {
|
||
secret []byte
|
||
issuer string
|
||
accessTTL time.Duration
|
||
refreshTTL time.Duration
|
||
}
|
||
|
||
func NewManager(secret, issuer string, accessTTL, refreshTTL time.Duration) *Manager {
|
||
return &Manager{secret: []byte(secret), issuer: issuer, accessTTL: accessTTL, refreshTTL: refreshTTL}
|
||
}
|
||
|
||
func (m *Manager) AccessTTL() time.Duration { return m.accessTTL }
|
||
func (m *Manager) RefreshTTL() time.Duration { return m.refreshTTL }
|
||
|
||
// Sign 签发 token;typ 取 "access" / "refresh",ver 为签发时的会话版本。
|
||
func (m *Manager) Sign(userID uint64, username, role, typ string, ver int) (string, time.Time, error) {
|
||
ttl := m.accessTTL
|
||
if typ == "refresh" {
|
||
ttl = m.refreshTTL
|
||
}
|
||
now := time.Now()
|
||
exp := now.Add(ttl)
|
||
claims := Claims{
|
||
UserID: userID,
|
||
Username: username,
|
||
Role: role,
|
||
Ver: ver,
|
||
RegisteredClaims: jwt.RegisteredClaims{
|
||
Issuer: m.issuer,
|
||
Subject: typ,
|
||
IssuedAt: jwt.NewNumericDate(now),
|
||
ExpiresAt: jwt.NewNumericDate(exp),
|
||
},
|
||
}
|
||
tok := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
|
||
s, err := tok.SignedString(m.secret)
|
||
return s, exp, err
|
||
}
|
||
|
||
var ErrInvalidToken = errors.New("invalid token")
|
||
|
||
// Parse 校验签名与有效期。
|
||
func (m *Manager) Parse(token string) (*Claims, error) {
|
||
claims := &Claims{}
|
||
tok, err := jwt.ParseWithClaims(token, claims, func(t *jwt.Token) (any, error) {
|
||
if _, ok := t.Method.(*jwt.SigningMethodHMAC); !ok {
|
||
return nil, ErrInvalidToken
|
||
}
|
||
return m.secret, nil
|
||
})
|
||
if err != nil || !tok.Valid {
|
||
return nil, ErrInvalidToken
|
||
}
|
||
return claims, nil
|
||
}
|