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>
This commit is contained in:
@@ -82,6 +82,7 @@
|
||||
- [x] 文件主表,文档间关联用雪花 ID(弃用按名关联)
|
||||
- [x] 后端首批单测(19 纯逻辑用例:引擎/DSL/docx/报告)+ mcp-go 集成测试(Profile 迁移)
|
||||
- [x] **真实鉴权(JWT)闭环**:后端注册/登录/校验 + RequireAuth 保护路由 + owner=已验证 uid(去掉 header 兜底);前端登录/注册门 + 存 token + Bearer + 401 自动登出 + 顶栏用户/登出。实跑验证(含 CORS Authorization 修复)
|
||||
- [x] 生产安全硬化:JWT 默认密钥生产 fail-fast · /admin 加 RequireAdmin(ADMIN_USER_IDS 白名单)· CORS 来源可配(CORS_ALLOW_ORIGIN)
|
||||
- [ ] 集成/前端测试(`runGraph` / `handleReport` 需 mock pool/tools/sink;前端无测试)
|
||||
|
||||
---
|
||||
|
||||
@@ -3,7 +3,9 @@ package auth
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"log"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
@@ -13,8 +15,28 @@ import (
|
||||
// TokenTTL 是访问令牌有效期。
|
||||
const TokenTTL = 24 * time.Hour
|
||||
|
||||
// secret 是 JWT 签名密钥。生产经环境变量 JWT_SECRET 注入;缺省仅供开发(务必覆盖)。
|
||||
var secret = []byte(envOr("JWT_SECRET", "sundynix-dev-secret-change-me"))
|
||||
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")
|
||||
|
||||
@@ -2,6 +2,7 @@ package middleware
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
@@ -40,3 +41,45 @@ func RequireAuth() gin.HandlerFunc {
|
||||
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "需要登录"})
|
||||
}
|
||||
}
|
||||
|
||||
// RequireAdmin 保护运维控制面:必须登录,且(设了 ADMIN_USER_IDS 时)uid 须在白名单内。
|
||||
// ADMIN_USER_IDS 为空:开发期放行任意登录用户;生产期(APP_ENV=prod/GIN_MODE=release)直接拒绝
|
||||
// ——逼运维显式配置管理员,杜绝"任意账号改模型/密钥配置"。
|
||||
func RequireAdmin() gin.HandlerFunc {
|
||||
allow := splitEnv("ADMIN_USER_IDS")
|
||||
prod := strings.EqualFold(os.Getenv("APP_ENV"), "production") || strings.EqualFold(os.Getenv("APP_ENV"), "prod") ||
|
||||
strings.EqualFold(os.Getenv("GIN_MODE"), "release")
|
||||
return func(c *gin.Context) {
|
||||
uid, _ := c.Get(CtxUserID)
|
||||
id, _ := uid.(string)
|
||||
if id == "" {
|
||||
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "需要登录"})
|
||||
return
|
||||
}
|
||||
if len(allow) == 0 {
|
||||
if prod {
|
||||
c.AbortWithStatusJSON(http.StatusForbidden, gin.H{"error": "未配置管理员(ADMIN_USER_IDS)"})
|
||||
return
|
||||
}
|
||||
c.Next() // 开发期放行
|
||||
return
|
||||
}
|
||||
for _, a := range allow {
|
||||
if a == id {
|
||||
c.Next()
|
||||
return
|
||||
}
|
||||
}
|
||||
c.AbortWithStatusJSON(http.StatusForbidden, gin.H{"error": "需要管理员权限"})
|
||||
}
|
||||
}
|
||||
|
||||
func splitEnv(key string) []string {
|
||||
var out []string
|
||||
for _, p := range strings.Split(os.Getenv(key), ",") {
|
||||
if p = strings.TrimSpace(p); p != "" {
|
||||
out = append(out, p)
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
package router
|
||||
|
||||
import (
|
||||
"os"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
|
||||
"github.com/sundynix/sundynix-gateway/internal/blob"
|
||||
@@ -55,8 +57,8 @@ func New(db *store.Postgres, cache *store.Redis, bus *nats.Bus, blobStore *blob.
|
||||
p.GET("/billing", h.Billing)
|
||||
}
|
||||
|
||||
// 运维控制面:LLM 模型配置(独立运维控制台调用;鉴权待后续接管理员角色)。
|
||||
admin := api.Group("/admin")
|
||||
// 运维控制面:LLM 模型配置(含 API 密钥管理)—— 必须管理员(RequireAdmin)。
|
||||
admin := api.Group("/admin", middleware.RequireAdmin())
|
||||
{
|
||||
admin.GET("/models", h.ListModels)
|
||||
admin.POST("/models", h.SaveModel)
|
||||
@@ -68,10 +70,18 @@ func New(db *store.Postgres, cache *store.Redis, bus *nats.Bus, blobStore *blob.
|
||||
return r
|
||||
}
|
||||
|
||||
// cors 放开跨源访问,允许桌面端/浏览器带自定义身份头与 SSE 访问网关(开发期)。
|
||||
// cors 控制跨源访问。允许来源经 CORS_ALLOW_ORIGIN 配置(缺省 "*" 仅供开发;
|
||||
// 生产应设为具体源,如 https://app.example.com)。Vary 保证按 Origin 正确缓存。
|
||||
func cors() gin.HandlerFunc {
|
||||
origin := "*"
|
||||
if v := os.Getenv("CORS_ALLOW_ORIGIN"); v != "" {
|
||||
origin = v
|
||||
}
|
||||
return func(c *gin.Context) {
|
||||
c.Header("Access-Control-Allow-Origin", "*")
|
||||
c.Header("Access-Control-Allow-Origin", origin)
|
||||
if origin != "*" {
|
||||
c.Header("Vary", "Origin")
|
||||
}
|
||||
c.Header("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
|
||||
c.Header("Access-Control-Allow-Headers", "Content-Type, Authorization, X-Session-ID, X-User-ID")
|
||||
if c.Request.Method == "OPTIONS" {
|
||||
|
||||
Reference in New Issue
Block a user