Files
sundynix-agentix/sundynix-gateway/internal/middleware/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

86 lines
2.5 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 middleware
import (
"net/http"
"os"
"strings"
"github.com/gin-gonic/gin"
"github.com/sundynix/sundynix-gateway/internal/auth"
)
// CtxUserID 是鉴权后写入 gin.Context 的已验证用户 ID 键。
const CtxUserID = "uid"
// Auth 解析 Authorization: Bearer <JWT>,校验通过则把已验证 userID 写入上下文。
// 非阻断:无 token / 无效 token 时不报错,由各 handler(经 userID 兜底 header)或
// 后续 RequireAuth 决定是否放行。
func Auth() gin.HandlerFunc {
return func(c *gin.Context) {
h := c.GetHeader("Authorization")
if strings.HasPrefix(h, "Bearer ") {
if uid, err := auth.Parse(strings.TrimSpace(h[len("Bearer "):])); err == nil {
c.Set(CtxUserID, uid)
}
}
c.Next()
}
}
// RequireAuth 在 Auth 之后使用:上下文无已验证 userID 则 401 拒绝。
// 用于 owner 作用域的业务路由;SSE/导出等按 task_id 寻址的端点不挂(EventSource 无法带头)。
func RequireAuth() gin.HandlerFunc {
return func(c *gin.Context) {
if v, ok := c.Get(CtxUserID); ok {
if s, _ := v.(string); s != "" {
c.Next()
return
}
}
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
}