member_center/docs/DESIGN.md

17 KiB
Raw Blame History

系統設計草案Member Center

日期2026-01-30

1. 需求摘要(已確認)

  1. 多個網站共用的會員登入中心SSO
  2. 電子報訂閱管理(發送另建)
  3. 未註冊會員可訂閱;註冊後沿用訂閱資料
  4. 未註冊會員可取消訂閱(單一清單退訂)
  5. 登入需支援 API 與 Redirect 兩種方式OAuth2 + OIDC

2. 架構原則

  • OAuth2 + OIDCAuthorization Code + PKCE
  • 會員中心只管理 Email 與訂閱狀態
  • 檔案存取授權沿用 OAuth2/JWT/JWKS並以 scope + claim 做資源邊界控制
  • 對外資料 API 以 service API 為主,授權完全由 scope 控制
  • Double Opt-in
  • 各站自行設計 UI主要走 API少數狀況使用 redirect
  • 多租戶為邏輯隔離,但會員資料跨站共享
  • 公開訂閱端點必須使用 list_id + email 做資料邊界,禁止僅以 email 查詢或操作
  • 訂閱狀態同步採 webhookevent payloadqueue 可作為未來擴充選項
  • 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_nameuser_profiles.last_name 必填
  • user_profiles.gender 使用列舉:male / female / other / unspecified
  • user_profiles.date_of_birth 使用 date
  • user_addresses.country_code 使用 ISO 3166-1 alpha-2
  • user_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, propertiestenant_id, usage=tenant_api|send_api|web_login|webhook_outbound|platform_service|file_api
  • 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

  1. 站點建立 OAuth clientusage=web_login,設定 redirect_uris
  2. 站點導向 /oauth/authorize,帶 client_id, redirect_uri, code_challenge, code_challenge_method=S256, response_type=code, scope=openid email profile
  3. 若使用者尚未登入,/oauth/authorize 會導向會員中心 Web login登入後回到原 authorize request
  4. 成功後導回 redirect_uri 並附 code
  5. 站點以 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 不同 originWeb login 僅允許導回 Auth:IssuerAuth:AllowedLoginReturnUrlPrefixes 內的 return URL。
  • API 可用 Auth:WebLoginUrl 指定登入頁位置;預設為 /account/login
  • web_login 可使用 public client + PKCE不要求 client secret。
  • web_login client 可使用 openid email profile,並預留 profile:basic.read

6.2 OAuth2 API 使用(站點自行 UI

  1. 站點以 API 驗證使用者登入(會員中心提供 login API
  2. 成功後取得 token含 ID Token 可選)
  3. 站點以 access_token 呼叫其他會員中心 API

6.3 未登入狀態的訂閱流程(在獨立平台)

  1. 使用者在各站點輸入 Email 並選擇訂閱清單
  2. 站點呼叫 POST /newsletter/subscribe
  3. 會員中心建立 pending 訂閱並發送驗證信(透過外部發信系統)
  4. 使用者點擊信件連結 /newsletter/confirm?token=...
  5. 訂閱狀態改為 active
  6. 會員中心發出事件 subscription.activated 到 event/queue

6.4 未登入退訂(單一清單)

  1. 信件提供「一鍵退訂」連結 /newsletter/unsubscribe?token=...
  2. 驗證 token 後將該訂閱標記為 unsubscribed
  3. 會員中心發出事件 subscription.unsubscribed 到 event/queue

6.4b Send Engine 發信前申請 One-Click Token

  1. Send Engine 依收件者呼叫 POST /newsletter/one-click-unsubscribe-token,或批次呼叫 POST /newsletter/one-click-unsubscribe-tokens
  2. body 帶 tenant_id + list_id + subscriber_id(批次版為 subscriber_ids[]
  3. 會員中心簽發 token與手動退訂 token 分離 purpose
  4. Send Engine 將 token 寫入 List-Unsubscribe 連結

6.6 Send Engine 事件同步Member Center → Send Engine

  1. Member Center 發出事件(subscription.activated / subscription.unsubscribed / preferences.updated
  2. 以 webhook 推送至 Send Engine簽章與重放防護
  3. X-Client-Id 使用 Send Engine auth_clients.id(可按 tenant 做設定覆蓋)
  4. Send Engine 驗證 tenant scope更新本地名單快照

6.7 Send Engine 退信/黑名單回寫(選用)

  1. Send Engine 依事件類型決定回寫時機與原因碼:
  2. hard_bounce / soft_bounce_threshold / suppressionMember Center 取消該 email 所有訂閱(跨租戶)並加入黑名單
  3. complaintMember Center 僅取消該筆訂閱,不加入黑名單
  4. 回寫請求需帶 tenant_id + subscriber_id + list_idMember Center 端做租戶邊界驗證
  5. 回寫授權可用:
    • tenant client scopenewsletter:events.write
    • platform client scopenewsletter:events.write.globalSES 聚合事件)

6.5 註冊後銜接

  1. 使用者完成註冊
  2. 系統搜尋 newsletter_subscriptions.email
  3. user_id 補上並保留偏好
  4. 可選:發出事件 subscription.linked_to_user

6.8 已登入修改密碼

  1. 使用者登入會員中心
  2. 進入 change password 頁面
  3. 提交 current_password + new_password
  4. 系統驗證目前密碼正確後更新 password hash
  5. 更新成功後刷新目前 session

6.8b 會員基本資料維護

  1. 使用者登入會員中心
  2. 進入 profile 頁
  3. 讀取基本資料姓、名、nick name、電話、生日、性別、公司、部門、職稱、統編、remark 等)
  4. 更新後寫入 user_profiles
  5. 記錄 audit log

6.8c 地址簿管理

  1. 使用者登入會員中心
  2. 進入地址簿頁面
  3. 新增、編輯或刪除地址
  4. 系統驗證地址資料歸屬目前 user
  5. 若設定預設地址,需確保同用途只有一筆預設值
  6. 若只剩最後一筆地址,不允許刪除
  7. 記錄 audit log

6.8d 已登入會員管理自己的訂閱

  1. 使用者登入會員中心
  2. 系統依 user_id 讀取已綁定的電子報訂閱
  3. 顯示可管理的訂閱清單
  4. 使用者可直接取消訂閱
  5. 系統驗證訂閱歸屬目前 user 後更新狀態
  6. 發送 subscription.unsubscribed 事件
  7. 不需再次經過 email token 驗證
  8. 已綁定 user_id 後仍保留 list_id + email 的 public 訂閱 / 退訂入口

6.9 檔案存取授權File Access

  1. Upload 採 S2S
    • A serviceclient_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
  2. Download 採 delegated short-lived token
    • client 向 A service 要求下載
    • A service 驗商業規則與檔案權限
    • A service 向新 File Access 專案取得短效 download token或由 File Access 專案提供簽發元件
    • client 帶短效 token 直接向 access agent / file space 請求檔案
    • access agent 驗 token 後放行

規則:

  • 不直接將一般 S2S access token 暴露給 client 作為下載 token
  • download token 應至少帶:
    • aud=file_access_api
    • scope=files:download.read
    • tenant_id
    • file_idobject_key
    • method=GET
    • 短效 exp
    • 建議帶 jti
  • upload token 應至少帶:
    • aud=file_access_api
    • scope=files:upload.write
    • tenant_id
  • access agent 應驗:
    • JWT signatureJWKS
    • iss
    • aud
    • exp
    • scope
    • tenant_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_name
  • audience
  • allowed_scopes
  • allowed_client_usages
  • requires_tenant
  • allows_delegated_token

建議初始資源:

  • member_center_api
    • scopesopenidemailprofilenewsletter:list.readnewsletter:events.writenewsletter:events.write.globalprofile:basic.readprofile:basic.writeprofile:addresses.readprofile:addresses.writeprofile:subscriptions.readprofile:subscriptions.write
    • usagestenant_apiplatform_service、互動式登入 client
  • send_engine_api
    • scopesnewsletter:send.writenewsletter:send.read
    • usagessend_api
  • file_access_api
    • scopesfiles:upload.writefiles:download.readfiles:download.delegatefiles:deletefiles:metadata.read
    • usagesfile_api

設計原則:

  • resource registry 由 DB 與管理 UI 管理非敏感欄位
  • TokenController 依 scope 與 usage 對照 resource registry 計算 resources / audiences
  • delegated token 與一般 S2S token 共用同一套 resource registry不重複維護 audience 規則
  • 對外資料讀寫授權完全由 scope 決定,只要 client 被授權該 scope 即可存取對應能力

目前實作狀態:

  • DB registry 第一版已加入 auth_resourcesauth_resource_scopesauth_client_usage_permissions
  • 預設 seed 已包含 member_center_apisend_engine_apifile_access_api
  • OAuth client usage-scope matrix 已由 DB 驅動,包含 file_api
  • resource registry 管理 UI 仍待補
  • delegated download token 發放流程不放在 Member Center會在新 File Access 專案實作

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/subscriptionsSend Engine 端點Member Center 呼叫)
  • POST /webhooks/lists/full-syncSend Engine 端點Member Center 呼叫)
  • POST /subscriptions/disableMember Center 端點Send Engine 呼叫)
  • POST /integrations/send-engine/webhook-clients/upsertMember 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_outbound OAuth Client 綁定 tenant_id,所有清單/事件 API 需驗證租戶邊界
  • OAuth Client 需區分用途:tenant_api / send_api / webhook_outbound / platform_service / file_api(禁止混用)
  • 新增 scopenewsletter:list.readnewsletter:send.writenewsletter:send.readnewsletter:events.read
  • 新增 scopenewsletter:events.write
  • 新增 scopenewsletter:events.write.global
  • 規劃新增 profile scopes
    • profile:basic.read
    • profile:basic.write
    • profile:addresses.read
    • profile:addresses.write
    • profile:subscriptions.read
    • profile:subscriptions.write
  • 規劃新增 file access scopes
    • files:upload.write
    • files:download.read
    • files:download.delegate
    • files:delete
    • files:metadata.read
  • 規劃新增 audiencefile_access_api
  • JWT Access Token 已改為 JWSDisableAccessTokenEncryption),供 Send Engine 以 JWKS 驗簽
  • aud 計算由 resource registry 驅動,不於 token 發放流程硬寫各服務 audience

租戶端取 TokenClient Credentials

  • 租戶使用 OAuth Client Credentials 向 Member Center 取得 access token
  • token 內含 tenant_id 與 scope
  • Send Engine 收到租戶請求後以 JWKS 驗簽 JWTJWS
  • 驗簽通過後將 tenant_id 固定在 request context不接受 body 覆寫
  • File Access 與未來外部服務亦沿用此模型File Access 的 delegated token 規則由新 File Access 專案處理
  • 若其他服務要讀取會員個資,應只授予必要的 profile read scopes不應沿用過寬的 profile OIDC scope
  • service API 為主要整合模式,存取控制以 scopes 為唯一授權來源

7.2 尚未完成(待辦)

  • POST /webhooks/lists/full-syncMember Center 端尚未發送此事件(僅保留契約)
  • 註冊後訂閱綁定(newsletter_subscriptions.user_id 補值)已在註冊 / external login 流程落地
  • subscription.linked_to_user 事件已發送
  • 安全設定頁access/refresh 時效)目前僅存值,尚未實際套用到 OpenIddict token lifetime
  • Audit Logs 目前以查詢為主,關鍵操作的寫入覆蓋率仍不足
  • resource registry 已完成 DB 驅動第一版file access delegated token issuing 不屬於 Member Center會在新專案實作

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.md
  • docs/USE_CASES.md
  • docs/FLOWS.md
  • docs/OPENAPI.md
  • docs/SCHEMA.sql
  • docs/TECH_STACK.md