16 KiB
16 KiB
系統設計草案(Member Center)
日期:2026-01-30
1. 需求摘要(已確認)
- 多個網站共用的會員登入中心(SSO)
- 電子報訂閱管理(發送另建)
- 未註冊會員可訂閱;註冊後沿用訂閱資料
- 未註冊會員可取消訂閱(單一清單退訂)
- 登入需支援 API 與 Redirect 兩種方式(OAuth2 + OIDC)
2. 架構原則
- OAuth2 + OIDC(Authorization Code + PKCE)
- 會員中心只管理 Email 與訂閱狀態
- 檔案存取授權沿用 OAuth2/JWT/JWKS,並以 scope + claim 做資源邊界控制
- 對外資料 API 以 service API 為主,授權完全由 scope 控制
- Double Opt-in
- 各站自行設計 UI,主要走 API;少數狀況使用 redirect
- 多租戶為邏輯隔離,但會員資料跨站共享
- 公開訂閱端點必須使用
list_id + email做資料邊界,禁止僅以email查詢或操作 - 訂閱狀態同步採 webhook(event payload);queue 可作為未來擴充選項
- PostgreSQL
- 實作:C# .NET Core + MVC + OpenIddict
3. 使用者故事(精簡)
- 訪客:在任一站點未登入狀態下輸入 Email 訂閱電子報
- 訪客:在信件中點擊連結完成 double opt-in
- 訪客:點擊取消訂閱連結即可退訂(單一清單)
- 會員:在任一站點登入後,其他站點可無痛登入(SSO)
- 站點後台:可管理站點資訊、訂閱清單、會員基本資料
4. 核心模組
- Identity Service:註冊、登入、修改密碼、密碼重設、Email 驗證
- OAuth2/OIDC Service:授權流程、token 發放、ID Token
- Auth Resource Registry:管理外部服務 resource / audience / scopes / client usage 對應
- Profile Service:會員基本資料、聯絡方式、公司資料、地址簿
- Subscription Service:訂閱/退訂/偏好管理
- Admin Console:租戶與清單管理
- Mailer Integration:驗證信/退訂信/確認信的發送介面(外部系統)
- Event Publisher:訂閱事件發佈(供發信系統或數據系統消費)
5. 資料模型(概念)
- tenants
- id, name, domains, status, created_at
- users (ASP.NET Core Identity)
- id, user_name, email, password_hash, email_confirmed, lockout, is_blacklisted, blacklisted_at, blacklisted_by, created_at, last_login_at, last_seen_at, disabled_at, disabled_by
- user_profiles
- user_id, last_name, first_name, nick_name, mobile_phone, landline_phone, date_of_birth, gender, company_name, department, job_title, company_phone, tax_id, invoice_title, remark, updated_at
- user_addresses
- id, user_id, label, recipient_name, recipient_phone, country_code, postal_code, state_region, city, district, address_line1, address_line2, company_name, usage, is_default, address_meta_json, created_at, updated_at
資料約束建議:
user_profiles.first_name、user_profiles.last_name必填user_profiles.gender使用列舉:male/female/other/unspecifieduser_profiles.date_of_birth使用dateuser_addresses.country_code使用 ISO 3166-1 alpha-2user_addresses.address_line1必填user_addresses.usage使用列舉:shipping/billing/both- 同一個
user_id + usage只允許一筆預設地址 - 地址至少保留一筆,禁止刪除最後一筆地址
address_meta_json作為國家特有欄位補充,不取代主結構欄位- roles / user_roles (Identity)
- id, name, created_at
- OpenIddictApplications
- id, client_id, client_secret, display_name, permissions, redirect_uris, properties(含
tenant_id,usage=tenant_api|send_api|webhook_outbound|platform_service)
- id, client_id, client_secret, display_name, permissions, redirect_uris, properties(含
- OpenIddictAuthorizations
- id, application_id, status, subject, type, scopes
- OpenIddictTokens
- id, application_id, authorization_id, subject, type, status, expiration_date
- OpenIddictScopes
- id, name, display_name, resources
- newsletter_lists
- id, tenant_id, name, status, created_at
- newsletter_subscriptions
- id, list_id, email, user_id (nullable), status, preferences, created_at
- email_blacklist
- id, email, reason, blacklisted_at, blacklisted_by
- email_verifications
- id, email, tenant_id, token_hash, purpose, expires_at, consumed_at
- unsubscribe_tokens
- id, subscription_id, token_hash, expires_at, consumed_at
- audit_logs
- id, actor_type, actor_id, action, payload, created_at
- system_flags
- id, key, value, updated_at
關聯說明:
- newsletter_subscriptions.email 與 users.email 維持唯一性關聯
- 使用者註冊時,如 email 存在訂閱紀錄,補上 user_id
- 單一清單退訂:unsubscribe token 綁定 subscription_id
- blacklist 記錄於 email_blacklist(全租戶共用)
- email 為會員與訂閱領域的對外主 key,不提供改 email 流程
6. 核心流程
6.1 OAuth2/OIDC Redirect 登入(Authorization Code + PKCE)
狀態:已支援 usage=web_login。
- 站點建立 OAuth client,
usage=web_login,設定redirect_uris - 站點導向
/oauth/authorize,帶client_id,redirect_uri,code_challenge,code_challenge_method=S256,response_type=code,scope=openid email profile - 若使用者尚未登入,
/oauth/authorize會導向會員中心 Web login,登入後回到原 authorize request - 成功後導回
redirect_uri並附code - 站點以
code+code_verifier向/oauth/token換取 token
實作註記:
- API 與 Web 需共用 DataProtection application name
MemberCenter,使 API authorize endpoint 可讀取 Web login cookie。 - 若 API 與 Web 位於不同子網域,需設定
Auth:CookieDomain,例如.example.com。 - 若 API 與 Web 不同 origin,Web login 僅允許導回
Auth:Issuer或Auth:AllowedLoginReturnUrlPrefixes內的 return URL。 - API 可用
Auth:WebLoginUrl指定登入頁位置;預設為/account/login。 web_login可使用 public client + PKCE,不要求 client secret。web_loginclient 可使用openid email profile,並預留profile:basic.read。
6.2 OAuth2 API 使用(站點自行 UI)
- 站點以 API 驗證使用者登入(會員中心提供 login API)
- 成功後取得 token(含 ID Token 可選)
- 站點以 access_token 呼叫其他會員中心 API
6.3 未登入狀態的訂閱流程(在獨立平台)
- 使用者在各站點輸入 Email 並選擇訂閱清單
- 站點呼叫
POST /newsletter/subscribe - 會員中心建立
pending訂閱並發送驗證信(透過外部發信系統) - 使用者點擊信件連結
/newsletter/confirm?token=... - 訂閱狀態改為
active - 會員中心發出事件
subscription.activated到 event/queue
6.4 未登入退訂(單一清單)
- 信件提供「一鍵退訂」連結
/newsletter/unsubscribe?token=... - 驗證 token 後將該訂閱標記為
unsubscribed - 會員中心發出事件
subscription.unsubscribed到 event/queue
6.4b Send Engine 發信前申請 One-Click Token
- Send Engine 依收件者呼叫
POST /newsletter/one-click-unsubscribe-token,或批次呼叫POST /newsletter/one-click-unsubscribe-tokens - body 帶
tenant_id + list_id + subscriber_id(批次版為subscriber_ids[]) - 會員中心簽發 token(與手動退訂 token 分離 purpose)
- Send Engine 將 token 寫入
List-Unsubscribe連結
6.6 Send Engine 事件同步(Member Center → Send Engine)
- Member Center 發出事件(
subscription.activated/subscription.unsubscribed/preferences.updated) - 以 webhook 推送至 Send Engine(簽章與重放防護)
X-Client-Id使用 Send Engineauth_clients.id(可按 tenant 做設定覆蓋)- Send Engine 驗證 tenant scope,更新本地名單快照
6.7 Send Engine 退信/黑名單回寫(選用)
- Send Engine 依事件類型決定回寫時機與原因碼:
hard_bounce/soft_bounce_threshold/suppression:Member Center 取消該 email 所有訂閱(跨租戶)並加入黑名單complaint:Member Center 僅取消該筆訂閱,不加入黑名單- 回寫請求需帶
tenant_id + subscriber_id + list_id,Member Center 端做租戶邊界驗證 - 回寫授權可用:
- tenant client scope:
newsletter:events.write - platform client scope:
newsletter:events.write.global(SES 聚合事件)
- tenant client scope:
6.5 註冊後銜接
- 使用者完成註冊
- 系統搜尋
newsletter_subscriptions.email - 將
user_id補上並保留偏好 - 可選:發出事件
subscription.linked_to_user
6.8 已登入修改密碼
- 使用者登入會員中心
- 進入 change password 頁面
- 提交
current_password + new_password - 系統驗證目前密碼正確後更新 password hash
- 更新成功後刷新目前 session
6.8b 會員基本資料維護
- 使用者登入會員中心
- 進入 profile 頁
- 讀取基本資料(姓、名、nick name、電話、生日、性別、公司、部門、職稱、統編、remark 等)
- 更新後寫入
user_profiles - 記錄 audit log
6.8c 地址簿管理
- 使用者登入會員中心
- 進入地址簿頁面
- 新增、編輯或刪除地址
- 系統驗證地址資料歸屬目前 user
- 若設定預設地址,需確保同用途只有一筆預設值
- 若只剩最後一筆地址,不允許刪除
- 記錄 audit log
6.8d 已登入會員管理自己的訂閱
- 使用者登入會員中心
- 系統依
user_id讀取已綁定的電子報訂閱 - 顯示可管理的訂閱清單
- 使用者可直接取消訂閱
- 系統驗證訂閱歸屬目前 user 後更新狀態
- 發送
subscription.unsubscribed事件 - 不需再次經過 email token 驗證
- 已綁定
user_id後仍保留list_id + email的 public 訂閱 / 退訂入口
6.9 檔案存取授權(File Access)
- Upload 採 S2S:
A service以client_credentials向 Member Center 取 token- token 帶 file upload 專用 scope
A service帶 token 呼叫 access agent / file space- access agent 以 JWKS 驗簽 JWT,並驗
iss/aud/exp/scope/tenant_id
- Download 採 delegated short-lived token:
- client 向
A service要求下載 A service驗商業規則與檔案權限A service取得或簽發短效 download token- client 帶短效 token 直接向 access agent / file space 請求檔案
- access agent 驗 token 後放行
- client 向
規則:
- 不直接將一般 S2S access token 暴露給 client 作為下載 token
- download token 應至少帶:
aud=file_access_apiscope=files:download.readtenant_idfile_id或object_keymethod=GET- 短效
exp - 建議帶
jti
- upload token 應至少帶:
aud=file_access_apiscope=files:upload.writetenant_id
- access agent 應驗:
- JWT signature(JWKS)
issaudexpscopetenant_id- 檔案識別與 method 是否與 token 一致
6.10 外部資源授權抽象(Audience / Resource Registry)
目的:
- 統一 Send Engine、File Access 與未來其他外部服務的 token 發放規則
- 避免每新增一個服務就新增一組
Auth__XAudience與對應程式分支
共通原則:
- Member Center 為 token issuer 與 JWKS 提供者
- 外部服務皆以
iss/aud/exp/scope/tenant_id驗證 token aud不直接以程式硬編碼,而是由 resource registry 決定
resource registry 至少需定義:
resource_nameaudienceallowed_scopesallowed_client_usagesrequires_tenantallows_delegated_token
建議初始資源:
member_center_api- scopes:
openid、email、profile、newsletter:list.read、newsletter:events.write、newsletter:events.write.global、profile:basic.read、profile:basic.write、profile:addresses.read、profile:addresses.write、profile:subscriptions.read、profile:subscriptions.write - usages:
tenant_api、platform_service、互動式登入 client
- scopes:
send_engine_api- scopes:
newsletter:send.write、newsletter:send.read - usages:
send_api
- scopes:
file_access_api- scopes:
files:upload.write、files:download.read、files:download.delegate、files:delete、files:metadata.read - usages:
file_api
- scopes:
設計原則:
- resource registry 由 DB 與管理 UI 管理非敏感欄位
TokenController依 scope 與 usage 對照 resource registry 計算 resources / audiences- delegated token 與一般 S2S token 共用同一套 resource registry,不重複維護 audience 規則
- 對外資料讀寫授權完全由 scope 決定,只要 client 被授權該 scope 即可存取對應能力
7. API 介面(草案)
- GET
/oauth/authorize - POST
/oauth/token - GET
/.well-known/openid-configuration - POST
/auth/login(API-only login) - POST
/auth/refresh - POST
/newsletter/subscribe - GET
/newsletter/confirm - POST
/newsletter/unsubscribe - POST
/newsletter/unsubscribe-token - POST
/newsletter/one-click-unsubscribe-token - POST
/newsletter/one-click-unsubscribe-tokens - GET
/newsletter/preferences - POST
/newsletter/preferences - POST
/webhooks/subscriptions(Send Engine 端點,Member Center 呼叫) - POST
/webhooks/lists/full-sync(Send Engine 端點,Member Center 呼叫) - POST
/subscriptions/disable(Member Center 端點,Send Engine 呼叫) - POST
/integrations/send-engine/webhook-clients/upsert(Member Center 端點,Send Engine 呼叫)
7.1 進度狀態(2026-02)
API
GET /newsletter/subscriptions?list_id=...:已實作POST /webhooks/subscriptions:已實作(Member Center 發送)POST /subscriptions/disable:已實作POST /integrations/send-engine/webhook-clients/upsert:已實作POST /webhooks/lists/full-sync:尚未實作(保留規格)
Auth / Scope
tenant_api/send_api/webhook_outboundOAuth Client 綁定tenant_id,所有清單/事件 API 需驗證租戶邊界- OAuth Client 需區分用途:
tenant_api/send_api/webhook_outbound/platform_service/file_api(禁止混用) - 新增 scope:
newsletter:list.read、newsletter:send.write、newsletter:send.read、newsletter:events.read - 新增 scope:
newsletter:events.write - 新增 scope:
newsletter:events.write.global - 規劃新增 profile scopes:
profile:basic.readprofile:basic.writeprofile:addresses.readprofile:addresses.writeprofile:subscriptions.readprofile:subscriptions.write
- 規劃新增 file access scopes:
files:upload.writefiles:download.readfiles:download.delegatefiles:deletefiles:metadata.read
- 規劃新增 audience:
file_access_api - JWT Access Token 已改為 JWS(
DisableAccessTokenEncryption),供 Send Engine 以 JWKS 驗簽 aud計算由 resource registry 驅動,不於 token 發放流程硬寫各服務 audience
租戶端取 Token(Client Credentials)
- 租戶使用 OAuth Client Credentials 向 Member Center 取得 access token
- token 內含
tenant_id與 scope - Send Engine 收到租戶請求後以 JWKS 驗簽 JWT(JWS)
- 驗簽通過後將
tenant_id固定在 request context,不接受 body 覆寫 - File Access 與未來外部服務亦沿用此模型,差異由 scopes / audience / delegated token 規則表達
- 若其他服務要讀取會員個資,應只授予必要的 profile read scopes,不應沿用過寬的
profileOIDC scope - service API 為主要整合模式,存取控制以 scopes 為唯一授權來源
7.2 尚未完成(待辦)
POST /webhooks/lists/full-sync:Member Center 端尚未發送此事件(僅保留契約)- 註冊後訂閱綁定(
newsletter_subscriptions.user_id補值)已在註冊 / external login 流程落地 subscription.linked_to_user事件已發送- 安全設定頁(access/refresh 時效)目前僅存值,尚未實際套用到 OpenIddict token lifetime
- Audit Logs 目前以查詢為主,關鍵操作的寫入覆蓋率仍不足
- resource registry 與 file access token issuing 尚未實作,現況仍是兩組 audience 硬編碼
8. 安全與合規
- 密碼強度與防暴力破解(rate limit + lockout)
- Token rotation + refresh token revoke
- Redirect URI 白名單 + PKCE
- Double opt-in(可配置)
- Audit log
- delegated download token 需短效、不可重放,必要時可引入
jti與 nonce/jti blacklist - Email 可作為未來 MFA 的挑戰通道
- GDPR/CCPA:資料匯出與刪除(規劃中)
9. 其他文件
docs/UI.mddocs/USE_CASES.mddocs/FLOWS.mddocs/OPENAPI.mddocs/SCHEMA.sqldocs/TECH_STACK.md