e05e6f5903
1) JWT 默认密钥:生产模式(APP_ENV=production|prod 或 GIN_MODE=release)下若未设 JWT_SECRET 直接 log.Fatal,杜绝用开发默认值签发可伪造令牌;开发期警告并放行。 2) /admin 运维控制面(含模型 API 密钥管理)改挂 RequireAdmin:必须登录 + (设了 ADMIN_USER_IDS 则)uid 须在白名单;生产期未配置管理员直接 403。 3) CORS Allow-Origin 由 CORS_ALLOW_ORIGIN 配置(缺省 * 仅开发),非 * 时加 Vary。 build + auth 单测通过。仍属"小范围灰度"级,TLS/可观测/集成测试/HA 见 PROGRESS。 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
92 lines
2.7 KiB
Go
92 lines
2.7 KiB
Go
// Package auth 提供无状态鉴权能力:JWT 签发/校验 + 密码哈希。
|
||
package auth
|
||
|
||
import (
|
||
"errors"
|
||
"log"
|
||
"os"
|
||
"strings"
|
||
"time"
|
||
|
||
"github.com/golang-jwt/jwt/v5"
|
||
"golang.org/x/crypto/bcrypt"
|
||
)
|
||
|
||
// TokenTTL 是访问令牌有效期。
|
||
const TokenTTL = 24 * time.Hour
|
||
|
||
const devSecret = "sundynix-dev-secret-change-me"
|
||
|
||
// secret 是 JWT 签名密钥。生产必须经 JWT_SECRET 注入强密钥;
|
||
// 生产模式(APP_ENV=production/prod 或 GIN_MODE=release)下未设则直接 fatal,杜绝可伪造令牌。
|
||
var secret = []byte(resolveSecret())
|
||
|
||
func resolveSecret() string {
|
||
if s := os.Getenv("JWT_SECRET"); s != "" {
|
||
return s
|
||
}
|
||
if isProd() {
|
||
log.Fatal("[auth] 生产模式必须设置 JWT_SECRET(强随机密钥),拒绝使用开发默认值")
|
||
}
|
||
log.Println("[auth] ⚠️ 使用开发默认 JWT 密钥,生产务必设置 JWT_SECRET")
|
||
return devSecret
|
||
}
|
||
|
||
// isProd 判定是否生产环境。
|
||
func isProd() bool {
|
||
env := strings.ToLower(os.Getenv("APP_ENV"))
|
||
return env == "production" || env == "prod" || strings.ToLower(os.Getenv("GIN_MODE")) == "release"
|
||
}
|
||
|
||
// ErrInvalidToken 表示令牌无效/过期/签名不符。
|
||
var ErrInvalidToken = errors.New("invalid token")
|
||
|
||
// Issue 为某用户签发 JWT(subject = userID)。
|
||
func Issue(userID string) (string, error) { return issue(userID, TokenTTL) }
|
||
|
||
func issue(userID string, ttl time.Duration) (string, error) {
|
||
now := time.Now()
|
||
claims := jwt.RegisteredClaims{
|
||
Subject: userID,
|
||
IssuedAt: jwt.NewNumericDate(now),
|
||
ExpiresAt: jwt.NewNumericDate(now.Add(ttl)),
|
||
}
|
||
return jwt.NewWithClaims(jwt.SigningMethodHS256, claims).SignedString(secret)
|
||
}
|
||
|
||
// Parse 校验 JWT 并返回 userID(subject)。无效/过期/签名不符返回 ErrInvalidToken。
|
||
func Parse(token string) (string, error) {
|
||
t, err := jwt.ParseWithClaims(token, &jwt.RegisteredClaims{}, func(t *jwt.Token) (any, error) {
|
||
if _, ok := t.Method.(*jwt.SigningMethodHMAC); !ok { // 仅接受 HMAC,防 alg 混淆攻击
|
||
return nil, ErrInvalidToken
|
||
}
|
||
return secret, nil
|
||
})
|
||
if err != nil || !t.Valid {
|
||
return "", ErrInvalidToken
|
||
}
|
||
c, ok := t.Claims.(*jwt.RegisteredClaims)
|
||
if !ok || c.Subject == "" {
|
||
return "", ErrInvalidToken
|
||
}
|
||
return c.Subject, nil
|
||
}
|
||
|
||
// HashPassword 用 bcrypt 哈希明文密码。
|
||
func HashPassword(pw string) (string, error) {
|
||
b, err := bcrypt.GenerateFromPassword([]byte(pw), bcrypt.DefaultCost)
|
||
return string(b), err
|
||
}
|
||
|
||
// CheckPassword 校验明文与 bcrypt 哈希是否匹配。
|
||
func CheckPassword(hash, pw string) bool {
|
||
return bcrypt.CompareHashAndPassword([]byte(hash), []byte(pw)) == nil
|
||
}
|
||
|
||
func envOr(k, def string) string {
|
||
if v := os.Getenv(k); v != "" {
|
||
return v
|
||
}
|
||
return def
|
||
}
|