非公開的 Web API 一定會進行驗證來確保呼叫者是允許使用的,在這篇文章中將紀錄我遇過的幾種驗證方式。

API Key

這是最簡單也最最方便的一種方式,它是由 API 提供者隨機產生一個字串給呼叫者,而呼叫者送出的 Request 必須包含這個隨機字串讓 API 提供者可以驗證其身份,而這個用於驗證的隨機字串就叫做 API Key。

雖然可以只使用 Key 就知道是哪個呼叫者呼叫的,但也有些 API 提供者也會要求也要多帶 Client Id 進行驗證。

放置位置

呼叫時要如何帶給 API 提供者,一般是由 API 提供者去指定一個位置,有些會放在 Header 或 Query String 中,也有少部分直接放在 Body 裡面。

  • 放在 Header 中:最常見的一種方式,有的會直接用標準的 Header 名稱 (如 Authorization),或是自己指定一個名稱 (如 X-Api-Key)。
  • 放在 Query String 中:此種方式就是直接將 Key 帶在 Url 的 Query String 中,如 ?key=<API_KEY>,但 Web Server 那邊 Log 可能要調整避免儲存到 Key。
  • 放在 Body 中:這一種我比較少看過,就是直接把 Key 包含在 Body 裡面。

相關實例:

  • Imgur:使用公共資料相關的 API,只需將 ClientId 帶在 Header 的 Authorization 中即可。
  • Google Cloud APIs:部分允許使用 API Key 的 API 只需將 Key 帶在 Query String 中即可,如 ?key=<API_KEY>

在 Server 端只儲存 Hash 結果

API Key 就跟我們一般使用者密碼一樣是用於驗證身份,那我們同樣也可以把儲存密碼的方式拿來儲存 API Key。

你可以只儲存經過 Hash 的值,然後在驗證時用 Hash 的值來比對而非原始的值。如果需要提示呼叫者目前的 Key,則可以只儲存前幾個字元供識別即可。

當然這不是一個一定要做的事,因為就算 API Key 外洩也只能在同一個 API 提供者使用,不像密碼可能會出現多個網站共用的情性。

API Token

API Token 與 API Key 相似,跟 Key 的差別在 Token 可能是經過編碼或加密產生的,格式可能是公開的格式 (JWT) 或是只有他們自己知道的格式。我們可以把它視為一個由 API 提供者所發的身份證,呼叫者呼叫 API 時或附上它,而 API 提供者可直接透過它知道呼叫者的身份及權限等資訊。

通常在 OAuth 通過驗證取得 access token 時,這個 token 就是以 JWT 的方式產生。

JSON Web Tokens (JWT)

JWT 是由 Header、Payload 及 Signature 三個部分組成,彼此間使用 . 連結:

  • Header:JWT 第一個部分,用來標示類型為 JWT 及紀錄簽章演算法的類型 (SHA256, RSA…),是使用 Base64 編碼的 JSON 格式。
{
  "alg": "HS256",
  "typ": "JWT"
}
  • Payload:JWT 第二個部分,用來存放資料的地方,可以 Claim 名稱可以用標準的也可以自訂,一樣也是使用 Base64 編碼的 JSON 格式。
{
    "sub": "1234567890",
    "name": "Puck Wang",
    "admin": true
}
  • Signature:JWT 第三個部分,是一個訊息的簽章,用於檢查前兩部分是否有被修改。
HMACSHA256(
  base64UrlEncode(header) + "." + base64UrlEncode(payload),
  secret
)

將三個部分組合起來後就會像這樣

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IlB1Y2sgV2FuZyIsImFkbWluIjp0cnVlfQ.KcGicYB-emFBnUrXIViwDzCML6FB8W8QfD1lttRV0t4

HMAC Signature (簽章驗證)

雜湊訊息鑑別碼 (Hash-based message authentication code,縮寫為 HMAC),是使用密碼雜湊函式及一個金鑰來保證資料的完整性。

flowchart LR subgraph 發送者 direction TB Message1[Message]-->Alg1[HMAC Algorithm] Alg1-->MAC1[MAC] end Message1-->Data MAC1-->Data Data[Mesage & MAC] subgraph 接收者 direction LR Message2[Message]-->Alg2[HMAC Algorithm] Alg2-->MAC2[MAC]-->eq MAC-->eq eq{=?} eq -- "==" --> Success[Message 未被竄改] eq -- != --> Failure[Message 已被竄改] end Data-->MAC Data-->Message2[Message]

在這個驗證方式中,呼叫 API 時並不是直接帶 API Key 在 Request 中,而是使用 HMAC 的演算法並將 API Key 作為計算時的金鑰,將指定的訊息計算後產生出 Signature,再把 Signature 帶在 Request 中。而 API 提供者收到 Request 後也用同樣的方式去產生 Signature,如果兩者相同就算是通過驗證。

訊息格式會由 API 提供者去制定,而 HMAC 的演算法選擇部分就要看原生語言支援了,大部分應該都有 MD5、SHA1、SHA256、SHA384…,建議用 SHA256 以後的可能比較安全,因為 MD5 和 SHA-1 已經不安全了。另外是有些是會支援多個演算法給呼叫者選擇,而呼叫者就可以選擇一個比較方便使用的,並在後續紀錄於 Header 中讓 API 提供者知道是哪個演算法。

為了避免被彩虹表攻擊及防止重送攻擊 (replay attack),通常會在訊息裡面包含時間的資訊,而 API 提供者驗證時,就會規定這個時間必須在 XX 時間以內,以此限制這個簽章有效時間。但這樣沒有完全防止重送攻擊,只是縮短可能的時間而已,所以除了加入時間資訊外,還會加入其他資訊來達到更完善的防護。

放置位置

比較常見的做法是放在 Header 中,但有些會直接放在 Authorization,有些會用自訂 Header 來放。

  • 使用 Authorization 的範例
Authorization: HMAC-SHA256 ClientId=<ClientId>&Timestamp=<Timestamp>&Signature=<Signature>
  • 使用自訂 Header 的範例
x-ClientId: <ClientId>
x-Timestamp: <Timestamp>
x-Signature: <Signature>

最簡單的一個範例

在這個範例中,會需要用到的資料:

  • ClientId:識別呼叫者身份。
  • Timestamp:發送 Request 當下時間搓 (Unix timestamp),不可與伺服器時間相差 XX 時間。

首先將它們依照指定格式組成一個字串後,接下來拿 API Key 作為 Key 使用 HMAC-SHA256 算出 Signature

HMACSHA256("<ClientId><Timestamp>", <ApiKey>) 

最後把 ClientIdSignatureTimestamp 三個值包含在 Request 裡面,那 API 提供者就可以用同樣的方式去算出 Signature,如果兩者相等且 Timestamp 也在指定時間以內,就代表通過驗證。

這個範例還是可能會有被重送攻擊的風險,接下來將在訊息中加入更多資訊以更完整的防止重送攻擊。

利用 Timestamp 及 Nonce 來防止重送攻擊

這個方法中,每個 Request 都必須包含一個隨機值 Nonce,而 API 提供者則會檢查在一段時間內不允出現重複的 Nonce 來達到防止重送攻擊。

呼叫者 部分,需要準備的資料:

  • ClientId: 識別呼叫者身份。
  • Timestamp:發送 Request 當下時間搓 (Unix timestamp),不可與伺服器時間相差 XX 時間。
  • Nonce:隨機值,可以直接使用 UUID 或時間 (ms),指定時間內不可重複。

首先將它們依照指定格式組成字串後,接下來拿 API Key 作為 Key 使用 HMAC-SHA256 計算出 Signature,再把 ClientIdNonceTimestampSignature 四個值包含在 Request 裡面,範例 Header 如下:

x-ClientId: <ClientId>
x-Timestamp: <Timestamp>
x-Nonce: <Nonce>
x-Signature: HMACSHA256("<ClientId><Timestamp><Nonce>", <ApiKey>) 

API 提供者 部分,除了需要檢查 Signature 是否相同及 Timestamp 是否過期外,還要多檢查在 XX 時間內 Nonce 有沒有重複,如果重複就要視為不通過驗證。

Nonce 及 Timestamp 的時間限制是相同的,且大部分為 5 ~ 15 分鐘,而 Nonce 長度不限,只要在時間內不容易碰撞即可。

這個方法雖然已大幅度降低重送攻擊的風險,比較麻煩的地方就是必須用 Memory Cache 或 Redis 去紀錄 Nonce 一定的時間來用於檢查有沒有重複,這就增加了一些執行成本。

加入更多資料來防止重送攻擊

除了使用 Nonce 以外,你也可以將 Query String、Body 甚至 Header 也都加入訊息作為驗證依據,讓攻擊者就算拿到 Signature 也只能執行相同的操作不能拿去執行其他操作,藉此縮小風險。

呼叫者 部分,需要準備的資料:

  • ClientId: 識別呼叫者身份。
  • Timestamp:發送 Request 當下時間搓 (Unix timestamp),不可與伺服器時間相差 XX 時間。
  • Nonce:隨機值,可以直接使用 UUID 或時間 (ms),指定時間內不可重複。
  • QueryString:Query String。
  • Body:Body 經過 Hash 計算後的值。
  • URL:Request URL。
  • Method:Request Method。

首先將它們依照指定格式組成字串後,接下來拿 API Key 作為 Key 使用 HMAC-SHA256 計算出 Signature,再把 ClientIdNonceTimestampSignature 帶在 Request 的 Header 裡面:

x-ClientId: <ClientId>
x-Timestamp: <Timestamp>
x-Nonce: <Nonce>
x-Signature: HMACSHA256("<ClientId><Method><URL><QueryString><Body><Timestamp><Nonce>", <ApiKey>) 

API 提供者 部分,只要檢查以下有任意一個不通過,就代表不通過驗證:

  1. Timestamp 是否已超過指定時間範圍?
  2. Nonce 是否重複?
  3. Signature 是否相等?

相關實例

OAuth

待補

參考資料


感謝閱讀!

喜歡這篇文章或是有幫助到你嗎? 歡迎分享給你的朋友!

有任何問題、回饋或您認為我會感興趣的任何東西嗎? 請在下面發表評論,或者是直接聯絡我


Puck Wang

Puck Wang

Hi! 我是 Puck Wang,這個部落格的作者,是一位全端網站開發者,常使用 .Net 和 React 進行開發,專注於架構研究,你可以在這個部落格看到我精選的筆記內容,希望對你會有所幫助。

更多關於我的訊息,可至關於關於頁面。