// Package bff 实现监管控制台的 Backend-For-Frontend(三期 B)。 // // 安全目标(替换 MVP 演示态前端直连 + 前端持密钥): // - 凭证(API Key/Secret)仅存于 BFF 后端,绝不下发浏览器 // - 浏览器用会话令牌(Session Token)访问 BFF // - BFF 校验会话 + RBAC,再以服务端 HMAC 签名代理到 api-svc package bff import ( "bytes" "crypto/rand" "encoding/hex" "io" "net/http" "sync" "time" "github.com/gin-gonic/gin" "github.com/tcs-iptv/tcs/internal/httpx" ) // roleCred 角色对应的 api-svc 凭证(仅存于 BFF)。 type roleCred struct { apiKey string apiSecret string } // session 登录会话。 type session struct { user string role string expiresAt time.Time } // BFF 控制台后端。 type BFF struct { apiBase string creds map[string]roleCred // role -> cred users map[string]struct{ pass, role string } mu sync.RWMutex tokens map[string]session client *http.Client } // New 创建 BFF。apiBase 如 http://localhost:8080 func New(apiBase string) *BFF { b := &BFF{ apiBase: apiBase, creds: map[string]roleCred{}, users: map[string]struct{ pass, role string }{}, tokens: map[string]session{}, client: &http.Client{Timeout: 10 * time.Second}, } return b } // SetCred 配置角色凭证(从 Vault/环境加载,不入前端)。 func (b *BFF) SetCred(role, apiKey, apiSecret string) { b.creds[role] = roleCred{apiKey, apiSecret} } // AddUser 配置控制台用户(生产接 SSO/LDAP)。 func (b *BFF) AddUser(user, pass, role string) { b.users[user] = struct{ pass, role string }{pass, role} } func newToken() string { buf := make([]byte, 24) _, _ = rand.Read(buf) return hex.EncodeToString(buf) } // Login 用户名口令登录,返回会话令牌(不下发任何密钥)。 func (b *BFF) Login(c *gin.Context) { var req struct{ User, Pass string } if err := c.ShouldBindJSON(&req); err != nil { httpx.Error(c, 400, "INVALID_REQUEST", err.Error()) return } u, ok := b.users[req.User] if !ok || u.pass != req.Pass { httpx.Error(c, 401, "UNAUTHORIZED", "用户名或口令错误") return } tok := newToken() b.mu.Lock() b.tokens[tok] = session{user: req.User, role: u.role, expiresAt: time.Now().Add(8 * time.Hour)} b.mu.Unlock() httpx.OK(c, gin.H{"token": tok, "role": u.role, "user": req.User}) } // sessionOf 校验会话令牌。 func (b *BFF) sessionOf(tok string) (session, bool) { b.mu.RLock() s, ok := b.tokens[tok] b.mu.RUnlock() if !ok || time.Now().After(s.expiresAt) { return session{}, false } return s, true } // AuthMiddleware 校验浏览器会话令牌(Bearer)。 func (b *BFF) AuthMiddleware() gin.HandlerFunc { return func(c *gin.Context) { tok := c.GetHeader("X-Session-Token") s, ok := b.sessionOf(tok) if !ok { httpx.Error(c, 401, "UNAUTHORIZED", "会话无效或过期,请重新登录") c.Abort() return } c.Set("bff_role", s.role) c.Next() } } // Proxy 以服务端凭证 HMAC 签名后代理到 api-svc(密钥不出 BFF)。 func (b *BFF) Proxy(c *gin.Context) { role, _ := c.Get("bff_role") cred, ok := b.creds[role.(string)] if !ok { httpx.Error(c, 403, "FORBIDDEN", "角色无对应凭证") return } // 透传 /api/v1/* 路径 path := c.Param("path") fullPath := "/api/v1" + path method := c.Request.Method var body []byte if c.Request.Body != nil { body, _ = io.ReadAll(c.Request.Body) } sig := httpx.Sign(cred.apiSecret, method, fullPath) url := b.apiBase + fullPath if c.Request.URL.RawQuery != "" { url += "?" + c.Request.URL.RawQuery } req, _ := http.NewRequest(method, url, bytes.NewReader(body)) req.Header.Set("Authorization", "TCS "+cred.apiKey+":"+sig) if len(body) > 0 { req.Header.Set("Content-Type", "application/json") } resp, err := b.client.Do(req) if err != nil { httpx.Error(c, 502, "BAD_GATEWAY", err.Error()) return } defer resp.Body.Close() out, _ := io.ReadAll(resp.Body) c.Data(resp.StatusCode, "application/json", out) }