跳轉到

自訂驗證

製作登入頁

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

Image auth-thirtyparty

登入頁

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

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

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

行動裝置的考量

使用者也可以透過 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 的任務,在此分成兩種情境:

  1. 透過瀏覽器登入
  2. 透過 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 的正確性,並取得真正的使用者資訊加密回傳,此方式有一些好處:

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

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. 加密並回傳

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

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