設定URL登入驗證
一、何謂 URL 登入驗證?¶
一個公司通常會有數個不同職責的系統,例如當你登入企業資源規劃系統 (ERP) 作業時,因業務需求需至 UOF X 的表單功能申請一張採購單,傳統的作法會開啟瀏覽器進入 UOF X 登入頁,接著輸入帳號密碼登入,然後從 menu 開啟表單分頁 > 申請表單 > 從表單清單中找出採購單。
這樣繁瑣的流程是否可以簡化? 可以的,我們可以把這全部的流程一次搞定,簡化後的流程只有兩個步驟,和一個操作:
- 從企業資源規劃系統 (ERP) 點選 按鈕或連結
- 自動打開瀏覽器且登入 UOF X ,並開啟空白採購單
看起來真棒,但中間的登入、找表單步驟跑去哪了? 這就是 URL登入 的功用啦!
URL登入將登入的 權柄 交給外部系統,所以只要從 已登入 的外部系統,就可以直接打開 UOF X,省去了登入的操作;並且透過 魔法導頁 的功能,直接開啟對應的功能頁面,不管是要填問券、檢視公告、簽核單據、申請表單...等,一步就到位,大大提升了使用者的操作體驗,讓作業更有效率喔。
URL 登入流程如下:
URL 登入驗證需要外部系統提供 Callback API ,因此需要程式碼的撰寫才能完成。 您可以透過連結下載範例程式,此範例為 MVC 架構,使用的程式語言為 C#。
範例程式下載: https://github.com/
二、整合 URL 登入驗證的優點¶
-
系統無縫轉移:
整合 URL 登入驗證後,組織可以在其他系統直接開啟 UOF X。用戶不須記憶 UOF X 的帳號和密碼,也不須紀錄 UOF X 網址,簡化了用戶的登入流程。 -
功能導向更直接:
使用 URL 登入進入 UOF X 後,不單只是進入個人首頁,也可以直接開啟相對應的功能,例如: 直接打開空白的採購單、請假單;開啟特定的問券、公告;檢視被核決的表單內容... 等,讓一切更有效率。
三、URL 登入驗證設定¶
公司管理員 可以新增多組 URL 登入驗證,以及是否要啟用此設定;在新增驗證方式的設定頁面,你可以替該組驗證方式指定一個名稱,以區分其他驗證方式,命名可以是描述性的,例如「XX 系統 URL 登入」等,建議不要多個系統共用同一組設定。
[管理者首頁>系統管理>系統設定>帳號安全和登入>URL 登入]
請紀錄設定的 顯示名稱 (URL 登入名稱) 、 Hash Key ,會在稍後用到。
Callback URL 是要用來驗證 access-token 及取得使用者資訊的 API,如果現在無法得知可以先隨意輸入,確定後再回來修改。
Hashkey
HashKey
是一把亂數產生的金鑰,大小寫英數符號都可以,您可以自己產生,但長度建議要有 64 個字元。
四、產生進入 URL 登入頁的網址¶
在範例程式中,我們提供一個簡單外部系統網頁,當填入參數後按下按鈕,就可以模擬透過 URL 登入開啟 UOF X 的情境,畫面如下:
當使用者按下 '連線到 UOF X' 按鈕,則會依序進行下列事項:
請查看範例
Controllers > HomeController.cs
1、產生 access-token¶
首先需要產生一組 access-token 供後續 callback 使用,token 類型與內容沒有限制,但應該包含下列資訊與規範:
- 需含能代表登入帳號的資訊: 例如範例中把帳號辨識碼 (sid) 放入 token
- 要有時效性: 無時效性的 token 容易被拿來進行資安攻擊
[HttpPost]
public ActionResult ButtonClick(HomeViewModel body)
{
...
// 產生要登入的一次性帳號識別碼並放入 Token 中, callback 時用來識別身分
// (請依自己需求調整,但不建議把真實帳號放入 Token 中,因為 token 內容是公開的)
var sid = GetSid(uofxAccount);
// 產生短時效 Token (可改成使用自己製作的 token)
var accessToken = TokenHelper.GenToken(sid, DateTimeOffset.Now.AddMinutes(5));
...
}
Note
範例中的 JWT Token 內容是公開的,只要拿到 token 的人都可以輕易取得其內容,因此請勿放敏感資料在其中,不過 token 含有 簽章 來避免被串改,所以不用擔心 token 的公開特性。
2、設定魔法連結¶
魔法連結是指 URL model 中要前往的目標位置 target,target 的類型可自訂也可套用範例中的MagicLinkModel
,主要需提供Module
、Action
與Payload
三個參數。若 target 設定為null
會前往使用者大廳,若要前往其他目標可按照下列參數類型進行設定,最後提供給 URL model 中的 target 時,再進行 JSON Serialize 的轉換,target 接受的參數型別為string
。
MagicLinkModel target = new MagicLinkModel()
{
Module: ModuleType.Bpm,
Action: BpmActionType.Apply,
Payload: { formCode: "表單代號" }
}
目標 | Module | Action | Payload |
---|---|---|---|
申請表單 | ModuleType.Bpm |
BpmActionType.Apply |
{ formCode: "表單代號" } |
簽核表單 | ModuleType.Bpm |
BpmActionType.Sign |
{ formSn: "表單編號" } |
Note
若設定的目標參數不正確或找不到編號單號等狀況,會轉至 404 頁面,若發生此狀況,請再次檢查設定的目標位置是否正確。
3、產生 URL Model¶
URL model 需要下列資訊,公司代碼
、URL 登入名稱
、access-Token
、Target
和 時間戳
(Timestamp),時間戳請放當下時間,在 UOF X 會對時間戳進行驗證,確保此連結具有時效性,避免被濫用。
因為要將 URL model 放進 query-string,所以還需要把 model 用 base64 編碼
// 產生短時效 Token (可改成使用自己製作的 token)
...
// 進入 UOF X 後要前往的頁面
...
// 產生 URL Model 並使用 Base64 編碼
var urlLoginModel = new UrlLoginModel()
{
CorpCode = uofxCorpCode, //公司代碼
UrlLoginName = uofxUrlLoginName, //URL 登入名稱
Token = accessToken, //access-Token
Target = target == null ? null : JsonSerializer.Serialize(target), //進入 UOF X 後要前往的頁面
Timestamp = DateTimeOffset.UtcNow.ToUnixTimeSeconds()
};
var urlLoginJsonString = JsonSerializer.Serialize(urlLoginModel);
var urlLoginBase64 = Convert.ToBase64String(Encoding.UTF8.GetBytes(urlLoginJsonString));
4、製作 hash¶
接下來要製作 hash,避免 URL Model 內容被修改,請使用 HashKey
把 URL Model 進行 hash,使用的規格為 HMACSHA-256
HMAC(Hash-based Message Authentication Code)
// 進入 UOF X 後要前往的頁面
...
// 產生 URL Model 並使用 Base64 編碼
...
// 產生 Hash
/*
哈希演算法:SHA-256
金鑰哈希演算法:HMAC(Hash-based Message Authentication Code)
編碼:使用 UTF-8 編碼將輸入字串和金鑰轉換為位元組
輸出格式:將哈希結果轉換為十六進制字串
*/
var hashString = HashHelper.HMACSHA256(urlLoginBase64, _HashKey);
Note
hash 結果請勿去除連字符 '-',保留原樣即可,例 0e-aa-c1-f6-d6-36...
。
5、組合出網址¶
最後我們將 URL Model 的 base64 字串,以及 hash 的結果放入 query string,並組出完整的網址。
{UOF X 站台網址}/UniversalLink/url?p={url model base64 字串}&h={hash}
// 產生 URL Model 並使用 Base64 編碼
...
// 產生 Hash
...
// 要開啟 UOF X 的 URL
var fullUrl = new Uri($"{uofxUrl}/UniversalLink/url?p={Uri.EscapeDataString(urlLoginBase64)}&h={Uri.EscapeDataString(hashString)}").ToString();
return Redirect(fullUrl);
Note
請使用 Uri.EscapeDataString
來確保 query string 不會出現不適當的字元。
五、建立 Callback API¶
在流程中最後會透過 Callback API 來驗證 access-token 的正確性,並取得真正的使用者資訊加密回傳,此方式有一些好處:
- 避免在 post message 暴露太多登入資訊
- 二次驗證加強安全性,避免 access-token 被偽造
- 透過加密隱藏真正要登入的使用者
Note
Callback API 可以是一個獨立的站台,不需要跟外部系統放在一起。
1、Callback API 規格¶
我們需要一個支援 web-api 的站台,並提供一個 API 給 UOF X 使用
項目 | 值 | 備註 |
---|---|---|
method | GET | |
URL | 自訂 | 需與 URL 登入驗證設定相同 |
API 回應結果
狀態碼 | 值 | 備註 |
---|---|---|
200 | 加密字串 | 成功回應 |
所有非200 | 不限 | 失敗回應 |
凡是非狀態碼 200 的回應,UOF X 皆會以 callback 異常處理,並在 log 中紀錄狀態碼和 response body。
在範例中 (Controllers > HomeController.cs
) 有實作 API ( [HttpGet("/url/callback")]
),後續將以此進行說明。
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、加密並回傳¶
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);
恭喜你已經完成全部的 URL 登入流程,如果你還沒設定正確的 Callback URL,請記得回 URL登入驗證設定 設定。