OIDC擴展功能2:自定義掃碼登錄

在中央授權中心OIDC功能推廣過程,發現有如下情況:

  • 在集團某些系統中可能會有一些單位賬號,這些賬號可能由幾個員工共同管理。
  • 有時同一個員工在某個系統中又有多個單位賬號。
  • 無權限的員工可能需要臨時借用別的員工單位賬號登錄系統。

為了使中央授權中心支持此業務場景,特提供此擴展功能。主要步驟為:

  • 相信手機端掃PC端的二維碼,進行一些自定義操作,如:綁定此次登錄的單位賬號,或提交此次登錄單位賬號的申請。
  • PC端監控登錄是否完成,登錄完成之后進入第三方系統。

此功能實現主要包括:PC端、相信手機端兩個OIDC授權功能;中央資訊云緩存服務;以及前端的展示功能。

流程時序圖如下:

第三方系統,僅需準備中央授權心的一個OIDC客戶端賬號并注冊兩個跳轉地址以及對應的state值、scope值,一個登錄界面。

  • client_id、client_secret
  • 手機端RedirectUrl、ScopeA、StateA
  • PC端RedirectUrl、ScopeB、StateB
  • PC端登錄界面


一. 登錄界面處理

//Url中的換行僅為了提高可讀性,實際代碼中應去掉。
<script src="https://sso.foxconn.com/lib/jquery/dist/jquery.min.js"></script>
<script src="https://sso.foxconn.com/Civet/CustomLogin/LoginJs?
    clientId=[您的client_id]&
    MobileRedirectUrl=[手機端RedirectUrl]&
    MobileScope=[手機端RedirectUrl對應的Scope,可為空,默認為:openid civet.ext.custom_scan;此無此兩個Scope時會自動加上]&
    MobileState=[手機端RedirectUrl對應的State,可為空]&
    PCRedirectUrl=[PC端RedirectUrl]&
    PCScope=[PC端RedirectUrl對應的Scope,可為空,默認為:openid profile foxconn]&
    PCState=[PC端RedirectUrl對應的State,可為空]
    &PCClientId=[PC端ClientId,可為空,默認為前面clientId值相同]
"></script>
<--
特別說明:若定義了PCClientId值,且與clientId值不相等,則PCScope必須包含一個clientId自定義的scope。
    舉例:clientId=channel,PCClientId=BGNo1。 
          那么 PCScope必須包含以channel開頭的scope,此scope是channel事先定義好的為 channel.login.scan并授權過的。 一般 PCScope=openid profile foxconn channel.login.scan。這樣從中央授權中心獲得的AccessToken就有權限調用channel提供的接口來解析用戶登錄的公共賬號信息。
-->
<script>
  var hsso_options =  { 
    scanPrompt: "相信掃一掃,在手機端選擇要登錄的XX系統賬號",
    imgSize: 145, 
    imgMarginTop: 10, 
    imgMarginBottom: 20 
  };
  var hsso = new HQiTSSO("#div1", hsso_options);
</script>

此功能必須引用jQuery 1.7版本或以上。

JS主要方法為: new HQiTSSO(selector, options);

  • selector
    DOM對像選擇字符串,用來指定顯示二維碼容器。

  • options
    配置參數,可以為空。json對像,可包含下列值,且均可以為空。

    • scanPrompt 提示用戶掃碼,默認值為“請使用相信「掃一掃」功能”。
    • s0Prompt 用戶已掃碼的提示,默認值為“請在相信手機端完成登錄操作”。
    • s5Prompt 已管理員審核時的提示,默認值為“等待管理人員同意”。
    • sn10Prompt 管理員拒絕時的提示,默認值為“管理人員不同意此次登錄”。
    • s10Prompt 相信手機端操作已完成時的提示,默認值為“中央授權中心正在登錄”。
    • imgSize 二維碼圖片寬度,單位px,默認值為180。
    • imgMarginTop 二維碼圖片上端空白高度,單位px,默認值為30。
    • imgMarginBottom 二維碼圖片下端空白高度,單位px,默認值為30。
    • useDefaultCsss 是否加載默認CSS樣式,默認值為true。


二. 手機端RedirectUrl處理

基本步驟:

  • JS方法HQiTSSO在頁面上生成一個redirect_uri=手機端RedirectUrl的OIDC授權端點鏈接二維碼。
  • 手機端掃碼后進入手機端RedirectUrl頁面,云緩存記錄掃碼人賬號及掃碼相關信息。
  • 手機端RedirectUrl頁面,通過OIDC的令牌端點(Token Endpoint)獲得IDToken和AccessToken,通過獲得IDToken或AccessToken搭載的信息sub值來獲得登錄者的工號。
  • 手機端RedirectUrl頁面根據用戶賬號,顯示自定義操作界面,調用接口回寫自定義操作數據。
  • 若需要其它用戶審核,可通過頻道開放接口SendMsg方法向審核人發消息。
  • 審核操作完成之后,同樣調用接口回寫自定義操作狀態。

回寫自定義操作數據的接口

//此為Restful API
請求地址:https://sso.foxconn.com/Civet/CustomLogin/WriteStatusOperation
請求方法:POST
請求格式:JSON
請求頭:Authorization: Bearer <access_token>
請求數據:
{
    "session_id": "[返回手機端RedirectUrl帶上的state值的豎線前部分]",
    "status": [狀態,可選值有:10, 5, -10],
    "data": "[自定義操作數據,HQiT SSO將原樣傳至PC端]",
    "auditor": "[審核人賬號,status=5時必填]"
}

access_token獲取地址:見令牌端點(Token Endpoint)

返回值說明

//操作成功
{status : 200, message : "OK"}

//常見異常
{status : 401, message : "Token Validation Failed。"}
{status : 400, message : "請求數據不完整session_id不能為空。"}
{status : 401, message : "請求數據status值不正確,它只能是:5, 10, -10。"}
{status : 501, message : "未知異常信息"}


三. PC端RedirectUrl處理

基本步驟:

  • 自定義操作完成之后,PC端Web前端發起一個redirect_uri=PC端RedirectUrl的OIDC授權端點請求(地址稍微有些改動),且授權中心根據云緩存數據自動完成身份驗證。
  • PC端頁面重載至PC端RedirectUrl,并在URL中state值前面加上“自定義操作數據”,以英文豎線(|)隔開。
  • 第三方系統解碼“自定義操作數據”獲得用戶自定義操作內容,如:使用XXX單位賬號。中央授權中心僅是將“自定義操作數據”原樣返回不參與加密解決處理。
  • 第三方系統通過OIDC的令牌端點(Token Endpoint)獲得IDToken,通過獲得IDToken搭載的信息custom_scan值來獲得掃碼相關信息。
  • 與OIDC驗證一樣可以使用AccessToken調用其它接口,如:UserInfo端點(UserInfo Endpoint)

掃碼信息獲取

令牌(IDToken)解析出來的custom_scan值,為base64d編碼數據,解碼后可得到如下數據

{
    st: "1677636000",  /*掃碼開始時間戳*/
    aud: "X007",     /*審核人,可以為空*/
    lt: "1677636060",  /*審核發起時間戳*/
    et: "1677636600"   /*操作完成時間戳*/
}

自定義操作數據加密建議

為了防止“自定義操作數據”被篡改,強烈建此操作數據進行加密或附加簽名處理。加密或附加簽名時應考慮將當前時間截、當前用戶工號信息加入以提高其安全性。

參考代碼如下:

  1. using System;  
  2. using System.Security.Cryptography;  
  3. using System.Text;  
  4.   
  5. namespace sso.foxconn.com  
  6. {  
  7.     class HQiTTools  
  8.     {  
  9.         static string CivetNo;  //當前用戶個人賬號  
  10.         const string EncryptKey = "02EA80E7BFBE49279923127C04FD2418";  
  11.         static void Main(string[] args)  
  12.         {  
  13.             CivetNo = "X007";  //當前用戶個人賬號  
  14.             string DeptAccountNo = "HQiT_PMO"//部門賬號  
  15.             //Data編碼  
  16.             string data = DataEncrypt(DeptAccountNo);  
  17.             string deptAccNo = DataDecrypt(data);  
  18.             Console.WriteLine("編碼結果:" + data);  
  19.             if (ErrorMsg == "")  
  20.             {  
  21.                 Console.WriteLine("解碼結果:" + deptAccNo);  
  22.             }  
  23.             else  
  24.             {  
  25.                 Console.WriteLine("解碼錯誤:" + ErrorMsg);  
  26.             }  
  27.             Console.ReadKey();  
  28.         }  
  29.   
  30.         /// <summary>  
  31.         /// Data編碼  
  32.         /// </summary>  
  33.         /// <param name="DeptAccountNo"></param>  
  34.         /// <returns></returns>  
  35.         public static string DataEncrypt(string DeptAccountNo)  
  36.         {  
  37.             long timestamp = (DateTime.UtcNow.Ticks - 621355968000000000) / 10000000;  
  38.             string data = timestamp + "." + DeptAccountNo;  
  39.             data += "." + ToMD5(data + CivetNo + EncryptKey);  
  40.             byte[] bytes = Encoding.UTF8.GetBytes(data);  
  41.             return Convert.ToBase64String(bytes).Replace("+""-").Replace("/""_").Trim('=');  
  42.         }  
  43.   
  44.         static string ErrorMsg = "";  
  45.         /// <summary>  
  46.         /// Data解碼  
  47.         /// </summary>  
  48.         /// <returns></returns>  
  49.         public static string DataDecrypt(string data) {  
  50.             string[] strs = null;  
  51.             if (!string.IsNullOrWhiteSpace(data))  
  52.             {  
  53.                 string text = data.Replace("-""+").Replace("_""/");  
  54.                 int num = text.Length % 4;  
  55.                 if (num != 0)  
  56.                 {  
  57.                     text += new string('=', 4 - num);  
  58.                 }  
  59.                 try  
  60.                 {  
  61.                     strs = Encoding.UTF8.GetString(Convert.FromBase64String(text)).Split('.');  
  62.                 }  
  63.                 catch { }  
  64.             }  
  65.             if (strs == null || strs.Length != 3)  
  66.             {  
  67.                 ErrorMsg = "輸入數據錯誤";  
  68.                 return null;  
  69.             }  
  70.   
  71.             //創建時間  
  72.             DateTime createTime = DateTime.MinValue;  
  73.             try  
  74.             {  
  75.                 createTime = new DateTime(long.Parse(strs[0]) * 10000000 + 621355968000000000);  
  76.             }  
  77.             catch { }  
  78.             if (Math.Abs((DateTime.UtcNow - createTime).TotalMinutes) > 10)  
  79.             {  
  80.                 ErrorMsg = "數據過期";  
  81.                 return null;  
  82.             }  
  83.             if (ToMD5(strs[0] + "." + strs[1] + CivetNo + EncryptKey) != strs[2])  
  84.             {  
  85.                 ErrorMsg = "簽名不正確";  
  86.                 return null;  
  87.             }  
  88.             return strs[1];  
  89.         }  
  90.   
  91.         /// <summary>  
  92.         /// MD5加密  
  93.         /// </summary>  
  94.         /// <param name="input"></param>  
  95.         /// <returns></returns>  
  96.         static string ToMD5(string input)  
  97.         {  
  98.             byte[] bytes = Encoding.UTF8.GetBytes(input);  
  99.             byte[] hash = null;  
  100.             using (HashAlgorithm hashAlgorithm = MD5.Create())  
  101.             {  
  102.                 hash = hashAlgorithm.ComputeHash(bytes);  
  103.             }  
  104.             return BitConverter.ToString(hash).Replace("-""");  
  105.         }  
  106.   
  107.     }  
  108. }  

復制代碼,在線運行一下

方法擴展

若第三方系統由多個微服務組成,且所有微服務都集成了中央身份授權中心單點登錄驗證。 在使用此“自定義掃碼登錄”功能時,考慮減少手機端掃碼操作,可重新生成一個“自定義操作數據”值放至在PC端授權鏈接中。

授權端點地址:

//Url中的換行僅為了提高可讀性,實際代碼中應去掉。
https://sso.foxconn.com/connect/authorize?
    client_id=[您的client_id]&
    scope=[PC端的Scope]&
    response_type=code&
    redirect_uri=[PC端RedirectUrl]&
    state="[自定義操作數據]|[其它狀態信息]"


四. 在頻道賬號的應用

這里只是說明此功能在頻道賬號上的一個應用案例。

這里涉及到四類參與者:

  • 相信個人用戶
  • 中央授權中心
  • 第三方系統:頻道/頻道賬號系統,它的client_id=channel。
  • 第四方系統:需使用頻道賬號的某個功能系統,假設它的client_id=functionSystem001。

第四方系統登錄界面處理

如前文所說,配合頻道/頻道賬號系統設定,登錄界面內容如下。

//Url中的換行僅為了提高可讀性,實際代碼中應去掉。
<script src="https://sso.foxconn.com/lib/jquery/dist/jquery.min.js"></script>
<script src="https://sso.foxconn.com/Civet/CustomLogin/LoginJs?
    clientId=channel&
    MobileRedirectUrl=https://believe.foxconn.com/mp_login/ChannelSelector.html&
    MobileState=[可選項/可為空,值有:roleIdGt0、roleIdGt1,分別表示不许無權用戶臨時登錄、只许擁有者登錄]&
    PCClientId=[functionSystem001, 您的client_id]&
    PCRedirectUrl=[PC端RedirectUrl]&
    PCScope=openid profile foxconn channel.login.scan&
    PCState=[PC端RedirectUrl對應的State,可為空]
"></script>
<script>
  var hsso_options =  { 
    scanPrompt: "相信掃一掃,在手機端選擇要登錄的相信頻道賬號",
    imgSize: 145, 
    imgMarginTop: 10, 
    imgMarginBottom: 20 
  };
  var hsso = new HQiTSSO("#div1", hsso_options);
</script>

注:涉及的scope開通可與相信團隊聯系開通。


第四方系統獲得頻道的賬號信息

使用前文所述的“PC端RedirectUrl處理”:
1)獲得state值英文豎線(|)前的“自定義操作數據”;
2)獲得AccessToken。

使用上面得到內容,請求頻道賬號系統獲得頻道賬號信息。

POST https://believe.foxconn.com/mp_login/channelinfo
  Authorization: Bearer <access_token>
  Content-Type: application/x-www-form-urlencoded
    
    login_data=<自定義操作數據>&
    lang=tw     //支持cn/tw/en/jp

返回結果


{
    "data": {
        "uid": "CivetDeve",
        "name": "生活頻道開發指南",
        "icon": "https://icivetmedia.foxconn.com/group1/M00/48/69/CoaWx1p7x8uAKsf3AABo84SjADM518.jpg",
        "roleId": 2
    },
    "code": 200,
    "msg": "OK"
}

// roleId,值有: 0/1/2,分別表示臨時同意登錄、運營人員登錄、頻道擁有者登錄。
//    其中,roleId=0時強列建議授權登錄時檢查掃碼信息確認登錄同意人的賬號,或不予登錄。