Files
sundynix-agentix/sundynix-gateway/internal/auth/auth.go
T
Blizzard e05e6f5903 fix(gateway): 三处生产安全硬化(默认密钥/admin裸奔/CORS)
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>
2026-06-18 12:55:04 +08:00

92 lines
2.7 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 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 为某用户签发 JWTsubject = 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 并返回 userIDsubject)。无效/过期/签名不符返回 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
}