情境說明
建立購物車這支 API,其實同時服務三種不同的呼叫者
同一支負責建立購物車的 carts/create,可能是消費者用瀏覽器登入後直接呼叫,也可能是合作夥伴系統代替消費者下單,還可能是門市店員在幫客人代客下單。這三種來源的身份驗證方式完全不一樣,這篇要拆解 Shopping 服務怎麼判斷每一次請求究竟屬於哪一種身份,驗證通過之後又還會做哪些「這個人可不可以做這件事」的檢查。
carts/create 身份驗證 業務資格檢查

先說結論,整支 API 的驗證其實分成兩個完全不同層級,發生的位置與檢查的問題都不一樣,混在一起看容易誤解成同一套機制。

身份驗證
Authentication
授權判斷
Authorization
業務資格檢查
CartService
1
第一層:身份驗證與授權
發生在 CartsController.Create 方法執行之前,由 ASP.NET Core 的驗證管線處理,確認這次請求是誰打的,回答的是身份問題。
2
第二層:業務層資格檢查
發生在 CartService.CartCreateAsync 方法內部,是手動寫在程式碼裡的呼叫,確認這個已登入的會員這次操作允不允許,回答的是資格問題,不是身份問題。
💡

白話理解:一個會員完全可能通過第一層身份驗證,他確實是這個帳號本人,卻在第二層被業務規則擋下來,例如這個會員在商店黑名單裡,或短時間內呼叫太多次觸發限流。這兩層各自獨立,任何一層擋下都會讓整支 API 失敗,但失敗的原因與回應方式不一樣。

Shopping 同時支援三種不同的驗證方式,因為 carts/create 這支 API 會被三種不同來源呼叫。系統用一段動態判斷邏輯決定這次要用哪一種驗證方式,判斷順序如下。

有 N1-INTERNAL-MEMBER-ID
PartnerApi 驗證
有 NY-SESSION-TOKEN
N1SessionToken 驗證
以上都沒有
NineYiCookies 驗證
驗證方式使用時機判斷依據
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 驗證的程式裡,每次請求都會經過這段。

1
白名單先擋一次
先查一份白名單設定,命中的路徑直接放行,完全不做後面的驗證。
2
取出加密的登入憑證
從 Cookie(或對應的 Header)取出 authuauth 加密字串。
3
依情境判斷登入狀態
依有沒有 auth 走不同分支,判斷是已登入、免登,還是允許以訪客身份繼續。

核心分支邏輯依 auth 存不存在分成兩條路徑,整理成表格如下。

情境判斷依據結果
auth Cookie直接解密 Cookie 內容解密成功則拿到會員資料,解密失敗視為登入失敗
沒有 auth,但有 uauthuauth 解出匿名裝置編號,去 Redis 查登入快取Redis 裡有登入資料且未過期,視為登入成功並要求 Cookie 續期
Redis 查無登入資料,且允許免登這次請求的路徑命中設定檔的免登白名單建立一個只有裝置編號的訪客身份,不視為登入失敗
Redis 查無登入資料,允許以訪客身份繼續路徑命中另一份訪客白名單,且對應商店有開啟訪客購物車功能建立明確標記為訪客的身份,購物車後續流程會跳過部分只對真實會員有意義的檢查
以上皆不成立沒有任何登入憑證,也不允許免登判定為未登入,驗證流程失敗

另外兩種驗證方式都不走 Cookie,而是各自實作一套驗證流程,格式類似,都是驗證通過後手動把會員資料寫進系統可以識別的憑證裡。

1
PartnerApi 驗證
先驗證 Header 裡的服務金鑰是否存在於設定檔的合法金鑰清單,再檢查 Header 是否帶有會員編號與商店編號,兩者皆通過後才查出對應的會員資料,查不到則驗證失敗。
2
N1SessionToken 驗證
從 Header 解析 JWT 取得會員編號,同時需要商店編號才能定位商店,兩者都解析成功後同樣查出會員資料,用於門市店員的代客下單場景。
⚠️

共通點:這兩種驗證失敗時的處理方式一致,都會回傳固定格式的錯誤訊息,帶著相同的未登入錯誤碼,讓前端可以依錯誤碼統一處理未登入導轉。

不管走哪一種驗證方式,最後都要通過同一道共用的授權政策才算真的過關。

builder.Services.AddAuthorization(options =>
{
    options.DefaultPolicy = new AuthorizationPolicyBuilder(AuthenticationSchemeEnum.MultiAuthSchemes.ToString())
                            .RequireClaim(ClaimTypesEnum.LoginMemberEntity.ToString())
                            .Build();
});

這段的意思很直白,就算前面的驗證流程本身沒有失敗,只要最後沒有成功把代表登入狀態的 Claim 寫進憑證裡,一樣會判定授權失敗,回傳未登入的結果。這代表三種驗證方式雖然實作邏輯完全不同,但對外呈現的授權合格標準是同一個,都是有沒有這個 Claim。

ℹ️

但取得會員資料的方式並不一樣:三種驗證方式雖然最後都要滿足同一道 Claim 檢查,但在管線內「怎麼拿到會員資料」其實是分岔的,不是每一種都會即時查詢會員服務。

Cookie
一般會員驗證 — 不查會員服務
直接解密 Cookie 本身內含的資料組出會員資料,或是拿解出的識別碼去 Redis 查登入快取,全程不會在管線內另外呼叫會員資料查詢服務。
JWT / 金鑰
PartnerApi、N1SessionToken — 會查會員服務
這兩種都只是先驗證身份資訊本身合法(金鑰存在、JWT 可解析出會員編號),拿到的只是編號,還必須在管線內即時呼叫會員資料查詢服務,用商店編號+會員編號查出真正的會員資料;查不到就直接判定驗證失敗。

換句話說,Cookie 驗證是「資料自帶」(解密或查快取即可),PartnerApi/N1SessionToken 驗證是「先驗證身份合法性,再即時查詢會員資料」,兩種取得會員資料的成本與時機並不相同,但最終都要落地成同一個 Claim 才能通過共用授權政策。

身份驗證通過只代表這個人是誰確認無誤,還不代表這次操作一定會被放行。建立購物車的業務邏輯內部還有兩道手動寫在程式碼裡的資格檢查,執行順序如下。

限流檢查
會員黑名單檢查
通過才繼續建立購物車
1
限流檢查
判斷這個會員短時間內呼叫次數是否超過上限,超過則直接失敗,這部分的完整限流器設計,在先前的限流器分析文章裡已完整拆解。
2
會員黑名單檢查
判斷這個會員能不能繼續使用購物車,黑名單檢查本身也有分支,不是單純的通過或擋下兩種結果。
情境判斷依據處理方式
訪客身份被標記為訪客的請求直接跳過黑名單檢查,因為根本沒有會員身份可以查
全站黑名單黑名單清單中存在整個商城等級的紀錄清空這個會員的購物車,整支 API 直接失敗
商店黑名單,且允許付款方式清單為空黑名單存在對應商店的紀錄,且查不到任何允許使用的付款方式同樣清空購物車並失敗
商店黑名單,但還有允許的付款方式黑名單存在對應商店的紀錄,但至少有一種付款方式允許使用放行,後續結帳流程只能挑這些付款方式
完全不在任何黑名單黑名單查詢結果為空清單正常放行,不做任何額外處理

前面第一層提到,Cookie 驗證流程裡有兩種不需要登入也能放行的情境,一種單純叫免登,一種是明確標記為訪客身份,兩者容易被誤會成同一件事,這裡補充說明差異。

🛑

常見誤解:以為只要呼叫端傳入某個參數就能免登,實際上這兩種放行規格都不是呼叫端能傳的參數,而是伺服器依這次請求打的 API 路徑,去比對設定檔白名單自己算出來的,前端完全沒有管道能設定或偽造。

1
免登
代表這支 API 連匿名裝置都能直接呼叫,語意上仍然視同一般會員規則處理,例如加購物車這種單純的操作。
2
訪客身份(P1 免登)
代表這支 API 允許用一個明確標記為訪客的身份繼續往下走,carts/create 就屬於這一類,需要對應商店開啟訪客購物車功能。

兩者產生的會員資料骨架其實一樣,都只有裝置編號,沒有真正的會員編號,差別在多一個訪客標記旗標。carts/create 這條流程裡,建立購物車時要查詢的會員優惠券、會員點數、會員黑名單這三個步驟,都需要真實存在的會員編號才查得到有意義的結果,如果沒有這個訪客標記讓程式碼提早跳過,這幾支呼叫要嘛查不到資料、要嘛拿一個不存在的編號去打外部服務,浪費一次呼叫甚至可能拋出例外。所以 carts/create 必須明確標記成訪客身份,讓這幾個步驟知道要提早跳過。

一句話總結:免登跟訪客身份不是使用者狀態,而是後端替每一支 API 貼的存取政策標籤,同一個訪客呼叫不同 API 時可能落在不同分類,完全取決於當下打的是哪一支 API,也不會因為呼叫過幾次而互相升級。

  • 整支 carts/create 的驗證分成身份驗證與業務資格檢查兩層,各自獨立
  • 三種驗證方式依 Header 動態選擇,一般消費者走 Cookie 驗證
  • 不管走哪一種驗證方式,最後都要通過同一道共用授權政策才算過關
  • 身份驗證通過後,還要通過限流檢查與會員黑名單檢查才能真的建立購物車
  • 免登與訪客身份是兩種不同的放行規格,不是呼叫端能控制的參數