iT邦幫忙

0

[BFF_Security] Day 4:多個 BFF——Adapter、Lens、Namespacing 與 Composition

  • 分享至 

  • xImage
  •  

這篇來聊聊 BFF 的一些常見 pattern

週三上午十點,全體研發大會。
CEO 在投影機前意氣風發地宣布了公司的狂暴擴張計畫:

「各位!公司剛剛完成了對一家叫車新創的戰略收購!
從今天起,我們的架構要同時支撐:電商手機 App、電商桌面 Web、叫車 App 乘客端、以及叫車 App 司機端!
後端微服務也從原本的 3 個,暴增到了包含 User、Order、CRM、推薦系統、行程服務等 6 個微服務!本週五必須全線打通!」

會議室裡一片倒抽冷氣的聲音。
回到座位上,前端小杰抓著頭髮看著白板,幾近崩潰:
「四個端點,打六個微服務……這意味著我們要手寫 $4 \times 6 = 24$ 條各自為政的網路發包膠水代碼!每個端點都要重寫一遍認證、逾時、欄位整形,這根本是義大利麵式的自殺架構啊!」

更慘烈的車禍立刻爆發在編譯階段——
小杰剛把 users 服務與 crm 客戶關係服務整合進同一個端點,編譯器直接無情噴出紅字:
💥 Duplicate identifier: 'User' is already declared in crm.go and users.go!

「救命啊!」小杰哀嚎:「兩個微服務都叫 User,裡面的 email 欄位一個是私人信箱、一個是公司公務統編信箱,合併在一起直接撞名車禍了!」

架構師老陳默默喝了一口茶,走上前在白板上畫了一個巨大的立方體:
「別慌,是時候請出 bff-patterns.com 的終極武器了——Virtual Graph(虛擬圖譜)與四重奏模式!」

https://ithelp.ithome.com.tw/upload/images/20261018/2010493030JRXa3gsC.png

什麼是 Virtual Graph?

小杰一臉困惑:「老陳,『圖譜』聽起來好像某種龐大複雜的圖資料庫(Neo4j)或是 GraphQL,我們真要在 BFF 裡搞這麼重的東西嗎?」

「別被名字嚇到了!」老陳搖搖頭:「在 bff-patterns.com 的定義中,Virtual Graph 其實是一個極度輕量、優雅的『微服務能力與型別地圖(Metadata Registry)』。它不是實體資料庫,它不存任何業務資料!」

為了讓全體工程師徹底搞懂,老陳從三個維度把 Virtual Graph 的底牌掀開:

1. 長相(它在程式碼中具體長什麼樣?)

它在記憶體中就是一個標準的 Map(詳見 day4/vgraph/graph.go):

type Graph struct {
    ops map[string]*Op // 記錄全公司所有微服務端點能力的元資料清單
}

每個節點(Op)清清楚楚記錄著:

  • 你是哪隻微服務(Namespace,例如 users)
  • 操作名稱與內部型別(例如 get 產出 users_User)
  • 需要什麼參數(Args,例如 id)
  • 會回傳哪些欄位(Fields,例如 ["id", "name", "email"])
  • 授權身分模式(以使用者本人名義 UserCred,還是 BFF 服務帳號 ServiceCred)

2. 目的(為什麼不直接手寫 http.Get,非要架一張虛擬圖譜?)

如果沒有這張圖,BFF 就只能淪為工程師手寫 http.Get 的「人工膠水層」:

維度 沒有 Virtual Graph(手寫膠水代碼) 有 Virtual Graph(宣告式圖譜)
拼裝 API 每個端點重複手寫 Goroutine、HTTP Client、錯誤處理。 搭樂高積木:宣告「步驟 1 抓 User,步驟 2 拿結果抓 Order」,幾行宣告搞定。
欄位拼錯 參數名打錯、欄位拼錯,只有在線上被使用者打到時才會 500 報錯。 啟動期編譯檢查:BFF 一開機自動對照圖譜,參數少傳或欄位寫錯,立刻拒絕啟動!
上游改版衝擊 後端默默改欄位名,BFF 完全不知道全公司有哪些端點會壞掉,盲目踩雷。 即時衝擊分析(Impact):一行指令查出全站有哪些端點受影響,精確鎖定修正範圍。
資安防禦 不小心把前端 URL 參數傳給高權限內部服務,引爆混淆代理人漏洞。 靜態安全攔截(Validate):圖譜發現你拿高權限打內部服務卻用了不可信前端參數,直接阻斷!

3. 作法(它是怎麼運作的?三部曲)

  1. 上游登記(Register):微服務啟動時進圖譜「自我介紹」(g.Register("users", usersService)),自動掛上前綴隔離。
  2. 搭積木組裝(Compose):各端點(Web, Mobile)像填配方表一樣,宣告自己的步驟與欄位依賴。
  3. 靜態安檢與掛載(Validate & Mount):BFF 啟動時驗證配方表是否合法(無死鎖、無錯字、無越權),驗證通過後掛載到 HTTP 端點,由引擎自動波浪排程發射!

4. 常見疑惑:這跟 OpenAPI (Swagger) JSON 有什麼差?

小杰忍不住舉手:「等等,老陳,既然 Virtual Graph 記錄了端點、參數、回傳欄位與型別,這聽起來跟我們平常常見的 OpenAPI(Swagger)JSON 有什麼差?不都是在描述 API 規格嗎?」

「問得太漂亮了!」老陳在白板上寫下一行大字:

「OpenAPI JSON 是各家微服務各自交出來的『零件清單(說明書)』;」
「而 Virtual Graph 是把這堆零件組裝起來、齒輪咬合的『自動化發動機(可執行 DAG)』!」

維度 OpenAPI (Swagger) JSON Virtual Graph(虛擬圖譜)
本質定位 靜態規格契約(Contract):告訴外界「我這個 API 長這樣,你可以這樣呼叫我」。自己不會動。 可執行的排程拓撲網(Executable DAG):告訴 BFF 引擎「這些服務如何並發、如何依賴傳遞與組裝」。
視角範圍 單一微服務孤島:users 有一份、orders 有一份,彼此互不認識,甚至可能型別撞名。 全局聯邦(Federation):強制掛上 Namespacing 統一前綴,在同一個座標系解決撞車。
資料流動 不知道資料從哪來:只描述訂單需要 userId 參數,但完全不知道這個值該從何而來。 精準資料綁定(Binding):清楚宣告參數來自 Session("sub")、前端 Client、還是前一步的 Ref("users", "id")。
動態執行 無法執行:純粹是一份規格檔案,無法直接拿來做排程調度。 內建波浪排程(Waves):動態計算依賴,無相依的並行全開,有相依的精準接棒。

https://ithelp.ithome.com.tw/upload/images/20261018/20104930BEvhh3TFts.png

「它們不但不衝突,反而是完美的上下游關係!」老陳補充道:
「在現代工業級架構(如 WunderGraph)中,各微服務團隊交出的就是 openapi.json。BFF 在啟動時會自動吃進這幾份 OpenAPI 規格書(Introspection),加上 Namespace 前綴,在記憶體裡自動融合成一張可執行的 Virtual Graph!OpenAPI 是原料,Virtual Graph 是工廠裝配線!」

5. 實戰演練:一個真實可跑的 Virtual Graph 範例

小杰說:「老陳,講了這麼多理論,能給個最直觀的實際代碼範例嗎?我想看它從微服務登記到最終 JSON 吐出來的完整過程!」

老陳打開專案目錄中的代碼(day4/vgraph/):「看好,這就是我們今天架構的原型跑起來的樣子!」

① 第一步:微服務登記進圖(graph.go)

小杰看到這行代碼,立刻皺起眉頭:「等等老陳,我看不懂!這三個微服務不是各自獨立跑在不同機器、不同 Docker 容器裡的獨立進程嗎?它們怎麼可能拿到我們 BFF 記憶體裡的 g 物件?難道是遠端的 User Service 主動打電話給 BFF 註冊?」

老陳笑了:「別誤會!不是微服務主動跑來 BFF 註冊,而是 BFF 在自己開機時,主動把遠端微服務『加為好友、登記入圖』!」

https://ithelp.ithome.com.tw/upload/images/20261018/201049301MtOFfBNaa.png

老陳指著代碼解釋:

  1. 微服務根本不需要改代碼:它們就是標準的 REST/gRPC 服務,完全不需要知道 BFF 的存在。
  2. userService 是 BFF 的代理客戶端:它實作了 BFF 內部的 Source 介面。
    • 在 Demo 測試代碼中:為了讓測試不用真的開三個 HTTP 伺服器,專案用記憶體物件(memSource)模擬。
    • 在生產環境中:BFF 開機時讀取各微服務的 openapi.json,把微服務的網址包裝成 HTTPSource。當 BFF 後續調用 users.get 時,底層其實就是發出真正的 HTTP 請求!
  3. BFF 在開機時強制掛上前綴:
g := NewGraph("bff-secret-token")
// BFF 主動將三個遠端服務登錄進圖,強制指定專屬 Namespace:
g.Register("users",  userService,  UserCred) // 包含 get 操作 (回傳 id, name, email)
g.Register("crm",    crmService,   UserCred) // 包含 contact 操作 (回傳 id, company, email)
g.Register("orders", orderService, UserCred) // 包含 list 操作 (回傳 items, total)
② 第二步:BFF 面向不同客戶端宣告配方表(fakes.go)

手機 App 與桌面 Web 同時從圖譜中「挑選積木」,零膠水代碼,純宣告式組裝:

// 📱 手機版:只要姓名與最新一筆訂單
homeMobile = &Composition{
    Name: "mobile-home",
    Steps: []Step{
        {ID: "user",   Op: "users.get",   Args: map[string]Binding{"id": Session("sub")}},
        {ID: "orders", Op: "orders.list", Args: map[string]Binding{"user": Session("sub"), "limit": Const(1)}},
    },
    Output: map[string]Out{
        "name":         {Step: "user",   Field: "name"},
        "latest_order": {Step: "orders", Field: "items"},
    },
}

// 💻 桌面 Web 版:抓齊自然人、公司統編、訂單歷史,解決欄位撞名
homeWeb = &Composition{
    Name: "web-home",
    Steps: []Step{
        {ID: "user",    Op: "users.get",   Args: map[string]Binding{"id": Session("sub")}},
        {ID: "contact", Op: "crm.contact", Args: map[string]Binding{"userId": Session("sub")}},
        {ID: "orders",  Op: "orders.list", Args: map[string]Binding{"user": Session("sub"), "limit": Const(3)}},
    },
    Output: map[string]Out{
        "name":       {Step: "user",    Field: "name"},
        "home_email": {Step: "user",    Field: "email"}, // 來自 users
        "work_email": {Step: "contact", Field: "email"}, // 來自 crm,完美避開撞名!
        "company":    {Step: "contact", Field: "company"},
        "orders":     {Step: "orders",  Field: "items"},
    },
}
③ 第三步:啟動期自動抓錯(靜態安全檢查)

如果實習生不小心把欄位拼錯:

Output: map[string]Out{
    "nickname": {Step: "user", Field: "nickname"} // 💥 寫錯了!User 服務根本沒有 nickname
}

BFF 開機時執行 c.Validate(g),在第一秒直接拒絕啟動,噴出精準錯誤:

composition "mobile-home": output "nickname" references field "nickname" which users.get does not return!

錯誤在編譯與啟動期就被消滅,絕不會留到生產線上讓使用者遇到 500!

④ 第四步:實際執行與終端機輸出(main.go)

在專案目錄下實際執行:

cd day4/vgraph && go run .

終端機秒回輸出的真實 JSON:

mobile  {"name":"Nathan","latest_order":["o3:900"]}
web     {"name":"Nathan","home_email":"user-123@home.example","work_email":"user-123@acme.example","company":"ACME","orders":["o3:900","o2:300","o1:120"]}

「太神了!」小杰雙眼放光:「完全沒有寫半行重複的 HTTP Request 與 Unmarshal,不同客戶端拿到的資料完全量身客製,而且上線前全部經過靜態安全檢查!」
「沒錯,」老陳點點頭:「現在,就讓我們從微服務之間最常見的第一個災難——『型別撞名』開始拆解!」


第一幕:Namespacing——給型別掛上「防撞名牌」

「小杰,現實世界裡的微服務本來就會各自演進。」老陳指著白板:「users 服務的 User 是指『自然人』;crm 服務的 User 是指『企業客戶』。強行要求後端改名只會引發部門戰爭。」

小杰抓了抓頭:「等等,老陳,如果強制加上命名空間,那我們吐給前端的 JSON 難道要變成醜陋的 users_email、crm_email,或者每一筆資料都要包一層 {"users": {...}, "crm": {...}} 的巢狀 namespace 物件嗎?前端一定會抗議的!」

老陳笑著搖搖頭:

「別誤會!API Namespacing 是 BFF 內部的『防撞安全網』,而不是強加給前端的枷鎖!」

  1. 在 BFF 內部(虛擬圖譜):強制為微服務掛上前綴(users.get vs crm.contact、users_User vs crm_User),是為了解決微服務撞名時的調度混淆與型別覆蓋問題。
  2. 在 BFF 對外(客戶端 API):BFF 是外帶調味盤,內部雖然隔著 namespace,但在組裝輸出時,依然能自由將欄位映射成前端最想要的乾淨語意(例如 home_email 與 work_email)。前端根本不需要知道後端有什麼 namespace!

在專案目錄 day4/vgraph/graph.go 中,我們讓微服務在登記進入虛擬圖譜時,自動完成前綴隔離:

// 詳見 day4/vgraph/graph.go
func (g *Graph) Register(ns string, src Source, mode CredKind) error {
	// 嚴格校驗 Namespace 格式 (小寫開頭、禁特殊符號)
	if !nsPattern.MatchString(ns) { ... }
	if g.ns[ns] { return fmt.Errorf("namespace %q already registered", ns) }

	// 自動為所有操作與型別掛上專屬前綴!
	staged[full] = &Op{
		FullName: ns + "." + d.Name,      // 變成 "users.get" 與 "crm.contact"
		TypeName: ns + "_" + d.Type,      // 變成 "users_User" 與 "crm_User"
		...
	}
}

https://ithelp.ithome.com.tw/upload/images/20261018/20104930IC1MY7GHUq.png

小杰揉了揉眼睛,原本撞得頭破血流的型別錯誤瞬間煙消雲散。
BFF 內部清楚地透過命名空間知道該向誰發送請求,而 Web 前端拿到的依舊是量身打造、乾淨無衝突的專屬 JSON,清清楚楚,各得其所!


第二幕:API Composition——波浪式發射與精準衝擊分析

搞定了命名衝突,接著是多微服務的聚合發送。
Web 首頁要同時抓取「使用者資料、CRM 記錄、最新訂單、個人化推薦」四個服務。如果照順序一筆一筆抓,每個 100ms,加起來使用者就要看 400ms 的轉圈圈。

小杰不以為然地說:「這有什麼難的?BFF 本來不就是要去抓這麼多服務嗎?寫個類似前端的 Promise.all,或者 Go 開幾個 Goroutine 併發全開去抓,不就結了?」

老陳笑了笑:「小杰,如果所有 API 彼此互不相干,那確實 Promise.all 就完事了。但真實世界的業務哪有這麼理想?」

老陳在白板上指著第 5 個端點:

「看看這個端點:discount.calculate(算折扣)。它必須依賴訂單服務算出來的歷史消費總額才能運算!
如果 API 之間存在前後相依關係(A 的結果要餵給 B),你手寫 Promise 會怎麼寫?

  • 一個一個 await?直接退化回 Day 1 的瀑布流龜速。
  • 手寫巢狀 Promise 或通道鎖?微服務一多,程式碼立刻陷入深淵般的 Callback Hell,只要後端改一個欄位,整條手寫調度鏈就全崩了!」

真正的救贖:拓撲波浪排程(Execution Waves)

在專案目錄 day4/vgraph/compose.go 中,我們不讓工程師硬編碼發送順序,而是宣告一張「有向無環圖(DAG)」:
工程師只要宣告:「我的參數來自 Ref("orders", "total")」,剩下的排程交由 BFF 引擎自動推算:

https://ithelp.ithome.com.tw/upload/images/20261018/20104930M9SwkKPum2.png

這套排程器相比手寫 Promise.all,帶來了四大降維打擊的威力:

  1. 自動波浪解依賴:引擎每毫秒自動檢查「誰的依賴已經滿足?」,一就緒自動湊成同一波並行發射,完全免去手寫巢狀異步代碼。
  2. 死鎖與循環相依檢測(Cycle Detection):在服務啟動時自動跑拓撲檢查,如果工程師不小心寫出「A 等 B,B 又等 A」的死循環,啟動當場報錯,絕不在線上卡死!
  3. 智慧剪枝(Query Pruning):如果手機端只宣告需要 name,引擎會反向逆推依賴圖,自動砍掉訂單與折扣的請求,不上游發送多餘流量!
  4. 局部優雅降級(Partial Failure):可選的推薦系統如果超時或故障,不會像 Promise.all 一樣全盤皆崩,而是自動標記為 unavailable,首頁其餘核心資料依舊秒回!

在單元測試 TestRun_IndependentStepsRunInParallel 中,我們設立了毫秒級同步柵欄,驗證第一波的四個服務確實是同一瞬間並行發出,把原本的排隊延遲腰斬了 75%!

震撼的即時衝擊分析(Impact Analysis)

「那如果後天 CRM 團隊又悄悄把 company 欄位改名呢?」小杰心有餘悸地問。

「看看這個!」我在終端機裡敲下一行程式:

// 查詢若上游改動欄位,全站有哪些 BFF 組合會陣亡?
affected := Impact(allCompositions, "users.get", "email")
// 回傳: ["web-home"] (精確鎖定受影響端點,而 mobile-home 未取用該欄位,安然無恙!)

在微服務改動上線前,CI 管線或 BFF 啟動掛載時就會當場報警攔截,再也不用怕被後端隊友暗箭刺傷!


第三幕:驚天大漏洞——Lens 只是副「墨鏡」,根本不驗身分證!

下午三點,叫車業務上線測試。
叫車服務的核心模型是「一趟行程(trips.trip)」,裡面有路線、車資、乘客評價,以及司機專屬的私密備註(driver_notes)。

小杰得意地介紹他的 API Lens(資料濾鏡) 設計:
「看!我用了 bff-patterns.com 的 Lens 模式!
乘客端掛載時設為 Audience: rider,司機端設為 Audience: driver。這樣一來,driver_notes 只有司機看得到,乘客看這筆資料時該欄位就會自動隱形!超優雅對吧?」

// 小杰一開始寫的程式碼 (埋下滔天大禍!)
tripComp = &Composition{
	Steps: []Step{{ID: "t", Op: "trips.trip", Args: map[string]Binding{"id": Client("id")}}},
	Output: map[string]Out{
		"route":        {Step: "t", Field: "route"},
		"fare":         {Step: "t", Field: "fare"},
		"driver_notes": {Step: "t", Field: "driver_notes", Audience: "driver"}, // 司機專屬
	},
}
rider.Mount(tripComp, Lens{Audience: "rider"})
driver.Mount(tripComp, Lens{Audience: "driver"}) // ⚠️ 漏掉了角色驗證!

資安長老張走過來,二話不說打開終端機,用普通乘客的帳號登入,然後對司機端端點發了一個請求:
GET /driver/trip?id=trip-888!

下一秒,螢幕上赫然吐出了一行血淋淋的 JSON:
"driver_notes": "該乘客態度極差,沿途摔車門,且拒絕支付小費!"

小杰的臉刷地一下白了:「怎麼會這樣?!老張,這不對啊!Lens 的作用不就是像 PII 敏感資料遮蔽一樣,把不該看的欄位遮掉嗎?我在乘客端明明已經把 driver_notes 遮蔽掉了,這難道不是在做資安防護嗎?為什麼乘客能看到司機端點的資料?這明明是授權問題吧?!」

「你說得太對了!這本質上就是個徹頭徹尾的 Authorization 大漏洞!」老張敲著白板厲聲喝道:
「而你今天捅出這個簍子,就是因為你犯了全天下前後端開發者最常犯的致命盲點:『把 PII 欄位遮蔽(Lens),當成了 Authorization!』」

https://ithelp.ithome.com.tw/upload/images/20261018/20104930KwFABJl8TT.png

老張在白板上重重畫了一個對照表格,給全體工程師上了無比震撼的一課:

🚨 為什麼「欄位遮蔽」絕不等於「存取授權」?

很多工程師跟小杰一樣,有一種虛假的安全感:

  • 「我想說,乘客只會打開乘客 App 打 /rider/trip,司機只會打開司機 App 打 /driver/trip;既然我在乘客端把私密備註遮掉了,司機端才有,那資料不就安全了嗎?」

黑客才不管你有沒有前端 App!黑客打開 Postman,直接換個網址打 /driver/trip。
當 /driver/trip 端點收到請求時:

  1. 它沒有派警衛查驗身分(缺少 Authorization),根本沒查呼叫者是不是司機。
  2. 它只問了墨鏡(Lens):「我這個端點要露出哪些欄位?」墨鏡說:「司機端點要露出 driver_notes。」於是,私密資料就這樣被毫無防備地送給了黑客!
機制 現實比喻 它負責的事 它完全不知道的事
Lens(資料濾鏡)(PII / 欄位遮蔽) 墨鏡 決定回傳 JSON 的**「形狀」**(例如:乘客看 A 欄位、司機看 A+B 欄位)。 它根本不問門外站著的人是誰! 只要進了這個端點,它就按端點標籤把資料吐出來。
Authorization(授權)(例如 RequireRole: "driver") 門神 / 警衛 檢查這張證件有沒有權限進這道門。沒權限當場回傳 403 Forbidden 轟出去! 不管回傳 JSON 的欄位長相。

「絕對不能拿墨鏡當警衛,遮蔽欄位不等於存取控制!」
這就像銀行在貴賓 VIP 室的桌上擺了一份極機密資產報告,卻不在 VIP 室門口派警衛查驗證件,只寄望「一般散戶應該只會去普通櫃台吧」一樣荒謬。

正確的做法永遠是:門口先用 RequireRole: "driver" 查驗身分(Authorization),進門後才由 Lens 決定能看哪些欄位(PII Masking)!

在單元測試 TestLens_IsNotAuthorization 中,我們親手證明了這個漏洞的存在:

  1. 未設防版本:乘客身分呼叫 /driver/trip,輕鬆拿到 owes tip 差評備註,測試直接亮紅燈!
  2. 安全版本:補上 RequireRole: "driver" 後,越權請求在踏入微服務之前就被 403 當場擊斃,上游微服務連一次請求都沒收到!

第四幕:糊塗的快遞員——Confused Deputy(混淆代理人)爆發

還沒從小丑般的震驚中緩過來,實習生阿呆那邊又引爆了一枚核彈級漏洞。

在設計訂單查詢時,由於內部的訂單微服務部署在極高安全性的私有內網中,只接受具備管理員權限的 BFF 服務帳號(ServiceCred) 呼叫。
阿呆寫了一段組合代碼:

// 致命的混淆代理人代碼!
Step{
    ID: "orders",
    Op: "orders.list", // 這是以 ServiceCred (最高權限服務帳號) 運行的操作!
    Args: map[string]Binding{
        "user": Client("user"), // 💥 參數直接綁定來自前端 URL 的查詢字串!
    },
}

黑客打開瀏覽器,登入自己的帳號 user-123,然後在網址列輕輕敲入:
GET /web/ordersof?user=user-999!

結果——黑客毫無阻礙地拿到了全站所有使用者 user-999 的歷史訂單與信用卡消費明細!

https://ithelp.ithome.com.tw/upload/images/20261018/20104930goWOQlbVWR.png

老陳面色無比凝重:
「這就是雲端安全中最惡名昭彰的 Confused Deputy(混淆代理人) 攻擊!

  • 訂單微服務沒有錯,它按合約信任擁有高權限的服務帳號。
  • 黑客沒有破解任何密碼,他只是傳了個參數。
  • 錯的是 BFF!它就像一個手握總裁金庫萬能鑰匙的快遞員,被路邊的實習生隨口指使了一句,就傻傻地拿著萬能鑰匙去把別人的保險箱搬了出來!」

雙重鋼鐵防線:掛載期靜態阻斷

在 day4/vgraph/compose.go 中,我們為圖譜編譯器注入了一道絕對不妥協的靜態審查規則:

// 凡是以 ServiceCred (服務帳號) 運行的操作,
// 絕對禁止將前端傳入的 Client 不可信參數,灌入標註為 ObjectKeys 的敏感欄位中!
if op.Mode == ServiceCred && slices.Contains(op.Desc.ObjectKeys, name) {
	add("... client-controlled %q flows into object key %q of %s, which is called with the SERVICE credential (confused deputy)", ...)
}

在單元測試 TestConfusedDeputy_ServiceCredentialLetsClientReadAnyone 中,這段危險代碼在 BFF 啟動掛載期(Mount)直接被編譯器當頭棒喝、拒絕上線!
同時,我們強制推行 UserCred 模式:盡可能將使用者的真實 Token 傳遞給微服務,讓下游親眼辨認操作者身分,徹底粉碎了混淆代理人漏洞!


第五幕:部分失敗降級——絕不把 403 吐給前端的資安深意

到了下午六點,我們開始測試異常情境:
當一個組合同時發起 4 個微服務時,若「商品推薦系統」因為內部權限不足回傳了 403 Forbidden,或者網路逾時掛掉了,BFF 該怎麼回?

小杰寫的回應是:
{"error": "reco service returned 403 Forbidden: User has no VIP permission"}

老張立刻搖頭否決:
「絕對不能把真正的 403 和錯誤細節直接吐給前端!」

https://ithelp.ithome.com.tw/upload/images/20261018/20104930KC3NFfJPXY.png

💡 防探測的深層資安智慧:
如果你在回傳的 JSON 裡告訴黑客 "reco service: 403 Forbidden",黑客立刻就知道:
「原來這個未公開的 VIP 推薦系統是真實存在的!只是我當前的帳號進不去!」
這等於給了黑客一張內部架構的尋寶雷達圖。
正確做法是對外一律回傳模糊的 unavailable,將真正的資安與技術錯誤安全保留在伺服器端日誌(Result.Errors),既保障了前端的降級可用性,又保護了內網邊界!


尾聲:週三深夜的暴風雨前夕

深夜十點,專案目錄下的 20 個單元測試全數以高標準通過:

cd day4/vgraph && go test -race -v -count=1 ./...
# 20 項測試全部 PASS:命名空間隔離、波浪排程並發柵欄、Lens 角色越權攔截、混淆代理人靜態阻斷全數通過!

看著終端機上的綠燈,小杰長長舒了一口氣:
「一張虛擬圖譜,搞定了多端組合、消除了命名撞車、防住了 Lens 越權與混淆代理人……現代微服務的世界總算被我們理順了。」

老陳默默轉過身,指著機房角落那台落滿灰塵、嗡嗡作響的古老黑色伺服器:
「小杰,先別高興得太早。我們剛才搞定的,都是全新的、漂亮的現代微服務。
但明天一早,我們收購進來的那套運行了十幾年、沒人敢拔插頭的遠古遺留單體(Legacy Monolith),就要正式交接過來了。

  • 那台老古董只認一條寫死的 Master API Key,完全不懂什麼叫 OAuth,它看不見任何使用者身分!
  • 當使用者修改 Email 時,新舊兩套資料庫要怎麼同時寫入?如果寫到一半網路斷線,新舊系統資料分裂了(Data Drift),該怎麼補償回滾?
  • 還有……外部合作夥伴送來的 Form 表單 Webhook,黑客是怎麼用參數污染和重放快取投毒,把系統當場幹趴下的?」

小杰看著那台嗡嗡作響的老古董伺服器,吞了吞口水。
真正的硬核戰役,明天才要正式打響……

(未完待續,請見 Day 5:【第四夜:不能拔插頭的古董】新舊系統之間的夾心餅乾——Evolution、Bridge、SIA 與雙寫一致性(Coordination))


📚 本回參考文獻


圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言