8db9d33694
- A.1 备案对接: BindFiling/QueryFiling 关联网标号+备案号 - A.2 监管上报: DailyRegulatoryReport 日报 - B.1 号段管理: ListSegments + /admin/segments - C.1/C.2 全国统计按省聚合 + 跨省协同(单一可信源天然联动) - F.2 全国监管大屏: NationalStats(按省/类目/状态) - B(遗留) 监管大屏BFF: internal/bff + cmd/console-bff, 密钥仅存后端浏览器只用会话令牌 - G 真实链合约源码: contracts/tcs_registry/registry.go (ChainMaker Go) - 新增9个API+BFF服务; 5项新测试; 端到端BFF验证 - D/E(压测/等保/HSM)/F.1(标准)/真实链部署 标注需外部环境
155 lines
4.0 KiB
Go
155 lines
4.0 KiB
Go
// 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)
|
|
}
|