Aries: kong
在HMAC
的設定上看起來也不算複雜,但是這個clock_skew
設定多久才是比較好的呢?
Sam: 預設是300秒鐘,其實這沒有一定對的答案,需要透過設定之後,透過一段時間的觀察來確認。如果這次的來源對象是固定的,那問題可能比較好解決,只要跟對方確認伺服器是不是都有固定與NTP Server
持續同步時間即可。
Lala: 那這樣300秒是不是看起來有點太多?有沒有建議值呢?
Sam: 如果我沒記錯,像 Windows AD 使用的Kerberos 認證,容許值就是300秒。但一般來說我相信60秒就夠了。不過還是要視對象,如果對象是多數不確定來源,例如APP
會被裝在非常多不確定的來源裝置,這時候容許值可能就要提高一些。
Aries: 但現在手機大多數不是都會自動校時?這還是必要的嗎?
Sam: 你說的對,其實理論上不需要調高,但如果這件事情涉及到了廣大客戶,那就有可能要與維運人員時常的確認,有哪些客戶可能因為這個認證方式而被擋下來。我聽說過有些人為了讓自己提早20分鐘起床,那他的手機時鐘可能就會刻意調整早20分鐘。這時就可以確認客戶一定會登入失敗。因此,建議是稍微先放寬,並把這些因為這因素被擋下來的客戶造冊,提供給第一線客服人員了解這個狀況,並主動說明。這樣資訊單位的我們才不會變成箭靶。
Lala: 原來如此,既然都已經設定好了,那我們是不是要跟客戶,要如何透過HMAC
的方式來呼叫我們的新產品呢?
Sam: 沒錯,我已經準備好範例程式了,讓我來跟你們簡單說明。
圖26-1 隨時間變化的 header
首先筆者先用圖26-1來說明 client端的請求標頭(request header),要注意到hmac-auth
這個驗證方式,僅支援設定在authorization
以及proxy-authorization
上。一方面這兩個 header在預設下,都會被 kong
識別為不該被留在 log中的機敏資訊,因此在Kibana
中就不會看到敏感資料。
另外,在 http 中,絕大多數認證授權的欄位都會被放在authorization
中,因此如果有欄位搶奪的問題,還可以轉用proxy-authorization
作為替代。
補充參考資料:Kong Plugin Hub - HMAC-Auth中有提及, The plugin validates the digital signature sent in the
Proxy-Authorization
orAuthorization
header (in that order).
從圖26-1 就可以看出,每次不同的時間,Authorization
中的signature
會不斷的改變。這表示在這一刻即使Authorization
的內容被拿走,在下一秒鐘就又會變化,讓盜取者無從使用起。
接下來的Client端範例程式,筆者是透過過去曾經寫過的一個 open source project來說明。如果讀者有興趣,可以下載來跟筆者一起體驗。
class Program
{
// 全域參數
private static readonly string APPID = "2025_customer-user1"; //Consumer 在 hmacauth_credentials中的username
private static readonly string APPKey = "4MfcLIDVuxfH67gwuANeFiUTineOHHVs"; //Consumer 在 hmacauth_credentials中的secret
private static readonly string APIUrl = "http://localhost:8000/FinancalHMAC/2025-09-11"; //本次示範的API rul,Get
static void Main(string[] args)
{
// 呼叫 API 範例
CallApiWithCurrentTime();
}
static void CallApiWithCurrentTime()
{
// 取得當下 UTC 時間
string xdate = DateTime.Now.ToUniversalTime().ToString("r");
string sAuth = GenerateAuthorizationHeader(xdate);
// 印出 Authorization 和 x-date
Console.WriteLine("Authorization: " + sAuth);
Console.WriteLine("x-date: " + xdate);
string Result = CallApi(xdate, sAuth);
Console.WriteLine(Result);
}
static string GenerateAuthorizationHeader(string xdate)
{
string SignDate = "x-date: " + xdate;
string Signature = HMAC_SHA256.Signature(SignDate, APPKey);
return $"hmac username=\"{APPID}\", algorithm=\"hmac-sha256\", headers=\"x-date\", signature=\"{Signature}\"";
}
static string CallApi(string xdate, string sAuth)
{
string Result = string.Empty;
ServicePointManager.Expect100Continue = true;
ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls13;
using (HttpClient Client = new HttpClient(new HttpClientHandler { AutomaticDecompression = DecompressionMethods.GZip }))
{
Client.DefaultRequestHeaders.Add("Authorization", sAuth);
Client.DefaultRequestHeaders.Add("x-date", xdate);
try
{
var response = Client.GetAsync(APIUrl).Result;
Console.WriteLine("Status Code: " + (int)response.StatusCode); // 印出數字形式的狀態碼
Result = response.Content.ReadAsStringAsync().Result;
Console.WriteLine("API 呼叫成功:");
}
catch (Exception ex)
{
Console.WriteLine("API 呼叫失敗 (可能因為時間逾時):");
Result = ex.Message;
}
}
return Result;
}
}
public class HMAC_SHA256
{
public static string Signature(string xDate, string AppKey, string inputCharset = "utf-8")
{
Encoding _encode = Encoding.GetEncoding(inputCharset);
byte[] _byteData = Encoding.GetEncoding(inputCharset).GetBytes(xDate);
HMACSHA256 _hmac = new HMACSHA256(_encode.GetBytes(AppKey)); // 使用 HMACSHA256
using (CryptoStream _cs = new CryptoStream(Stream.Null, _hmac, CryptoStreamMode.Write))
{
_cs.Write(_byteData, 0, _byteData.Length);
}
return Convert.ToBase64String(_hmac.Hash);
}
}
程式碼範例說明如下:
上面的實作就代表著,Client
每次發動請求都會根據當下時間產生不同的簽章。而伺服器端收到請求後,會用相同方式驗證簽章與時間,這種方式可以有效的防止被中間人竊取之後的重放攻擊。接下來實際執行,並透過可觀測性的trace
以及log
來確認,是不是實作成功。
如果你也要跟著筆者一起測試,記得最上面的全域變數一定要跟著改喔!
APPID
=kong.hmacauth_credentials.username
APPKey
=kong.hmacauth_credentials.hmacauth_credentials中的secret
APIUrl
=這次示範的url
將全域變數變更後,請到專案目錄下,執行dotnet run
。
圖26-2 Client端的測試
示範程式會得到兩個結果,當初筆者是為了測試兩個案例。第一個是為了驗證正確的認證資訊,因此會得到http 200
的呼叫成功回應。第二個則是刻意模擬,如果裝置(APP)時間是偏差的狀態,因此刻意送出逾時4分鐘的時間戳記,得到了預期的認證失敗
的http 401
的回應。
圖26-3 log的紀錄
打開kibana
的紀錄,可以看到跟示範程式實驗的結果一致,首先先是第一次401
錯誤,接著第二次就是 200
的正確回應。圖26-3 還有一個細節,箭頭的地方指出hmacauth_credentials.username
。Day 25在設定kong
的時候有說到,consumer
在設定 hmacauth_credentials
的時候,是一個list
的型別。
這表示一個consumer
可以同時存在多種認證資訊(apikey
,hmacauth_credentials
,jwt
)外,也可以同時存在多組hmacauth_credentials
。讓我們回頭去思考一下這次客戶的需求內容(註:為確保本行資訊安全,所提供之服務須經安全性連線外,認證授權機制之機敏資訊需可供定期輪替,並確保逾時後授權失效之機制....),是不是有提到可以定期輪替?
通常維運最大的困擾就是,雖說雙方已經約定好例如在 9/22 8:00要完成年度輪替金鑰的作業,但如果仰賴雙方人工作業後雙方核對,偶爾總是會發生服務暫時無法使用(有可能誰早做了或是晚做了)。而kong
在這種可以允許存放多組credentials
,並可透過kibana
辨識攜帶哪一組進行請求的特性,可以讓維運人員先同時存在兩組credentials
,並藉由觀察kibana
確認consumer
是否已輪替金鑰,確保輪替行為完成後,再將舊的credentials
刪除。。
這表示可以讓雙方不會因為人工作業的失誤造成服務中斷,維運的風險可以降低,真是一舉兩得。
圖26-4 從Jaeger 的角度來看kong.hmac_auth
最後,筆者其實常常遇到一個困擾,特別是當service
與route
越設定越多的時候,常常會發生混亂。到底某個請求進來,是經過了那些kong
的plugin
的邏輯呢?
這時就可以從圖26-4 來看,Jaeger
可以告訴我們答案。可以看到這次的請求中,hmac_auth
的設定的確有在trace
中出現。表示設定的確生效了~
Lala: 有程式可以幫忙驗證設定是否成功,實在是幫了大忙了。
Sam: 那當然,如果只是透過文件告訴合作對象要如何撰寫程式,其實比不上提供範例程式,而且是驗證設定完成的結果給對方確認還來的直觀。
Aries: 那萬一對方不是用 dotnet
,會不會反過來跟我們索取例如python
的範例程式呢?
Sam: 我認為這倒不是問題,其實完善整個維運體系,本來就應該要持續累積各種範例,讓合作可以順利進行下去。我們撰寫了範例程式,不論是幾種版本,通常應該就要被累積在內部的知識庫或是repo
中,並寫清楚readme.md
讓未來的自己或是同伴可以理解,這才是比較好的做法。
Aries: 這樣說也是,未來據說這個API也可能會被用在android APP
上,那我跟github copilit
來一起撰寫一個java
版本的範例好了!
Sam: 那就麻煩你啦~
註:在
Java
的版本中,筆者與同事其實還有踩到另外一個雷,那是關於Java
版本對於GMT format 協議的理解似乎不太一樣,因此在取得時間格式時同事一直沒辦法搞定。當初是透過
grafana
中的x-date
欄位,確定Java
輸出格式的確不同於dotnet
,才找到workaround
的方式。這部分筆者就不在系列文中贅述,留給讀者們如果遇到這場景時,可以根據筆者提供的線索探索看看。
圖26-5 關注x-Date
欄位的GMT