跳轉到

自訂驗證

製作登入頁

首先,我們需要一個用於進行登入驗證的網頁,在範例中提供一個簡易的畫面如下,這是很常見的登入功能,使用者輸入帳號密碼後,按下登入按鈕進行驗證。

Image auth-thirtyparty

登入頁

登入畫面 sample 位在範例中的 Views > Home > Index.cshtml

假設此登入頁網址為 https://mysystem.com.tw ,當從 UOFX 開啟 dialog 進入此登入頁時,會透過 query-string 傳遞 info 參數,這時網址實際為 https://mysystem.com.tw/uofx/login?info=xxxxxxxxxxxxx,info 參數帶有重要的資訊,不應該做任何修改,其結構如下:

{
  "DomainId": "xxx",
  "Culture": "zH-TW",
  "PathCode": "ede"
}

行動裝置的考量

使用者也可以透過 UOFX 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. 返回 UOFX

要完成登入除了 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 物件,此物件包含回傳給 UOFX 所需要資訊,接著將物件序列化成 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 ,僅處理回傳給 UOFX 的任務,在此分成兩種情境:

  1. 透過瀏覽器登入
  2. 透過 UOFX APP 登入

如是透過 瀏覽器登入,則使用 Post Message 的方式傳遞資料,須注意最後要自己關閉畫面 window.close()。如果是透過 UOFX APP 登入,因為 APP 特性需透過約定的 Postback Function 來傳遞資料,請在你的系統中同時支援此兩種情境,使用者方能透過兩種管道登入。

<script>

  //這是給 app 使用的 Postback function
  function Postback() {
      var model = @Html.Raw(Model);
      return model;
  }

  //判斷是透過 web 或 app 連線
  if (window.opener) {
      //如果是 web 則使用 postMessage 回傳資料去 UOFX
      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 的正確性,並取得真正的使用者資訊加密回傳,此方式有一些好處:

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

Note

登入頁和 Callback API 並不一定要在同一個站台

1. Callback API 規格

我們需要一個支援 web-api 的站台,並提供一個 API 給 UOFX 使用

項目 備註
method GET
URL 自訂

API 回應結果

狀態碼 備註
200 加密字串 成功回應
所有非 200 不限 失敗回應

凡是非狀態碼 200 的回應,UOFX 皆會以 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 會事先在 UOFX 中使用者身上個別設定。 再來產生要回傳的 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. 加密並回傳

在加密之前,我們需要先有一個 HashKeyHashKey 是一把亂數產生的金鑰,您可以自己產生 (長度建議要有 64 字元), 請留意 HashKey 是與 UOFX 系統約定的 專用金鑰,應避免外流或共用。

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);