// 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 }