做统一认证登录

This commit is contained in:
2026-09-19 21:44:04 +08:00
parent 1a4471e34d
commit fa281363fb
78 changed files with 8127 additions and 1442 deletions
+321 -63
View File
@@ -1,63 +1,321 @@
package jwtutil
import (
"errors"
"time"
"github.com/golang-jwt/jwt/v5"
)
// 密钥(后续可从配置中读取)
var secret = []byte("yunzer_jwt_secret_key")
// Claims 定义JWT的claims结构
type Claims struct {
UserID int `json:"user_id"`
Username string `json:"username"`
TenantId int `json:"tenant_id"` // 租户ID
UserType string `json:"user_type"` // 用户类型:"user" / "employee" / "platform" 等
jwt.RegisteredClaims
}
// GenerateToken 生成JWT token
func GenerateToken(userID int, username string, tenantId int, userType string) (string, error) {
expirationTime := time.Now().Add(24 * time.Hour)
claims := &Claims{
UserID: userID,
Username: username,
TenantId: tenantId,
UserType: userType,
RegisteredClaims: jwt.RegisteredClaims{
ExpiresAt: jwt.NewNumericDate(expirationTime),
IssuedAt: jwt.NewNumericDate(time.Now()),
NotBefore: jwt.NewNumericDate(time.Now()),
},
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
tokenString, err := token.SignedString(secret)
return tokenString, err
}
// ParseToken 解析JWT token
func ParseToken(tokenString string) (*Claims, error) {
claims := &Claims{}
token, err := jwt.ParseWithClaims(tokenString, claims, func(token *jwt.Token) (interface{}, error) {
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, errors.New("unexpected signing method")
}
return secret, nil
})
if err != nil {
return nil, err
}
if !token.Valid {
return nil, errors.New("invalid token")
}
return claims, nil
}
package jwtutil
import (
"crypto/rsa"
"encoding/base64"
"errors"
"fmt"
"math/big"
"os"
"strings"
"sync"
"time"
beego "github.com/beego/beego/v2/server/web"
"github.com/golang-jwt/jwt/v5"
)
// 算法常量
const (
AlgHS256 = "HS256"
AlgRS256 = "RS256"
)
// DefaultKid 默认密钥 ID。历史 token 未携带 kid,统一按此 ID 处理。
const DefaultKid = "default"
// legacySecret 兼容用的内置密钥:仅用于解析历史已签发的 token。
// 生产环境必须在 app.conf 中配置 jwt_secret,否则会一直使用该固定值。
const legacySecret = "yunzer_jwt_secret_key"
// Claims JWT 载荷。旧字段保持不变,新增字段均为 omitempty,
// 历史 token 解析后为零值,不影响现有 78 处解析点。
type Claims struct {
UserID int `json:"user_id"`
Username string `json:"username"`
TenantId int `json:"tenant_id"` // 租户ID
UserType string `json:"user_type"` // 用户类型:"user" / "employee" / "platform" 等
// ---- 统一认证中心(OIDC)扩展字段 ----
ClientID string `json:"client_id,omitempty"` // 接入应用 client_id,即 aud 的业务标识
Sid string `json:"sid,omitempty"` // 会话ID,用于单点登出/踢下线
Scope string `json:"scope,omitempty"`
Amr string `json:"amr,omitempty"` // 认证方式:pwd/sms/otp/wx/...
jwt.RegisteredClaims
}
// ---------------------------------------------------------------- 密钥管理
var (
keyOnce sync.Once
hsKeys map[string][]byte // kid -> HMAC 密钥
rsaPriv *rsa.PrivateKey // RS256 签名
rsaPub *rsa.PublicKey // RS256 验签
rsaKid string // RS256 密钥 ID
issuerVal string
)
func loadKeys() {
hsKeys = map[string][]byte{DefaultKid: []byte(legacySecret)}
// 主密钥(覆盖内置默认值)
if s, _ := beego.AppConfig.String("jwt_secret"); strings.TrimSpace(s) != "" {
hsKeys[DefaultKid] = []byte(strings.TrimSpace(s))
}
// 轮换密钥:jwt_secrets = kid1:secret1,kid2:secret2
if s, _ := beego.AppConfig.String("jwt_secrets"); strings.TrimSpace(s) != "" {
for _, item := range strings.Split(s, ",") {
kv := strings.SplitN(strings.TrimSpace(item), ":", 2)
if len(kv) == 2 && strings.TrimSpace(kv[0]) != "" && strings.TrimSpace(kv[1]) != "" {
hsKeys[strings.TrimSpace(kv[0])] = []byte(strings.TrimSpace(kv[1]))
}
}
}
// RS256 密钥对:优先读文件路径,其次读内联 PEM
privPEM := readKeyConf("jwt_rsa_private_key", "jwt_rsa_private_key_file")
pubPEM := readKeyConf("jwt_rsa_public_key", "jwt_rsa_public_key_file")
if privPEM != "" {
if k, err := jwt.ParseRSAPrivateKeyFromPEM([]byte(privPEM)); err == nil {
rsaPriv = k
rsaPub = &k.PublicKey
}
}
if pubPEM != "" {
if k, err := jwt.ParseRSAPublicKeyFromPEM([]byte(pubPEM)); err == nil {
rsaPub = k
}
}
if rsaKid == "" {
rsaKid, _ = beego.AppConfig.String("jwt_rsa_kid")
}
if rsaKid == "" {
rsaKid = "rsa-1"
}
issuerVal, _ = beego.AppConfig.String("jwt_issuer")
}
func readKeyConf(inlineKey, fileKey string) string {
if v, _ := beego.AppConfig.String(inlineKey); strings.TrimSpace(v) != "" {
return strings.ReplaceAll(strings.TrimSpace(v), `\n`, "\n")
}
if p, _ := beego.AppConfig.String(fileKey); strings.TrimSpace(p) != "" {
if b, err := os.ReadFile(strings.TrimSpace(p)); err == nil {
return string(b)
}
}
return ""
}
func ensureKeys() {
keyOnce.Do(loadKeys)
}
// keyFunc 按 token header 的 alg + kid 选择验签密钥。
// 未携带 kid 或 kid 未注册时回落到默认密钥,保证历史 token 仍可解析。
func keyFunc(token *jwt.Token) (interface{}, error) {
ensureKeys()
kid, _ := token.Header["kid"].(string)
switch token.Method.Alg() {
case AlgHS256:
if sec, ok := hsKeys[kid]; ok && kid != "" {
return sec, nil
}
return hsKeys[DefaultKid], nil
case AlgRS256:
if rsaPub == nil {
return nil, errors.New("服务端未配置 RS256 公钥")
}
return rsaPub, nil
}
return nil, fmt.Errorf("unsupported signing method: %v", token.Header["alg"])
}
// ---------------------------------------------------------------- 签发
// TokenOptions 认证中心签发参数
type TokenOptions struct {
Alg string // HS256 / RS256;留空时优先 RS256,未配置 RSA 则回落 HS256
UserID int // 身份 ID(OIDC sub 用字符串形式)
Username string
TenantID int
UserType string
ClientID string
Sid string
Scope string
Amr string
Subject string
Audience []string
Jti string // JWT ID,用于吊销(登出/踢下线)
TTL time.Duration // 留空默认 30 分钟
Kid string
}
// SignToken 签发 token。RSA 未配置时自动回落 HS256,保证服务可启动。
func SignToken(opt TokenOptions) (string, error) {
ensureKeys()
alg := strings.ToUpper(strings.TrimSpace(opt.Alg))
if alg == "" {
alg = AlgRS256
}
if alg == AlgRS256 && rsaPriv == nil {
alg = AlgHS256
}
ttl := opt.TTL
if ttl <= 0 {
ttl = 30 * time.Minute
}
now := time.Now()
claims := &Claims{
UserID: opt.UserID,
Username: opt.Username,
TenantId: opt.TenantID,
UserType: opt.UserType,
ClientID: opt.ClientID,
Sid: opt.Sid,
Scope: opt.Scope,
Amr: opt.Amr,
RegisteredClaims: jwt.RegisteredClaims{
ID: opt.Jti,
Subject: opt.Subject,
Audience: opt.Audience,
ExpiresAt: jwt.NewNumericDate(now.Add(ttl)),
IssuedAt: jwt.NewNumericDate(now),
NotBefore: jwt.NewNumericDate(now),
},
}
if issuerVal != "" {
claims.Issuer = issuerVal
}
if opt.Subject == "" && opt.UserID > 0 {
claims.Subject = fmt.Sprintf("%d", opt.UserID)
}
var method jwt.SigningMethod
switch alg {
case AlgRS256:
method = jwt.SigningMethodRS256
default:
method = jwt.SigningMethodHS256
}
token := jwt.NewWithClaims(method, claims)
kid := strings.TrimSpace(opt.Kid)
if kid == "" && alg == AlgRS256 {
kid = rsaKid
}
if kid != "" {
token.Header["kid"] = kid
}
var key interface{}
if alg == AlgRS256 {
key = rsaPriv
} else {
kidToUse := kid
if kidToUse == "" {
kidToUse = DefaultKid
}
if sec, ok := hsKeys[kidToUse]; ok {
key = sec
} else {
key = hsKeys[DefaultKid]
}
}
return token.SignedString(key)
}
// GenerateToken 生成 JWT token(兼容旧签名与行为)。
// 默认 HS256;若 app.conf 配置了 jwt_issuer 则带上 iss。
func GenerateToken(userID int, username string, tenantId int, userType string) (string, error) {
ttl := 24 * time.Hour
ensureKeys()
now := time.Now()
claims := &Claims{
UserID: userID,
Username: username,
TenantId: tenantId,
UserType: userType,
RegisteredClaims: jwt.RegisteredClaims{
ExpiresAt: jwt.NewNumericDate(now.Add(ttl)),
IssuedAt: jwt.NewNumericDate(now),
NotBefore: jwt.NewNumericDate(now),
},
}
if issuerVal != "" {
claims.Issuer = issuerVal
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
return token.SignedString(hsKeys[DefaultKid])
}
// ---------------------------------------------------------------- 解析
// ParseToken 解析并校验 JWT。自动识别 HS256 / RS256,函数签名与旧版一致。
func ParseToken(tokenString string) (*Claims, error) {
claims := &Claims{}
_, err := jwt.ParseWithClaims(tokenString, claims, keyFunc,
jwt.WithValidMethods([]string{AlgHS256, AlgRS256}))
if err != nil {
return nil, err
}
return claims, nil
}
// ParseTokenRaw 解析 token 但不校验有效期,用于登出/吊销场景获取 jti。
func ParseTokenRaw(tokenString string) (*Claims, error) {
claims := &Claims{}
parser := jwt.NewParser(jwt.WithValidMethods([]string{AlgHS256, AlgRS256}), jwt.WithoutClaimsValidation())
if _, err := parser.ParseWithClaims(tokenString, claims, keyFunc); err != nil {
return nil, err
}
return claims, nil
}
// Issuer 返回配置的签发者(未配置时为空)
func Issuer() string {
ensureKeys()
return issuerVal
}
// JWK JSON Web Key(RS256 公钥)
type JWK struct {
Kty string `json:"kty"`
Use string `json:"use"`
Alg string `json:"alg"`
Kid string `json:"kid"`
N string `json:"n"`
E string `json:"e"`
}
// JWKS 返回 RSA 公钥集合(供 /auth/jwks.json 暴露)。
// 各应用本地用公钥验签即可,无需每次回调认证中心的 introspect 接口。
func JWKS() []JWK {
ensureKeys()
if rsaPub == nil || rsaPub.N == nil {
return nil
}
return []JWK{{
Kty: "RSA",
Use: "sig",
Alg: AlgRS256,
Kid: rsaKid,
N: base64.RawURLEncoding.EncodeToString(rsaPub.N.Bytes()),
E: base64.RawURLEncoding.EncodeToString(big.NewInt(int64(rsaPub.E)).Bytes()),
}}
}
// HasRSA 是否已配置 RS256 密钥对(决定认证中心能否签发非对称 token)
func HasRSA() bool {
ensureKeys()
return rsaPriv != nil && rsaPub != nil
}
// Kid 返回当前 RS256 密钥 ID
func Kid() string {
ensureKeys()
return rsaKid
}