Shopping-Cart-Auth
carts/create,可能是消費者用瀏覽器登入後直接呼叫,也可能是合作夥伴系統代替消費者下單,還可能是門市店員在幫客人代客下單。這三種來源的身份驗證方式完全不一樣,這篇要拆解 Shopping 服務怎麼判斷每一次請求究竟屬於哪一種身份,驗證通過之後又還會做哪些「這個人可不可以做這件事」的檢查。先說結論,整支 API 的驗證其實分成兩個完全不同層級,發生的位置與檢查的問題都不一樣,混在一起看容易誤解成同一套機制。
CartsController.Create 方法執行之前,由 ASP.NET Core 的驗證管線處理,確認這次請求是誰打的,回答的是身份問題。CartService.CartCreateAsync 方法內部,是手動寫在程式碼裡的呼叫,確認這個已登入的會員這次操作允不允許,回答的是資格問題,不是身份問題。白話理解:一個會員完全可能通過第一層身份驗證,他確實是這個帳號本人,卻在第二層被業務規則擋下來,例如這個會員在商店黑名單裡,或短時間內呼叫太多次觸發限流。這兩層各自獨立,任何一層擋下都會讓整支 API 失敗,但失敗的原因與回應方式不一樣。
Shopping 同時支援三種不同的驗證方式,因為 carts/create 這支 API 會被三種不同來源呼叫。系統用一段動態判斷邏輯決定這次要用哪一種驗證方式,判斷順序如下。
| 驗證方式 | 使用時機 | 判斷依據 |
|---|---|---|
| PartnerApi | 合作夥伴系統代替消費者呼叫 | Header 帶 N1-INTERNAL-MEMBER-ID |
| N1SessionToken | 門市店員代客下單 | Header 帶 NY-SESSION-TOKEN |
| NineYiCookies | 消費者用瀏覽器登入後直接呼叫,mweb 走這一種 | 以上兩個 Header 都沒有時的預設值 |
這段判斷邏輯寫在應用程式啟動設定裡,程式碼如下。
options.ForwardDefaultSelector = context => { //// 優先判斷 PartnerApi(N1-INTERNAL-MEMBER-ID) var memberIdStr = context.Request.Headers["N1-INTERNAL-MEMBER-ID"].ToString(); if (string.IsNullOrEmpty(memberIdStr) == false) return AuthenticationSchemeEnum.PartnerApi.ToString(); //// 其次判斷代客下單(NY-SESSION-TOKEN) var sessionToken = context.Request.Headers["NY-SESSION-TOKEN"].ToString(); if (string.IsNullOrEmpty(sessionToken) == false) return AuthenticationSchemeEnum.N1SessionToken.ToString(); //// 預設使用 NineYiCookies return AuthenticationSchemeEnum.NineYiCookies.ToString(); };
mweb 前端呼叫 carts/create 走的是 NineYiCookies 這條路,實際驗證邏輯集中在專門處理 Cookie 驗證的程式裡,每次請求都會經過這段。
auth/uauth 加密字串。auth 走不同分支,判斷是已登入、免登,還是允許以訪客身份繼續。核心分支邏輯依 auth 存不存在分成兩條路徑,整理成表格如下。
| 情境 | 判斷依據 | 結果 |
|---|---|---|
有 auth Cookie | 直接解密 Cookie 內容 | 解密成功則拿到會員資料,解密失敗視為登入失敗 |
沒有 auth,但有 uauth | 用 uauth 解出匿名裝置編號,去 Redis 查登入快取 | Redis 裡有登入資料且未過期,視為登入成功並要求 Cookie 續期 |
| Redis 查無登入資料,且允許免登 | 這次請求的路徑命中設定檔的免登白名單 | 建立一個只有裝置編號的訪客身份,不視為登入失敗 |
| Redis 查無登入資料,允許以訪客身份繼續 | 路徑命中另一份訪客白名單,且對應商店有開啟訪客購物車功能 | 建立明確標記為訪客的身份,購物車後續流程會跳過部分只對真實會員有意義的檢查 |
| 以上皆不成立 | 沒有任何登入憑證,也不允許免登 | 判定為未登入,驗證流程失敗 |
另外兩種驗證方式都不走 Cookie,而是各自實作一套驗證流程,格式類似,都是驗證通過後手動把會員資料寫進系統可以識別的憑證裡。
共通點:這兩種驗證失敗時的處理方式一致,都會回傳固定格式的錯誤訊息,帶著相同的未登入錯誤碼,讓前端可以依錯誤碼統一處理未登入導轉。
不管走哪一種驗證方式,最後都要通過同一道共用的授權政策才算真的過關。
builder.Services.AddAuthorization(options =>
{
options.DefaultPolicy = new AuthorizationPolicyBuilder(AuthenticationSchemeEnum.MultiAuthSchemes.ToString())
.RequireClaim(ClaimTypesEnum.LoginMemberEntity.ToString())
.Build();
});
這段的意思很直白,就算前面的驗證流程本身沒有失敗,只要最後沒有成功把代表登入狀態的 Claim 寫進憑證裡,一樣會判定授權失敗,回傳未登入的結果。這代表三種驗證方式雖然實作邏輯完全不同,但對外呈現的授權合格標準是同一個,都是有沒有這個 Claim。
但取得會員資料的方式並不一樣:三種驗證方式雖然最後都要滿足同一道 Claim 檢查,但在管線內「怎麼拿到會員資料」其實是分岔的,不是每一種都會即時查詢會員服務。
換句話說,Cookie 驗證是「資料自帶」(解密或查快取即可),PartnerApi/N1SessionToken 驗證是「先驗證身份合法性,再即時查詢會員資料」,兩種取得會員資料的成本與時機並不相同,但最終都要落地成同一個 Claim 才能通過共用授權政策。
身份驗證通過只代表這個人是誰確認無誤,還不代表這次操作一定會被放行。建立購物車的業務邏輯內部還有兩道手動寫在程式碼裡的資格檢查,執行順序如下。
| 情境 | 判斷依據 | 處理方式 |
|---|---|---|
| 訪客身份 | 被標記為訪客的請求 | 直接跳過黑名單檢查,因為根本沒有會員身份可以查 |
| 全站黑名單 | 黑名單清單中存在整個商城等級的紀錄 | 清空這個會員的購物車,整支 API 直接失敗 |
| 商店黑名單,且允許付款方式清單為空 | 黑名單存在對應商店的紀錄,且查不到任何允許使用的付款方式 | 同樣清空購物車並失敗 |
| 商店黑名單,但還有允許的付款方式 | 黑名單存在對應商店的紀錄,但至少有一種付款方式允許使用 | 放行,後續結帳流程只能挑這些付款方式 |
| 完全不在任何黑名單 | 黑名單查詢結果為空清單 | 正常放行,不做任何額外處理 |
前面第一層提到,Cookie 驗證流程裡有兩種不需要登入也能放行的情境,一種單純叫免登,一種是明確標記為訪客身份,兩者容易被誤會成同一件事,這裡補充說明差異。
常見誤解:以為只要呼叫端傳入某個參數就能免登,實際上這兩種放行規格都不是呼叫端能傳的參數,而是伺服器依這次請求打的 API 路徑,去比對設定檔白名單自己算出來的,前端完全沒有管道能設定或偽造。
carts/create 就屬於這一類,需要對應商店開啟訪客購物車功能。兩者產生的會員資料骨架其實一樣,都只有裝置編號,沒有真正的會員編號,差別在多一個訪客標記旗標。carts/create 這條流程裡,建立購物車時要查詢的會員優惠券、會員點數、會員黑名單這三個步驟,都需要真實存在的會員編號才查得到有意義的結果,如果沒有這個訪客標記讓程式碼提早跳過,這幾支呼叫要嘛查不到資料、要嘛拿一個不存在的編號去打外部服務,浪費一次呼叫甚至可能拋出例外。所以 carts/create 必須明確標記成訪客身份,讓這幾個步驟知道要提早跳過。
一句話總結:免登跟訪客身份不是使用者狀態,而是後端替每一支 API 貼的存取政策標籤,同一個訪客呼叫不同 API 時可能落在不同分類,完全取決於當下打的是哪一支 API,也不會因為呼叫過幾次而互相升級。
- 整支 carts/create 的驗證分成身份驗證與業務資格檢查兩層,各自獨立
- 三種驗證方式依 Header 動態選擇,一般消費者走 Cookie 驗證
- 不管走哪一種驗證方式,最後都要通過同一道共用授權政策才算過關
- 身份驗證通過後,還要通過限流檢查與會員黑名單檢查才能真的建立購物車
- 免登與訪客身份是兩種不同的放行規格,不是呼叫端能控制的參數


