<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Provisioning on Tarragon</title><link>https://tarrragon.github.io/blog/tags/provisioning/</link><description>Recent content in Provisioning on Tarragon</description><generator>Hugo -- gohugo.io</generator><language>zh-TW</language><copyright>Tarragon (CC BY 4.0)</copyright><lastBuildDate>Mon, 18 May 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://tarrragon.github.io/blog/tags/provisioning/index.xml" rel="self" type="application/rss+xml"/><item><title>API 認證的三層信任邊界：使用者、系統、跨系統 Provisioning</title><link>https://tarrragon.github.io/blog/work-log/api-%E8%AA%8D%E8%AD%89%E7%9A%84%E4%B8%89%E5%B1%A4%E4%BF%A1%E4%BB%BB%E9%82%8A%E7%95%8C%E4%BD%BF%E7%94%A8%E8%80%85%E7%B3%BB%E7%B5%B1%E8%B7%A8%E7%B3%BB%E7%B5%B1-provisioning/</link><pubDate>Mon, 18 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/work-log/api-%E8%AA%8D%E8%AD%89%E7%9A%84%E4%B8%89%E5%B1%A4%E4%BF%A1%E4%BB%BB%E9%82%8A%E7%95%8C%E4%BD%BF%E7%94%A8%E8%80%85%E7%B3%BB%E7%B5%B1%E8%B7%A8%E7%B3%BB%E7%B5%B1-provisioning/</guid><description>&lt;h2 id="api-認證為什麼要分層">API 認證為什麼要分層&lt;/h2>
&lt;p>&lt;strong>API 認證的核心是「身分維度的分離」&lt;/strong> — 一個 request 同時牽涉「人」「呼叫的系統」「另一個系統有沒有對應身分」三個獨立問題，每個問題的 secret 機制不同、洩漏後果不同、撤銷方式不同。混用一個機制回答全部問題，等於用同一把鑰匙開家、車、保險箱。&lt;/p>
&lt;p>看似一個 API request，其實同時要回答：&lt;/p>
&lt;ul>
&lt;li>發起這個 request 的「&lt;strong>人&lt;/strong>」是誰？（identity）&lt;/li>
&lt;li>把這個 request 傳過來的「&lt;strong>系統&lt;/strong>」是誰？（caller）&lt;/li>
&lt;li>這個人在「&lt;strong>另一個系統&lt;/strong>」有沒有對應身分？（cross-system mapping）&lt;/li>
&lt;/ul>
&lt;p>每個問題都需要不同的 secret 機制來回答。設計時先拆身分維度，再選 token、shared secret、mTLS 或 provisioning workflow，才有辦法讓洩漏範圍、撤銷粒度與排障路由各自清楚。&lt;/p>
&lt;p>這篇整理兩層信任邊界（Layer 1 使用者、Layer 2 系統）跟一個跨系統 workflow（Layer 3 Provisioning），以及它們各自對應的 secret 機制。&lt;strong>每層的實作細節都另有獨立文章深入&lt;/strong>、本文聚焦「為什麼要分」「各層解什麼問題」的心智模型。&lt;/p>
&lt;blockquote>
&lt;p>&lt;strong>前提假設&lt;/strong>：以下所有機制都假設 transport 走 HTTPS / TLS。Token 與 secret 需要在加密通道內傳輸，否則中間人可直接取得 credential。HTTPS 是所有層共同依賴的 transport 前提。&lt;/p>
&lt;p>&lt;strong>本文 token 範圍&lt;/strong>：本文討論「opaque token」（隨機字串、server 端 lookup），不涵蓋 JWT（self-contained token、簽章驗證）。兩者安全模型不同，比較見 Layer 1 段落。&lt;/p>&lt;/blockquote>
&lt;hr>
&lt;h2 id="layer-1使用者層bearer-token">Layer 1：使用者層（Bearer Token）&lt;/h2>
&lt;p>&lt;strong>使用者層負責把 request 綁到已登入的人類或帳號主體&lt;/strong>。它回答的問題是：「這個 request 是哪個使用者發的？」&lt;/p>
&lt;p>&lt;strong>Bearer Token 是 capability credential（持有即授權）、不是 identity credential（身分證明）&lt;/strong>。差別在於：身分證遺失可以掛失補辦、別人撿到也無法直接領錢；Bearer Token 一旦被取得、攻擊者就能即時用該使用者身分發 request、沒有第二道關卡。這個本質決定了 token 的儲存、傳輸、撤銷機制都必須以「持有即危險」為前提設計。&lt;/p>
&lt;p>「Bearer Token」是 RFC 6750 定義的 HTTP authentication scheme（&lt;code>Authorization: Bearer &amp;lt;token&amp;gt;&lt;/code>）、屬於通用概念 — GitHub PAT、Stripe API Key、OAuth access token、Laravel Sanctum 的 PAT、JWT 都是 Bearer Token 的不同實作。&lt;/p>
&lt;h3 id="opaque-token-vs-jwt兩種根本不同的設計">Opaque Token vs JWT：兩種根本不同的設計&lt;/h3>
&lt;p>「Bearer Token」是上位概念、實作上有兩條主線、安全模型完全不同：&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>項目&lt;/th>
 &lt;th>Opaque Token（如 Sanctum）&lt;/th>
 &lt;th>JWT&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>Token 本身&lt;/td>
 &lt;td>隨機字串、無內含資訊&lt;/td>
 &lt;td>簽章 payload、內嵌使用者 claim&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>驗證方式&lt;/td>
 &lt;td>server 查 DB lookup&lt;/td>
 &lt;td>驗簽章、不需 DB&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>載入使用者&lt;/td>
 &lt;td>從 DB row 撈&lt;/td>
 &lt;td>直接讀 claim&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>撤銷&lt;/td>
 &lt;td>刪 DB row、立即生效&lt;/td>
 &lt;td>困難、需 blacklist 或短 TTL&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>洩漏暴露範圍&lt;/td>
 &lt;td>該 row 立即停用&lt;/td>
 &lt;td>直到 expire 都有效&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>跨服務驗證&lt;/td>
 &lt;td>需要共用 DB 或驗證 endpoint&lt;/td>
 &lt;td>共享公鑰即可、stateless&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>兩者各有適合情境：opaque token 撤銷快、適合「使用者主動登出 / 帳號被盜要立即停權」；JWT 不需 DB lookup、適合「跨多個 microservice、想避免每次都查中央 DB」。下面 Layer 1 的內容&lt;strong>只聚焦 opaque token&lt;/strong> — JWT 的設計細節（簽章演算法選擇、&lt;code>alg: none&lt;/code> 攻擊、key rotation）是獨立議題、不在本篇範圍。&lt;/p>
&lt;h3 id="opaque-token-的格式設計">Opaque Token 的格式設計&lt;/h3>
&lt;p>Opaque token 是隨機字串、但實際 format 在不同產品有兩條主流分流：&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>設計&lt;/th>
 &lt;th>範例&lt;/th>
 &lt;th>解的問題&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>&lt;strong>&lt;code>{PK}|{secret}&lt;/code>&lt;/strong>&lt;/td>
 &lt;td>&lt;code>1|abc123def456...&lt;/code>（Laravel Sanctum）&lt;/td>
 &lt;td>用 PK 收斂 DB 搜尋、把 timing 安全留給應用層&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;strong>&lt;code>{prefix}_{secret}&lt;/code>&lt;/strong>&lt;/td>
 &lt;td>&lt;code>ghp_xxx&lt;/code>（GitHub）、&lt;code>sk_live_xxx&lt;/code>（Stripe）&lt;/td>
 &lt;td>用語意 prefix 支援自動洩漏掃描跟 token type 辨識&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>兩種設計&lt;strong>沒有絕對優劣&lt;/strong>、取決於 token 的傳播範圍：純內部使用、Sanctum 設計簡潔且足夠；對外開放、容易散落公開 repo、prefix 設計能讓 GitHub Secret Scanning / Stripe webhook 等工具自動偵測洩漏。&lt;/p></description><content:encoded><![CDATA[<h2 id="api-認證為什麼要分層">API 認證為什麼要分層</h2>
<p><strong>API 認證的核心是「身分維度的分離」</strong> — 一個 request 同時牽涉「人」「呼叫的系統」「另一個系統有沒有對應身分」三個獨立問題，每個問題的 secret 機制不同、洩漏後果不同、撤銷方式不同。混用一個機制回答全部問題，等於用同一把鑰匙開家、車、保險箱。</p>
<p>看似一個 API request，其實同時要回答：</p>
<ul>
<li>發起這個 request 的「<strong>人</strong>」是誰？（identity）</li>
<li>把這個 request 傳過來的「<strong>系統</strong>」是誰？（caller）</li>
<li>這個人在「<strong>另一個系統</strong>」有沒有對應身分？（cross-system mapping）</li>
</ul>
<p>每個問題都需要不同的 secret 機制來回答。設計時先拆身分維度，再選 token、shared secret、mTLS 或 provisioning workflow，才有辦法讓洩漏範圍、撤銷粒度與排障路由各自清楚。</p>
<p>這篇整理兩層信任邊界（Layer 1 使用者、Layer 2 系統）跟一個跨系統 workflow（Layer 3 Provisioning），以及它們各自對應的 secret 機制。<strong>每層的實作細節都另有獨立文章深入</strong>、本文聚焦「為什麼要分」「各層解什麼問題」的心智模型。</p>
<blockquote>
<p><strong>前提假設</strong>：以下所有機制都假設 transport 走 HTTPS / TLS。Token 與 secret 需要在加密通道內傳輸，否則中間人可直接取得 credential。HTTPS 是所有層共同依賴的 transport 前提。</p>
<p><strong>本文 token 範圍</strong>：本文討論「opaque token」（隨機字串、server 端 lookup），不涵蓋 JWT（self-contained token、簽章驗證）。兩者安全模型不同，比較見 Layer 1 段落。</p></blockquote>
<hr>
<h2 id="layer-1使用者層bearer-token">Layer 1：使用者層（Bearer Token）</h2>
<p><strong>使用者層負責把 request 綁到已登入的人類或帳號主體</strong>。它回答的問題是：「這個 request 是哪個使用者發的？」</p>
<p><strong>Bearer Token 是 capability credential（持有即授權）、不是 identity credential（身分證明）</strong>。差別在於：身分證遺失可以掛失補辦、別人撿到也無法直接領錢；Bearer Token 一旦被取得、攻擊者就能即時用該使用者身分發 request、沒有第二道關卡。這個本質決定了 token 的儲存、傳輸、撤銷機制都必須以「持有即危險」為前提設計。</p>
<p>「Bearer Token」是 RFC 6750 定義的 HTTP authentication scheme（<code>Authorization: Bearer &lt;token&gt;</code>）、屬於通用概念 — GitHub PAT、Stripe API Key、OAuth access token、Laravel Sanctum 的 PAT、JWT 都是 Bearer Token 的不同實作。</p>
<h3 id="opaque-token-vs-jwt兩種根本不同的設計">Opaque Token vs JWT：兩種根本不同的設計</h3>
<p>「Bearer Token」是上位概念、實作上有兩條主線、安全模型完全不同：</p>
<table>
  <thead>
      <tr>
          <th>項目</th>
          <th>Opaque Token（如 Sanctum）</th>
          <th>JWT</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Token 本身</td>
          <td>隨機字串、無內含資訊</td>
          <td>簽章 payload、內嵌使用者 claim</td>
      </tr>
      <tr>
          <td>驗證方式</td>
          <td>server 查 DB lookup</td>
          <td>驗簽章、不需 DB</td>
      </tr>
      <tr>
          <td>載入使用者</td>
          <td>從 DB row 撈</td>
          <td>直接讀 claim</td>
      </tr>
      <tr>
          <td>撤銷</td>
          <td>刪 DB row、立即生效</td>
          <td>困難、需 blacklist 或短 TTL</td>
      </tr>
      <tr>
          <td>洩漏暴露範圍</td>
          <td>該 row 立即停用</td>
          <td>直到 expire 都有效</td>
      </tr>
      <tr>
          <td>跨服務驗證</td>
          <td>需要共用 DB 或驗證 endpoint</td>
          <td>共享公鑰即可、stateless</td>
      </tr>
  </tbody>
</table>
<p>兩者各有適合情境：opaque token 撤銷快、適合「使用者主動登出 / 帳號被盜要立即停權」；JWT 不需 DB lookup、適合「跨多個 microservice、想避免每次都查中央 DB」。下面 Layer 1 的內容<strong>只聚焦 opaque token</strong> — JWT 的設計細節（簽章演算法選擇、<code>alg: none</code> 攻擊、key rotation）是獨立議題、不在本篇範圍。</p>
<h3 id="opaque-token-的格式設計">Opaque Token 的格式設計</h3>
<p>Opaque token 是隨機字串、但實際 format 在不同產品有兩條主流分流：</p>
<table>
  <thead>
      <tr>
          <th>設計</th>
          <th>範例</th>
          <th>解的問題</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><strong><code>{PK}|{secret}</code></strong></td>
          <td><code>1|abc123def456...</code>（Laravel Sanctum）</td>
          <td>用 PK 收斂 DB 搜尋、把 timing 安全留給應用層</td>
      </tr>
      <tr>
          <td><strong><code>{prefix}_{secret}</code></strong></td>
          <td><code>ghp_xxx</code>（GitHub）、<code>sk_live_xxx</code>（Stripe）</td>
          <td>用語意 prefix 支援自動洩漏掃描跟 token type 辨識</td>
      </tr>
  </tbody>
</table>
<p>兩種設計<strong>沒有絕對優劣</strong>、取決於 token 的傳播範圍：純內部使用、Sanctum 設計簡潔且足夠；對外開放、容易散落公開 repo、prefix 設計能讓 GitHub Secret Scanning / Stripe webhook 等工具自動偵測洩漏。</p>
<p>Sanctum 的 <code>{PK}|{secret}</code> 設計常被誤解為「業界標準」 — 其實是 Laravel 生態的特定選擇。具體機制、跟 GitHub / Stripe 設計的比較、各語言實作範例見 <a href="/blog/work-log/laravel-sanctum-%E7%9A%84-bearer-token-%E8%A8%AD%E8%A8%88%E5%89%96%E6%9E%90pksecret-%E7%82%BA%E4%BB%80%E9%BA%BC%E9%80%99%E6%A8%A3%E8%A8%AD%E8%A8%88/" data-link-title="Laravel Sanctum 的 Bearer Token 設計剖析：{PK}|{secret} 為什麼這樣設計" data-link-desc="Laravel Sanctum `{PK}|{secret}` 格式的設計理由、hash 儲存取捨、constant-time 比對位置，以及跟 GitHub PAT、Stripe API Key 的差異。">Laravel Sanctum 的 Bearer Token 設計剖析</a>。</p>
<h3 id="token-在-db-的儲存原則簡述">Token 在 DB 的儲存原則（簡述）</h3>
<p>無論用哪種 format、有三條跨設計通用的儲存原則：</p>
<ol>
<li><strong>DB 只存 hash、不存原文</strong> — token 是高熵隨機字串、SHA-256 即可、不需 bcrypt</li>
<li><strong>比對必須是 constant-time</strong> — 用各語言提供的 <code>hash_equals</code> / <code>compare_digest</code> / <code>ConstantTimeCompare</code>、不用 <code>==</code></li>
<li><strong>Lookup 用穩定字段、機密比對放應用層</strong> — DB 引擎不保證 constant-time 比對、把機密比對搬離 DB</li>
</ol>
<p>這三條的詳細推導、各語言 constant-time 函式對照、非 Laravel 環境的實作範例見 <a href="/blog/work-log/laravel-sanctum-%E7%9A%84-bearer-token-%E8%A8%AD%E8%A8%88%E5%89%96%E6%9E%90pksecret-%E7%82%BA%E4%BB%80%E9%BA%BC%E9%80%99%E6%A8%A3%E8%A8%AD%E8%A8%88/" data-link-title="Laravel Sanctum 的 Bearer Token 設計剖析：{PK}|{secret} 為什麼這樣設計" data-link-desc="Laravel Sanctum `{PK}|{secret}` 格式的設計理由、hash 儲存取捨、constant-time 比對位置，以及跟 GitHub PAT、Stripe API Key 的差異。">Laravel Sanctum 的 Bearer Token 設計剖析</a>。</p>
<h3 id="token-的生命週期">Token 的生命週期</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="ln">1</span><span class="cl">   Login                  Use                  Expire/Revoke
</span></span><span class="line"><span class="ln">2</span><span class="cl">─────────  ───────────────────────────  ─────────────────
</span></span><span class="line"><span class="ln">3</span><span class="cl">issued → DB 存 hash  →  Bearer 驗證    →   row deleted
</span></span><span class="line"><span class="ln">4</span><span class="cl">                            ↓
</span></span><span class="line"><span class="ln">5</span><span class="cl">                       set request.user</span></span></code></pre></div><ul>
<li><strong><code>expires_at</code></strong>（例如 7 天、30 天）— 限制洩漏 token 的暴露窗</li>
<li><strong><code>abilities</code> / <code>scopes</code></strong> — 限縮權限粒度（「只能讀」「只能存取某 resource」），降低單一 token 洩漏的破壞範圍</li>
<li><strong>登出即刪 row</strong> — opaque token 的撤銷成本低，這是它相對 JWT 的關鍵優勢</li>
<li><strong>rate limit / brute force 防護</strong> — token 是隨機字串、攻擊者可暴力試。應用層要對「token 驗證失敗」加 rate limit、避免被掃出有效 token</li>
<li><strong>長期 access 用 refresh token pattern</strong> — access token 短 TTL（小時級）、refresh token 長 TTL（月級）。Access token 洩漏只影響短窗、refresh token 撤銷後新的 access token 也無法發放</li>
</ul>
<h3 id="信任邊界">信任邊界</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="ln">1</span><span class="cl">[ 使用者 ] ─────────▶ [ API server ]
</span></span><span class="line"><span class="ln">2</span><span class="cl">              token        ↑
</span></span><span class="line"><span class="ln">3</span><span class="cl">                           知道「你是誰」
</span></span><span class="line"><span class="ln">4</span><span class="cl">                           但不會自動跨到其他系統</span></span></code></pre></div><p>Bearer Token 是 capability credential — 任何持有它的 client 都能以該使用者身分發 request。這也是為什麼 token 一旦離開原本的 API server，就會引發下一層問題：B 系統收到 A 系統的 token、根本不知道該怎麼驗證、也不該驗證。</p>
<hr>
<h2 id="layer-2系統層system-to-system-credential">Layer 2：系統層（System-to-system credential）</h2>
<p><strong>系統層負責驗證呼叫方服務本身的身分</strong>。它回答的問題是：「這個 request 是哪個系統發的？」</p>
<p>當系統 A 需要呼叫系統 B 的 API 時，Layer 1 的使用者 token 只代表「使用者」的身分。系統 B 仍需要獨立驗證「這個 request 來自合法的合作系統 A」，這個判斷要由系統層 credential 承擔。</p>
<h3 id="為什麼分得這麼清楚">為什麼分得這麼清楚</h3>
<p>想像系統 B 收到一個請求：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="ln">1</span><span class="cl">B 收到請求「給我會員 X 的資料」
</span></span><span class="line"><span class="ln">2</span><span class="cl">   ↓
</span></span><span class="line"><span class="ln">3</span><span class="cl">B 自問：這請求來自...
</span></span><span class="line"><span class="ln">4</span><span class="cl">   ├─ 我的合作夥伴系統 A？  → 可進入授權判斷
</span></span><span class="line"><span class="ln">5</span><span class="cl">   ├─ 未註冊的外部 caller？ → 回 401 / 403
</span></span><span class="line"><span class="ln">6</span><span class="cl">   └─ 偽裝成 A 的 caller？  → 回 401 / 403 並記錄告警</span></span></code></pre></div><p>純粹靠 Layer 1 的使用者 token 只能證明「這位 user 的身分」，無法證明「系統 A 的身分」。這個分工讓帳號被盜與合作系統被冒用分別走不同監控與撤銷流程。</p>
<h3 id="shared-secret與api-key的關係">「Shared Secret」與「API Key」的關係</h3>
<p>兩者常被混用、實際上是同一個機制（一邊發、一邊存的對稱字串）的不同部署方式：</p>
<table>
  <thead>
      <tr>
          <th>區分點</th>
          <th>Shared Secret</th>
          <th>API Key</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Caller identity</td>
          <td>兩邊都用同一把、沒有 caller 區分</td>
          <td>每個 client 一把、server 有 key → identity 對照表</td>
      </tr>
      <tr>
          <td>撤銷粒度</td>
          <td>換一邊、全部斷</td>
          <td>撤一把 key、只影響該 client</td>
      </tr>
      <tr>
          <td>典型部署</td>
          <td>內部固定夥伴系統</td>
          <td>對外開放 API、多 tenant</td>
      </tr>
  </tbody>
</table>
<p>下面討論的「Shared Secret」泛指這個 pattern；要做 per-client identity 與 revoke 時、改成 API Key 結構即可。</p>
<h3 id="常見方案的取捨">常見方案的取捨</h3>
<table>
  <thead>
      <tr>
          <th>方案</th>
          <th>機制</th>
          <th>撤銷粒度</th>
          <th>適合情境</th>
          <th>主要代價</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><strong>Shared Secret</strong></td>
          <td>兩邊放同一把字串</td>
          <td>全部 caller</td>
          <td>內部單一夥伴、低變更頻率</td>
          <td>多 client 時撤銷會牽動所有人</td>
      </tr>
      <tr>
          <td><strong>API Key</strong></td>
          <td>每個 client 一把、server 有對照表</td>
          <td>per-client</td>
          <td>對外開放、多 tenant</td>
          <td>server 需維護 key → identity mapping</td>
      </tr>
      <tr>
          <td><strong>HMAC 簽章</strong></td>
          <td>client 用 secret 簽 request body</td>
          <td>per-key</td>
          <td>secret 不想經過網路、需防 replay / 改寫</td>
          <td>兩邊都要實作簽章邏輯、debug 較難</td>
      </tr>
      <tr>
          <td><strong>mTLS</strong></td>
          <td>雙向 TLS 憑證</td>
          <td>撤憑證</td>
          <td>金融、醫療、零信任網路</td>
          <td>憑證生命週期管理複雜、CA / CRL 基礎建設成本</td>
      </tr>
      <tr>
          <td><strong>OAuth Client Credentials</strong></td>
          <td>client_id + secret 換短期 access token</td>
          <td>撤 long-lived secret、短 token 自然 expire</td>
          <td>跨組織、權限粒度需要、需配合 scope</td>
          <td>多一層 token endpoint、實作成本較高</td>
      </tr>
  </tbody>
</table>
<p>選擇預設值的判斷：純內部固定夥伴可從 Shared Secret 起步；對外或多 client 直接上 API Key；公網跨組織 + 需要短期撤銷上 OAuth Client Credentials；合規或高威脅環境用 mTLS。</p>
<p>mTLS 的 CA 階層、憑證生命週期、撤銷機制、nginx / service mesh 整合見 <a href="/blog/work-log/mtls-%E5%AF%A6%E9%9A%9B%E6%80%8E%E9%BA%BC%E8%A8%AD%E5%AE%9A%E8%88%87%E9%81%8B%E7%B6%ADca-%E9%9A%8E%E5%B1%A4%E6%86%91%E8%AD%89%E7%94%9F%E5%91%BD%E9%80%B1%E6%9C%9F%E6%92%A4%E9%8A%B7%E6%A9%9F%E5%88%B6/" data-link-title="mTLS 實際怎麼設定與運維：CA 階層、憑證生命週期、撤銷機制" data-link-desc="mTLS 落地的運維決策（CA 階層、憑證儲存、撤銷機制）與基礎設施整合（nginx / envoy / service mesh），以及跟 API Key / OAuth 的成本與安全取捨。">mTLS 實際怎麼設定與運維</a>。</p>
<h3 id="shared-secret-的隱形成本">Shared Secret 的隱形成本</h3>
<p>Shared Secret 部署簡單、但維運上有幾個固定痛點：</p>
<ul>
<li><strong>無法 per-caller 撤銷</strong> — 一旦洩漏，所有用這把 secret 的 client 都得換</li>
<li><strong>輪替需要兩邊同步</strong> — 任何一邊忘了更新就斷線、需要「雙密過渡期」讓兩邊有時間切換。具體實作見 <a href="/blog/work-log/shared-secret-%E5%AE%89%E5%85%A8%E8%BC%AA%E6%9B%BF%E8%A8%AD%E8%A8%88%E9%9B%99%E5%AF%86%E9%81%8E%E6%B8%A1%E6%9C%9F%E8%87%AA%E5%8B%95%E5%8C%96%E8%88%87%E7%B7%8A%E6%80%A5%E6%B5%81%E7%A8%8B/" data-link-title="Shared Secret 安全輪替設計：雙密過渡期、自動化與緊急流程" data-link-desc="系統間 Shared Secret 輪替的核心機制：dual-secret rollover、自動化工具比較（AWS Secrets Manager / Vault / GCP）、緊急 rotation 流程與多 client 環境的失敗模式。">Shared Secret 安全輪替設計</a></li>
<li><strong>常被放進 query param</strong> — 為了簡便、會留在 nginx access log、CDN log、瀏覽器 history 裡。應放在 request header（例如 <code>X-System-Secret: xxx</code>）或走 HMAC / OAuth</li>
</ul>
<h3 id="信任邊界-1">信任邊界</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="ln">1</span><span class="cl">[ 系統 A ] ═════════▶ [ 系統 B ]
</span></span><span class="line"><span class="ln">2</span><span class="cl">       shared secret
</span></span><span class="line"><span class="ln">3</span><span class="cl">       (server-to-server, server-only credential)</span></span></code></pre></div><p><strong>Layer 2 secret 的安全邊界是 server-side runtime</strong>。一旦進入瀏覽器或行動 app，攻擊者就能透過反編譯、JS source map、devtools network panel 等管道取得；取得後即可假冒系統 A 呼叫系統 B。Mobile app 的反編譯工具（jadx、Hopper、Ghidra 等）讓這個攻擊成本極低，obfuscation 只能增加時間成本。</p>
<p>如果 client 端需要呼叫 B，安全路由是讓 client 先呼叫 A，由 A 在 server 端用 Layer 2 secret 呼叫 B（A 當 proxy / BFF）；另一條路是用 OAuth 把 short-lived token 發給 client，long-lived secret 留在 server。</p>
<hr>
<h2 id="layer-3跨系統-provisioning身分對應-workflow不是新的信任邊界">Layer 3：跨系統 Provisioning（身分對應 workflow、不是新的信任邊界）</h2>
<p><strong>回答的問題</strong>：「系統 A 的使用者 X、在系統 B 對應到哪個身分？」</p>
<p><strong>Layer 3 跟 Layer 1 / 2 在概念上不對等</strong> — Layer 1 / 2 是「驗證某個身分」的信任邊界、各自需要獨立的 secret 機制；Layer 3 不引入新的 secret、是「<strong>讓兩個系統的使用者身分對應上</strong>」的 workflow。它建立在 Layer 1（A 已驗證使用者）跟 Layer 2（A 已被授權呼叫 B）之上、不取代任何一層。</p>
<p>之所以仍放進「層」的編號系統、是因為實際 API 串接時、開發者會把它跟前兩層一起遇到、必須在同一個心智模型裡處理。但設計時要清楚意識到：<strong>Layer 3 的失敗模式是「身分對不上」、不是「身分被偽造」</strong>、跟 Layer 1 / 2 的安全失敗模式不同。</p>
<h3 id="為什麼需要-provisioning">為什麼需要 provisioning</h3>
<p>當 A 跟 B 是兩個獨立 service 時，「<strong>A 的使用者 X</strong>」跟「<strong>B 的使用者 X</strong>」未必是同一筆資料。可能：</p>
<ul>
<li>B 從來沒見過 X 這個人</li>
<li>B 有自己對 X 的 record、但跟 A 不同 schema</li>
<li>B 看過 X、但兩邊的 user_id 還沒對應上</li>
</ul>
<p>需要一個機制把兩邊綁定 — 這個動作叫 <strong>provisioning</strong>。</p>
<h3 id="eager-vs-lazy-兩種策略">Eager vs Lazy 兩種策略</h3>
<p>Provisioning 策略的判斷核心是「何時承擔跨系統建檔成本」。Eager 把成本前移到註冊流程，Lazy 把成本延後到第一次使用；兩者差異不只是效能，而是資料膨脹、首用體驗與文件契約的取捨。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="ln">1</span><span class="cl">EAGER (註冊時就跨系統建檔)
</span></span><span class="line"><span class="ln">2</span><span class="cl">────────────────────────────
</span></span><span class="line"><span class="ln">3</span><span class="cl">使用者註冊系統 A
</span></span><span class="line"><span class="ln">4</span><span class="cl">   ↓
</span></span><span class="line"><span class="ln">5</span><span class="cl">   A 新增會員 row
</span></span><span class="line"><span class="ln">6</span><span class="cl">   ↓
</span></span><span class="line"><span class="ln">7</span><span class="cl">   A ──同步呼叫──▶ B.createUser()  ← 即使他可能永遠不用 B
</span></span><span class="line"><span class="ln">8</span><span class="cl">   ↓
</span></span><span class="line"><span class="ln">9</span><span class="cl">   兩邊都有資料、可以立刻呼叫 B 的 API</span></span></code></pre></div><p>Eager 適合大多數使用者都會用到 B 功能、且首用延遲成本高的服務。主要風險是 B 會累積大量低活躍 user，schema migration、備份與隱私刪除流程都會被放大。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="ln"> 1</span><span class="cl">LAZY (第一次需要時才建)
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">────────────────────────────
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">使用者註冊系統 A
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">   ↓
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">   A 新增會員 row              ← 只有 A 這邊
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">   ↓
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">   ...日後可能很久才用到 B...
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">   ↓
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">使用者第一次需要 B 的功能
</span></span><span class="line"><span class="ln">10</span><span class="cl">   ↓
</span></span><span class="line"><span class="ln">11</span><span class="cl">   呼叫 A 的「provision」endpoint
</span></span><span class="line"><span class="ln">12</span><span class="cl">   ↓
</span></span><span class="line"><span class="ln">13</span><span class="cl">   A ──呼叫──▶ B.findOrCreateUser()  ← 這時候才建
</span></span><span class="line"><span class="ln">14</span><span class="cl">   ↓
</span></span><span class="line"><span class="ln">15</span><span class="cl">   之後就跟 eager 一樣</span></span></code></pre></div><p>Lazy 適合只有一部分使用者會用到 B 功能、且第一次使用可以接受一次 provisioning 延遲的服務。主要風險是「第一次使用」這個時機需要被寫進文件、SDK 或錯誤碼，否則接手者會把 B 的 404 誤判成 request 格式或權限問題。</p>
<h3 id="lazy-的隱性-api-依賴順序">Lazy 的「隱性 API 依賴順序」</h3>
<p>Lazy provisioning 的最大成本是<strong>隱性依賴順序造成的認知負擔</strong>：</p>
<ul>
<li>文件若沒有寫清楚「呼叫 B 前先呼叫 A 的 provision endpoint」，接手者會在「B 回 404 找不到 user」的訊號上花大量時間排查</li>
<li>用 SDK 包裝可以把 provision 自動處理、對外只暴露單一 API</li>
<li>不用 SDK 時，文件需要在快速上手與錯誤碼段落顯眼註明這個依賴順序</li>
</ul>
<p>折衷做法：B 的 API 在第一次發現 user 不存在時、<strong>主動回一個 <code>PROVISIONING_REQUIRED</code> 錯誤碼</strong>、client 看到就知道要去呼叫 A 的 provision endpoint。比起靜默 500 或單純 404 更能引導 client 走到正確流程。</p>
<h3 id="信任邊界示意">信任邊界示意</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="ln">1</span><span class="cl">[ 使用者 ] ──Layer 1──▶ [ 系統 A ] ══Layer 2══▶ [ 系統 B ]
</span></span><span class="line"><span class="ln">2</span><span class="cl">                            │  Layer 3 workflow：
</span></span><span class="line"><span class="ln">3</span><span class="cl">                            └─ 觸發後在 B 建立對應身分</span></span></code></pre></div><p>Layer 3 不引入新的 secret、是「<strong>建立兩邊身分關聯</strong>」的 lifecycle 動作。它依賴 Layer 1（確認使用者身分）跟 Layer 2（A 被授權對 B 發指令）。沒有 Layer 1 / 2 的話、provisioning 自己無法獨立成立。</p>
<hr>
<h2 id="三層怎麼組合">三層怎麼組合</h2>
<p>把三層擺在一起的典型 request 流程：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="ln"> 1</span><span class="cl">        ┌─────────────┐                       ┌──────────────┐
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">        │  使用者      │                       │   系統 A     │
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">        │  (Browser/  │ ──── Layer 1 ──────▶ │              │
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">        │   App)      │      Bearer token     │              │
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">        └─────────────┘                       └──────┬───────┘
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">                                                     │
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">                                            Layer 3  │ Provision
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">                                                     │ (第一次)
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">                                                     ▼
</span></span><span class="line"><span class="ln">10</span><span class="cl">                                              ┌──────────────┐
</span></span><span class="line"><span class="ln">11</span><span class="cl">                                              │   系統 B     │
</span></span><span class="line"><span class="ln">12</span><span class="cl">                                              └──────────────┘
</span></span><span class="line"><span class="ln">13</span><span class="cl">                                                     ▲
</span></span><span class="line"><span class="ln">14</span><span class="cl">                                                     │
</span></span><span class="line"><span class="ln">15</span><span class="cl">                                            Layer 2  │ Shared secret
</span></span><span class="line"><span class="ln">16</span><span class="cl">                                                     │ (server-to-server)</span></span></code></pre></div><p>每一條線都是一層信任邊界，各自需要不同 secret 機制保護。</p>
<hr>
<h2 id="設計時最常見的三個失效模式">設計時最常見的三個失效模式</h2>
<h3 id="失效模式一讓使用者-token-也能驗-layer-2">失效模式一：讓使用者 token 也能驗 Layer 2</h3>
<p><strong>責任分工</strong>：「使用者身分」跟「呼叫系統身分」是兩個獨立維度、各自需要獨立 credential。系統 B 對「來自 A」的信任應綁定在系統層 credential，而不是任何單一使用者帳號上。</p>
<p><strong>常見誤用</strong>：B 接受「只要 request 帶有任一合法使用者 token 就放行」。</p>
<p><strong>風險判讀</strong>：這會把系統信任降階為使用者信任。任一帳號被盜（釣魚、密碼洩漏、token 外流）時，攻擊者就能用該使用者身分對 B 發 request，執行 B 開放給 A 的系統操作。</p>
<p><strong>操作路由</strong>：使用者層用 Layer 1 token，系統層用 Layer 2 credential，兩層都通過才放行。</p>
<h3 id="失效模式二把-layer-2-secret-放進-client">失效模式二：把 Layer 2 secret 放進 client</h3>
<p><strong>責任分工</strong>：Layer 2 secret 是「server 代表系統 A 對外的證明」，應留在 server 端的受信任執行環境。</p>
<p><strong>常見誤用</strong>：把 shared secret 寫進前端 JS、行動 app 編譯時、甚至 git public repo。</p>
<p><strong>風險判讀</strong>：client 環境（瀏覽器、mobile app）不在受控範圍。JS source 可在 devtools 直接看，mobile binary 可被反編譯出字串。Obfuscation 提高的是時間成本，沒有改變 secret 已散佈到不受信任環境的事實。</p>
<p><strong>操作路由</strong>：client 需要 B 的功能時，走「client → A → B」，由 A 在 server 端用 Layer 2 secret 呼叫 B；或用 OAuth 把 short-lived token 發給 client，long-lived secret 留在 server。</p>
<h3 id="失效模式三layer-3-依賴順序沒文件化">失效模式三：Layer 3 依賴順序沒文件化</h3>
<p><strong>責任分工</strong>：跨系統依賴順序是 API 契約的一部分，屬 publisher 的責任，需要在文件、SDK 或錯誤訊號中顯式表達。</p>
<p><strong>常見誤用</strong>：「呼叫 B 之前要先呼叫 A 的某個 endpoint」這個前置條件只存在於原始設計者的記憶中、文件沒寫、SDK 沒包、B 失敗時也只回 generic error。</p>
<p><strong>風險判讀</strong>：接手者看到「呼叫 B 失敗」時，會優先檢查 B 的文件、request 格式與 network 層。若真正根因是尚未呼叫 A 的 provision endpoint，偵錯路徑會被導到錯誤層級。</p>
<p><strong>操作路由</strong>（任選其一、優先序由上而下）：</p>
<ol>
<li>SDK 包裝、自動處理 provision、對外只暴露單一 API</li>
<li>B 主動回 <code>PROVISIONING_REQUIRED</code> error code、引導 client 補上前置呼叫</li>
<li>文件在「快速上手」段顯眼處註明依賴順序</li>
</ol>
<hr>
<h2 id="何時可以簡化三層">何時可以簡化三層</h2>
<p>三層框架的設計重點是「跨系統身分與 credential 分工」。當某一層回答的問題在架構裡不存在，設計可以縮小到實際存在的身分問題。</p>
<table>
  <thead>
      <tr>
          <th>情境</th>
          <th>簡化方式</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>單體 application（沒有跨系統呼叫）</td>
          <td>只需 Layer 1。沒有 system-to-system 互動、Layer 2 / 3 不存在</td>
      </tr>
      <tr>
          <td>內網微服務、共用 identity provider</td>
          <td>Layer 1 透過 service mesh 或共用 token 傳遞、Layer 2 可用 service mesh 內建 mTLS 取代手動 secret 管理</td>
      </tr>
      <tr>
          <td>後端 cron / batch job 之間互呼</td>
          <td>只需 Layer 2（system-to-system credential）、沒有使用者觸發、Layer 1 不適用</td>
      </tr>
      <tr>
          <td>兩個系統共用同一份 user DB</td>
          <td>可省略 Layer 3（身分天然對應），但 Layer 1 / 2 仍各自獨立</td>
      </tr>
  </tbody>
</table>
<p>簡化的判準是「<strong>該層回答的問題是否真實存在於這個架構</strong>」。單體 application 沒有跨系統呼叫時，Layer 2 的 caller 驗證可以省略；兩個系統共用同一份 user DB 時，Layer 3 的身分對應 workflow 可以省略。</p>
<p>簡化不等於降低基礎安全前提。HTTPS / TLS 與 token 儲存原則（hash + constant-time）是任何 Layer 1 的最低要求，跟「層」的數量無關。</p>
<hr>
<h2 id="收尾">收尾</h2>
<p>兩層信任邊界 + 一個身分對應 workflow：</p>
<ul>
<li><strong>Layer 1（使用者）</strong>：解決「你是誰」 — 用 Bearer Token、注意 capability credential 的暴露成本</li>
<li><strong>Layer 2（系統）</strong>：解決「哪個系統呼叫的」 — 用 Shared Secret / API Key / OAuth / mTLS、secret 不離 server</li>
<li><strong>Layer 3（Provisioning workflow）</strong>：解決「兩邊身分怎麼對上」 — 不是新的 secret、是 lifecycle 動作</li>
</ul>
<p>設計後端 API 時，先把這三個問題分開，secret 機制的選擇會變清楚。若排障訊號是「這個 token 在那邊不能用」，下一步是先判斷它卡在使用者層、系統層，還是 provisioning workflow。</p>
<h3 id="各層的深入文章">各層的深入文章</h3>
<p>本文聚焦「為什麼要分層」的心智模型、各層的具體實作細節都另有獨立文章：</p>
<ul>
<li><strong>Layer 1（使用者）</strong> → <a href="/blog/work-log/laravel-sanctum-%E7%9A%84-bearer-token-%E8%A8%AD%E8%A8%88%E5%89%96%E6%9E%90pksecret-%E7%82%BA%E4%BB%80%E9%BA%BC%E9%80%99%E6%A8%A3%E8%A8%AD%E8%A8%88/" data-link-title="Laravel Sanctum 的 Bearer Token 設計剖析：{PK}|{secret} 為什麼這樣設計" data-link-desc="Laravel Sanctum `{PK}|{secret}` 格式的設計理由、hash 儲存取捨、constant-time 比對位置，以及跟 GitHub PAT、Stripe API Key 的差異。">Laravel Sanctum 的 Bearer Token 設計剖析</a>：<code>{PK}|{secret}</code> format 為什麼這樣設計、DB 儲存三原則、各語言 constant-time 函式對照、跟 GitHub / Stripe 的設計比較</li>
<li><strong>Layer 2（系統）→ Shared Secret 維運</strong> → <a href="/blog/work-log/shared-secret-%E5%AE%89%E5%85%A8%E8%BC%AA%E6%9B%BF%E8%A8%AD%E8%A8%88%E9%9B%99%E5%AF%86%E9%81%8E%E6%B8%A1%E6%9C%9F%E8%87%AA%E5%8B%95%E5%8C%96%E8%88%87%E7%B7%8A%E6%80%A5%E6%B5%81%E7%A8%8B/" data-link-title="Shared Secret 安全輪替設計：雙密過渡期、自動化與緊急流程" data-link-desc="系統間 Shared Secret 輪替的核心機制：dual-secret rollover、自動化工具比較（AWS Secrets Manager / Vault / GCP）、緊急 rotation 流程與多 client 環境的失敗模式。">Shared Secret 安全輪替設計</a>：雙密過渡期、自動化 rotation 工具（AWS Secrets Manager / Vault / GCP）、緊急 vs 定期流程、多 client 同步難題</li>
<li><strong>Layer 2（系統）→ mTLS 部署</strong> → <a href="/blog/work-log/mtls-%E5%AF%A6%E9%9A%9B%E6%80%8E%E9%BA%BC%E8%A8%AD%E5%AE%9A%E8%88%87%E9%81%8B%E7%B6%ADca-%E9%9A%8E%E5%B1%A4%E6%86%91%E8%AD%89%E7%94%9F%E5%91%BD%E9%80%B1%E6%9C%9F%E6%92%A4%E9%8A%B7%E6%A9%9F%E5%88%B6/" data-link-title="mTLS 實際怎麼設定與運維：CA 階層、憑證生命週期、撤銷機制" data-link-desc="mTLS 落地的運維決策（CA 階層、憑證儲存、撤銷機制）與基礎設施整合（nginx / envoy / service mesh），以及跟 API Key / OAuth 的成本與安全取捨。">mTLS 實際怎麼設定與運維</a>：CA 階層、憑證生命週期、撤銷機制（CRL / OCSP / short-lived）、nginx / Envoy / service mesh 整合</li>
</ul>
<h3 id="沒展開的延伸議題">沒展開的延伸議題</h3>
<p>JWT 的簽章演算法選擇、<code>alg: none</code> 攻擊、token rotation 的具體實作、零信任網路下的 service-to-service 認證、OAuth flow 的完整 lifecycle、SSO（SAML / OIDC）跟本文三層的對應關係。每個都值得獨立成篇、本文聚焦在「先把層數想清楚」這個前置問題。</p>
]]></content:encoded></item></channel></rss>