有的人會說

「我是為你好」
「你可以相信我」
「我一直都是站在你這邊的」

然而,真正重要的是,過去的行為一直在替你背書

就像系統不相信前端傳來的 Supplier ID,人生也不該只相信別人替自己貼的標籤

  • 「我是朋友」→ 那你在我低潮時有沒有出現?
  • 「我是專業的」→ 有沒有實績、作品、他人驗證?
  • 「我是家人」→ 那你有沒有尊重我的界線?

身分,是被驗證出來的,不是宣稱出來的

a

系統真正可信的登入身份只能來自後端的驗證結果,而不是前端傳來的任何值

使用者登入後,系統已在後端掌握其真實身份,後端(如 Session、Token、Context)必須知道「現在是誰在操作」

例如,Service 層透過 UserService 取得當前登入身份,在業務邏輯層,不直接相信 Controller 或前端傳入的 Supplier ID,而是呼叫 NineYi.Sms.BL.Services.Users.UserService 來取得登入者資訊,因此我們也需要避免前端傳入不必要的身份欄位(如 Supplier ID),如果這個值「理論上一定等於登入者身份」,那就不該讓前端傳,直接由後端補齊

若業務需求必須傳入,例如允許一個帳號可以管理多個商店,這些人可能是集團帳號 / 營運人員,他們可以透過人管理多間商店,此時 API 的設計,會注入 userService,在使用方法前,檢查 ShopId 的合法性,以避免 A 店看 B 店的狀況

userService

a

OIDC 的本質,是把「你是誰」這件事,標準化地交給專門的身份伺服器來證明,而應用系統只負責相信並使用這個結果

電商後台會走到「獨立身分伺服器」,是因為遲早會遇到商家要共用帳號登入多個後台、子帳號 / 員工帳號,行為像是停權某個人但保留商家、強制全平台改 MFA 規則,這些都不是「單一後台系統」該承擔的複雜度,如果不拆,會造成登入邏輯散落在各服務,並且權限規則寫死在 DB schema、商家組織身分異動造成大量改 code,或因為資安事件導致影響整個後台系統!

s

身分伺服器與應用伺服器互動的方式

s

  1. 使用者在應用系統點擊「登入」,應用系統本身「不驗證帳密」,它只做一件事:我要知道這個人是誰!

  2. 應用系統將使用者導向身份伺服器(IdP),這個導向請求是依照 OIDC 標準格式,同時告訴 IdP:「驗證完請回來找我」

  3. 使用者在身份伺服器完成登入驗證,帳密、MFA、公司 AD、Google 帳號,這一步「只發生在身份伺服器」,身份伺服器產生 ID Token 並回傳,ID Token 是一張「我已確認此人身分」的數位證書,內容包含使用者身分資訊(claims)

  4. 應用系統驗證 ID Token 的可信度,確認簽章、發行者、有效期限,確定這不是偽造的身分,應用系統建立自己的登入狀態,轉成 ClaimsIdentity

  5. 寫入 Cookie / Session,從此視為「已登入」

OAuth2 是什麼?

它是「授權(Authorization)」的協定,也就是讓別人可以幫我做事,但不需要給我密碼。像是在後台授權給身分驗證器,OIDC 在 OAuth2 上加一層「身分驗證(Authentication)」,讓你知道「是誰登入的」。例如加上一個 id_token,裡面是使用者的身分資訊,而 ID Token 是一個「JWT 格式的身分憑證」,證明「這個人是誰」,通常是由 Identity Provider(身份伺服器)簽章發出

s

✅ Step 1:使用者發起登入(透過 OpenID Connect)

  • 使用者從瀏覽器發起登入(登入按鈕 → 被導向到身份伺服器)
  • 使用 OIDC 標準發送授權請求(response_type = id_token)
  • 完成登入後,身份伺服器(如 IdentityServer)會
    • 回傳一個 ID Token
    • 瀏覽器導回 RedirectUri

a

✅ Step 2: Middleware 處理登入流程

1
2
app.UseCookieAuthentication(...);
app.UseOpenIdConnectAuthentication(...);
  • 使用者收到的 Token 被處理後 → 轉換成 ClaimsIdentity
  • SecurityTokenValidated 被觸發,這邊是自訂的邏輯
1
SecurityTokenValidated = n => this.OnSecurityTokenValidated(n, expiration),

a

OnSecurityTokenValidated() 會將從 OIDC 回來的 Token 解析出

  • 使用者名稱 (name)
  • 操作者 (operator_name)
  • 角色 (role)
  • session id (sid)
  • token (session_token) — 如果有開啟 Session 管理

呼叫內部的 UserService 查詢更多的使用者資訊,並建立一個新的 ClaimsIdentity

  • 加入使用者資料 (UserData)
  • 加入角色、操作人、sid 等資訊

接著使用 OWIN 提供的 AuthenticationTicket 將 Identity 寫入

1
notification.AuthenticationTicket = new AuthenticationTicket(nid, ...);

同時寫入 StateMachine,把 Claims + UserData + Token 存入 Redis / Cache(例如 ICacheProvider)

並且同時寫入 Cookie,app.UseCookieAuthentication() 設定了 Cookie,Cookie 中帶有 Ticket,未來每次請求都會驗證這個 Cookie

a

✅ Step 4:自訂驗證機制檢查登入狀態

有自訂一個檢查登入的 Middleware

1
app.Use<CustomAuthenticationMiddleware>();

後續請求於每次請求進來時

  • 從 Cookie 中解析出 sid
  • 從 Redis / Cache 查找是否有對應 session
  • 放到 HttpContext.Items[“IsLogin”] = true 作為後續登入狀態的依據

然後再加上一層檢查身份是否有效

1
2
3
4
5
6
7
8
OnValidateIdentity = async context =>
{
var isLogin = Convert.ToBoolean(HttpContext.Current.Items["IsLogin"]);
if (!isLogin)
{
context.RejectIdentity();
}
}

這就是所謂的「驗證使用者目前的 Session 是否還有效」,防止 Cookie 有效但 Redis 中 session 被清除的狀況

s

階段 驗證方式
登入當下 使用 OpenID Connect 身份伺服器驗證
回傳之後 驗證 Token,轉換成 ClaimsIdentity
寫入 Cookie 使用 CookieAuthenticationOptions 管理
後續請求 Cookie 自動帶上,透過自訂中介層讀取 sid 驗證 session 狀態
Redis 驗證 透過 ICacheProvider 查找 sid 是否仍在 cache 中

f

技術 說明
Cookie 儲存 AuthenticationTicket,讓使用者下次請求不用重新登入
Redis(Cache) 存 sid + UserData + Token → 控制是否還在登入狀態
ClaimsIdentity 實際代表登入使用者的資料物件,放在 HttpContext.User 裡面

s

s

f