自訂驗證
製作登入頁¶
首先,我們需要一個用於進行登入驗證的網頁,在範例中提供一個簡易的畫面如下,這是很常見的登入功能,使用者輸入帳號密碼後,按下登入按鈕進行驗證。
登入畫面 sample 位在範例中的 Views > Home > Index.cshtml
假設此登入頁網址為 https://mysystem.com.tw
,當從 UOF X 開啟 dialog 進入此登入頁時,會透過 query-string 傳遞 info 參數,這時網址實際為 https://mysystem.com.tw/uofx/login?info=xxxxxxxxxxxxx
,info 參數帶有重要的資訊,不應該做任何修改,其結構如下:
行動裝置的考量
使用者也可以透過 UOF X APP 進行自訂登入驗證,因此在登入畫面的設計上,請考量行動裝置的螢幕大小,以及操作的便利性。
1. 開啟頁面後保留 query-string info 資訊¶
在範例中 (Controllers > HomeController.cs
),當透過 dialog 開啟登入頁後,會先將 info 資訊另行保存,待後續登入成功後須回傳此資訊。
[HttpGet("uofx/login")]
public IActionResult Login()
{
// 從網址中接收 info 參數
if (!Request.Query.TryGetValue("info", out var info))
{
// 如果沒有 info 參數,返回錯誤
ViewBag.ErrorMessage = "info is required";
}
// 保存 info model 到 TempData,後續登入成功後須回傳
TempData["InfoModel"] = info.ToString();
return View("index");
}
2. 登入成功,產生 access-token¶
當使用者按下登入按鈕後,假如帳密驗證成功,這時需要產生一組 access-token 供後續流程使用,token 類型與內容沒有限制,但應該包含下列資訊與規範:
- 需含能代表登入帳號的資訊: 例如範例中把帳號辨識碼 (sid) 放入 token
- 要有時效性: 無時效性的 token 容易被拿來進行資安攻擊
public ActionResult Login(string username, string password)
{
//驗證帳號密碼
...
// 產生要登入的一次性帳號識別碼並放入 Token 中, callback 時用來識別身分
// (請依自己需求調整,但不建議把真實帳號放入 Token 中,因為 token 內容是公開的)
var sid = GetSid(username);
// 產生短時效 Token (可改成使用自己製作的 token)
var accessToken = TokenHelper.GenToken(sid, DateTimeOffset.Now.AddMinutes(5));
...
}
Note
範例中的 JWT Token 內容是公開的,只要拿到 token 的人都可以輕易取得其內容,因此請勿放敏感資料在其中,不過 token 含有 簽章 來避免被串改,所以不用擔心 token 的公開特性。
3. 返回 UOF X¶
要完成登入除了 access-token 外,還需要登入時 query-string 的 info 參數,可以透過 JsonSerializer.Deserialize<InfoModel>
轉換為 InfoModel
:
//驗證帳號密碼
...
//產生 access-token
...
//取得 info model
var modelJson = TempData["InfoModel"] as string;
byte[] bytes = Convert.FromBase64String(modelJson);
var infoString = Encoding.UTF8.GetString(bytes);
var infoModel = JsonSerializer.Deserialize<InfoModel>(infoString, new JsonSerializerOptions
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
PropertyNameCaseInsensitive = true
});
...
接著建立 PostMessageModel
物件,此物件包含回傳給 UOF X 所需要資訊,接著將物件序列化成 JSON 字串。範例中回傳邏輯實作在 LoginSuccess.cshtml
,因此我們將 JSON 字串 導向此頁。
//驗證帳號密碼
...
//產生 access-token
...
//取得 info model
...
//製作 PostMessage Model
var model = new PostMessageModel
{
Info = infoModel,
Token = accessToken,
Message = "TPAD login success.",
StatusCode = 200 //必須為 200
};
// 將 model 序列化成 JSON 字串
var result = JsonSerializer.Serialize(model, new JsonSerializerOptions
{
PropertyNamingPolicy = new LowercaseFirstLetterPolicy(), // 將屬性名稱第一個字母轉為小寫
WriteIndented = true
});
return View("LoginSuccess", result); //將結果導向 LoginSuccess
LoginSuccess.cshtml 並無 UI ,僅處理回傳給 UOF X 的任務,在此分成兩種情境:
- 透過瀏覽器登入
- 透過 UOF X APP 登入
如是透過 瀏覽器登入,則使用 Post Message 的方式傳遞資料,須注意最後要自己關閉畫面 window.close()
。如果是透過 UOF X APP 登入,因為 APP 特性需透過約定的 Postback Function 來傳遞資料,請在你的系統中同時支援此兩種情境,使用者方能透過兩種管道登入。
<script>
//這是給 app 使用的 Postback function
function Postback() {
var model = @Html.Raw(Model);
return model;
}
//判斷是透過 web 或 app 連線
if (window.opener) {
//如果是 web 則使用 postMessage 回傳資料去 UOF X
var model = @Html.Raw(Model);
window.opener.postMessage(model, '*');
window.close();
}else{
//如果是 app 則使用約定的 Postback function 回傳資料
Postback();
}
</script>
資料傳遞
@Html.Raw(Model)
是 ASP.NET MVC 的 Razor 語法,用於將伺服器端的 Model 物件轉換為原始的 HTML 字串,並插入到 JavaScript 變數 model 中。此作法確保資訊能正確被解析,並避免編碼問題。
製作 Callback API¶
在流程中最後會透過 Callback API 來驗證 access-token 的正確性,並取得真正的使用者資訊加密回傳,此方式有一些好處:
- 避免在 post message 暴露太多登入資訊
- 二次驗證加強安全性,避免 access-token 被偽造
- 透過加密隱藏真正要登入的使用者
Note
登入頁和 Callback API 並不一定要在同一個站台
1. Callback API 規格¶
我們需要一個支援 web-api 的站台,並提供一個 API 給 UOF X 使用
項目 | 值 | 備註 |
---|---|---|
method | GET | |
URL | 自訂 |
API 回應結果
狀態碼 | 值 | 備註 |
---|---|---|
200 | 加密字串 | 成功回應 |
所有非 200 | 不限 | 失敗回應 |
凡是非狀態碼 200 的回應,UOF X 皆會以 callback 異常處理,並在 log 中紀錄狀態碼和 response body。
在範例中 (Controllers > HomeController.cs
) 有實作 API ( [HttpGet("uofx/accountkey")]
),後續將以此進行說明。
2. 驗證 access-token¶
access-token 會在呼叫 API 時,透過 query-string 傳送,因此第一步要取得 token 並驗證其正確性
API: GET https://mycallback.com.tw/uofx/accountkey?t=xxxxxx
// 從網址中接收 token 參數 (UOFX 會以 query-string 't' 傳遞過來)
if (!Request.Query.TryGetValue("t", out var accessToken))
throw new Exception("Token not found");
// 驗證 Token,並從 Token 中取得 sid
if (!TokenHelper.VirtyfyAndGetData(accessToken, out var sid))
throw new Exception("Invalid Token");
在驗證 token 的過程中,也把之前放在 token 的帳號辨識碼 (sid) 取出
3. 取得使用者資訊¶
接著藉由帳號辨識碼 (sid) 取得使用者帳號 (accountKey),accountKey 會事先在 UOF X 中使用者身上個別設定。
再來產生要回傳的 model CallbackResponseModel
,此 model 有兩個屬性:
屬性 | 類型 | 備註 |
---|---|---|
AccountKey | string |
使用者帳號 |
Timestamp | long |
當下的時間戳 |
// 根據 sid 取得帳號
var accountKey = GetAccountBySid(sid);
// 產生要回傳的 model
var result = new CallbackResponseModel()
{
AccountKey = accountKey,
Timestamp = DateTimeOffset.UtcNow.ToUnixTimeSeconds() //務必放當下時間,會依此時間來判斷是否過期
};
時間戳
時間戳是用來確保此 api 的回應內容具有 時效性 ,避免被有心人士以同樣的內容在不同時間進行非法登入。
4. 加密並回傳¶
在加密之前,我們需要先有一個 HashKey
,HashKey
是一把亂數產生的金鑰,您可以自己產生 (長度建議要有 64 字元),
請留意 HashKey
是與 UOF X 系統約定的 專用金鑰,應避免外流或共用。
API 的回應 (response) 內容 (body) 需要先透過 AES 加密,其規格如下:
項目 | 類型 | 備註 |
---|---|---|
加密演算法 | AES(Advanced Encryption Standard) | |
填充模式 | PKCS7 | |
加密模式 | CBC(Cipher Block Chaining) | |
反饋大小 | 128 位元 |
接下來要透過 HashKey
轉換成 AES 加密所需的 Key
和 IV
,轉換方式如下:
- Key: 透過 SHA256 hash
HashKey
成為 Key - IV: 取 Key 的後 16 個 byte 作為 IV
// 使用 AES 加密 model
byte[] aeskeyBytes = HashHelper.SHA256ToBytes(_HashKey);
var aesKey = aeskeyBytes; // HashKey 的 SHA256 Hash
var aesIv = aeskeyBytes.Skip(16).ToArray(); // HashKey 的 SHA256 Hash 後 16 bytes
var encodeResult = AesHelper.EncodeData(JsonSerializer.Serialize(result), aesKey, aesIv);
return Ok(encodeResult);