跳轉到

設定URL登入驗證

一、何謂 URL 登入驗證?

一個公司通常會有數個不同職責的系統,例如當你登入企業資源規劃系統 (ERP) 作業時,因業務需求需至 UOF X 的表單功能申請一張採購單,傳統的作法會開啟瀏覽器進入 UOF X 登入頁,接著輸入帳號密碼登入,然後從 menu 開啟表單分頁 > 申請表單 > 從表單清單中找出採購單。

這樣繁瑣的流程是否可以簡化? 可以的,我們可以把這全部的流程一次搞定,簡化後的流程只有兩個步驟,和一個操作:

  1. 從企業資源規劃系統 (ERP) 點選 按鈕或連結
  2. 自動打開瀏覽器且登入 UOF X ,並開啟空白採購單

看起來真棒,但中間的登入、找表單步驟跑去哪了? 這就是 URL登入 的功用啦!

URL登入將登入的 權柄 交給外部系統,所以只要從 已登入 的外部系統,就可以直接打開 UOF X,省去了登入的操作;並且透過 魔法導頁 的功能,直接開啟對應的功能頁面,不管是要填問券、檢視公告、簽核單據、申請表單...等,一步就到位,大大提升了使用者的操作體驗,讓作業更有效率喔。

URL 登入流程如下:

Image account-url-flow

登入流程

URL 登入驗證需要外部系統提供 Callback API ,因此需要程式碼的撰寫才能完成。 您可以透過連結下載範例程式,此範例為 MVC 架構,使用的程式語言為 C#。

範例程式下載: https://github.com/

二、整合 URL 登入驗證的優點

  1. 系統無縫轉移
    整合 URL 登入驗證後,組織可以在其他系統直接開啟 UOF X。用戶不須記憶 UOF X 的帳號和密碼,也不須紀錄 UOF X 網址,簡化了用戶的登入流程。

  2. 功能導向更直接
    使用 URL 登入進入 UOF X 後,不單只是進入個人首頁,也可以直接開啟相對應的功能,例如: 直接打開空白的採購單、請假單;開啟特定的問券、公告;檢視被核決的表單內容... 等,讓一切更有效率。

三、URL 登入驗證設定

公司管理員 可以新增多組 URL 登入驗證,以及是否要啟用此設定;在新增驗證方式的設定頁面,你可以替該組驗證方式指定一個名稱,以區分其他驗證方式,命名可以是描述性的,例如「XX 系統 URL 登入」等,建議不要多個系統共用同一組設定。

💡[管理者首頁>系統管理>系統設定>帳號安全和登入>URL 登入]

Image account-url-setting1

Image account-url-setting2

請紀錄設定的 顯示名稱 (URL 登入名稱) 、 Hash Key ,會在稍後用到。

Callback URL 是要用來驗證 access-token 及取得使用者資訊的 API,如果現在無法得知可以先隨意輸入,確定後再回來修改。

Hashkey

HashKey 是一把亂數產生的金鑰,大小寫英數符號都可以,您可以自己產生,但長度建議要有 64 個字元。

四、產生進入 URL 登入頁的網址

在範例程式中,我們提供一個簡單外部系統網頁,當填入參數後按下按鈕,就可以模擬透過 URL 登入開啟 UOF X 的情境,畫面如下:

Image account-url-sampleui

模擬外部系統 URL 登入

當使用者按下 '連線到 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,主要需提供ModuleActionPayload三個參數。若 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-TokenTarget時間戳 (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 的正確性,並取得真正的使用者資訊加密回傳,此方式有一些好處:

  1. 避免在 post message 暴露太多登入資訊
  2. 二次驗證加強安全性,避免 access-token 被偽造
  3. 透過加密隱藏真正要登入的使用者

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 加密所需的 KeyIV,轉換方式如下:

  1. Key: 透過 SHA256 hash HashKey 成為 Key
  2. 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登入驗證設定 設定。