feat(phase3): 备案对接/全国统计/号段管理/BFF安全化/链合约源码
- 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(标准)/真实链部署 标注需外部环境
This commit is contained in:
@@ -0,0 +1,154 @@
|
||||
// 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)
|
||||
}
|
||||
Reference in New Issue
Block a user