portal掃碼登入重構----對外版本

codestacklinuxer發表於2024-03-19

瀏覽器訪問的url:

  • http://192.168.1.207:8887/ui/wls_portal.html?nasip=192.168.1.249 發生302
  • 302 到http://192.168.1.207:8887/api/open/nps/v1/get_qr_portal_source?nasip=192.168.1.249&auth_type=employee_wireless 發生302
  • https://idp.pre.xxxxxxxxxx.cn/idp/app/login?app_id=xxxxx&ins_id=xxxxxxxxxxxxx5f84ba1e65e7&portal_type=xxxxxxxxxxx&redirect_url=http%3A%2F%2F192.168.1.207%3A8887%2Fapi%2Fopen%2Fnps%2Fv1%2Flogin%2Fverify_authtickets%3Fnasip%3D192.168.1.249%26auth_type%3Demployee_wireless
  • 302 https://idp.pre.xxxxx.cn/ui/idp-auth.html?app_id=xxxxx&redirect_url=http%3A%2F%2F192.168.1.207%3A8887%2Fapi%2Fopen%2Fnps%2Fv1%2Flogin%2Fverify_authtickets%3Fnasip%3D192.168.1.249%26auth_type%3Demployee_wireless&portal_type=nac_portal&ins_id=sxxxxx
  • 使用cookie 認證https://idp.xx.xxxxx.cn/idp/app/authenticate?app_id=xxxx&redirect_url=http%3A%2F%2F192.168.1.207%3A8887%2Fapi%2Fopen%2Fnps%2Fv1%2Flogin%2Fverify_authtickets%3Fnasip%3D192.168.1.249%26auth_type%3Demployee_wireless&portal_type=nac_portal&ins_id=xxxxx返回200ok (沒有認證過只有200 沒有重定向到提交賬密環節)
  • 獲取身份源https://idp.pre.xxxxxx.cn/idp/app/get_iden_source_list?app_id=xxxxxx&redirect_url=http%3A%2F%2F192.168.1.207%3A8887%2Fapi%2Fopen%2Fnps%2Fv1%2Flogin%2Fverify_authtickets%3Fnasip%3D192.168.1.249%26auth_type%3Demployee_wireless&portal_type=xxxx&ins_id=xxxxxxxxx
  • 選擇身份源出現二維碼;https://idp.xxxx.xxxxxxxx.cn/idp/app/login?app_id=xxxxxx&redirect_url=http%3A%2F%2F192.168.1.207%3A8887%2Fapi%2Fopen%2Fnps%2Fv1%2Flogin%2Fverify_authtickets%3Fnasip%3D192.168.1.249%26auth_type%3Demployee_wireless&portal_type=xxxx&ins_id=xxxxx&source_id=xxx此時傳送重定向302
  • 302 到https://idp.pre.xxxx.cn/ui/nac-login.html?osType=windows&requestId=xxxxxx8&idp_type=4&appid=xxxxx&corp_code=&portal_type=xxxxx&redirect_uri=https%3A%2F%2Fidp.pre.xxx.cn%2FdingtalkLogin&authentication_error=
  • 瀏覽器定時https://idp.pre.xxxxx.cn/idp/app/check_nac_auth?requestId=xxxxxxxx8&requestType=refresh
  • 如果refresh 返回的data 資料為true則訪問https://idp.pre.xxxxx.cn/idp/app/check_nac_auth?requestId=xxxxxxxc8&requestType=xx否則訪問以前的requestType=refresh
  • 掃碼成功後;https://idp.pre.xxxxx.cn/idp/app/check_nac_auth?requestId=xxxxx&requestType=submit 後會302
  • 302到http://192.168.1.207:8887/api/open/nps/v1/login/verify_authtickets?nasip=192.168.1.249&auth_type=employee_wireless&xxxx_ticket=axxxxx
  • 此時到管控置換秘鑰 根據ticket置換 掃描是idp 服務儲存的身份資訊使用者名稱密碼
  • 拿到使用者名稱密碼後 發起標準的portal 認證

手機端掃碼邏輯:

掃碼後第一個訪問的是:

  • https://idp.pre.eagleyun.cn/ui/nac-qrcode-confirm.html?osType=windows&requestId=xxxxxx&idp_type=4&appid=xxxxx&corp_code=&portal_type=xxx&redirect_uri=https%3A%2F%2Fidp.pre.eagleyun.cn%xxxxxx&authentication_error=
  • https://login.dingtalk.com/oauth2/auth?redirect_uri=https://idp.pre.xxx.cn/dingtalkLogin&response_type=code&client_id=xxxx&scope=openid&state=sxxxxx&prompt=consent
  • https://login.dingtalk.com/oauth2/challenge.htm?redirect_uri=https://idp.pre.xxxxxx.cn/dingtalkLogin&response_type=code&client_id=xxxx&scope=openid&state=xxxxxx&prompt=consent
  • https://login.dingtalk.com/oauth2/confirm_auth
  • https://idp.pre.eagleyun.cn/dingtalkLogin?code=xxxx&authCode=xxxxx&state=xxxxxxx
  • https://idp.pre.eagleyun.cn/ui/loginSuccess.html
  • 訪問/ui/loginSuccess.html 彈出成功頁面

idp-server中監聽dingtalkLogin ;並且根據code 獲取user 身份資訊

上述跟蹤資料的時候requestId 和 state 的值都是一樣的(擷取資料時 分兩次擷取的)

目前釘釘提供的頁面登入授權最新為https://open.dingtalk.com/document/orgapp/logon-free-third-party-websites

授權登入地址:

https://login.dingtalk.com/oauth2/auth?
redirect_uri=https%3A%2F%2Fwww.aaaaa.com%2Fauth
&response_type=code
&client_id=dingxxxxxxx   //應用的AppKey 
&scope=openid   //此處的openId保持不變
&state=dddd
&prompt=consent

對應手機掃碼授權如下:

https://login.dingtalk.com/oauth2/auth?redirect_uri=https://idp.pre.xxxx.cn/dingtalkLogin&response_type=code&client_id=xxxx
&scope=openid&state=saml_idp_xxxxxx4c6&prompt=consent

portal掃碼登入重構----對外版本

//原始url為:
//https://login.dingtalk.com/oauth2/auth?redirect_uri=https://idp.pre.xxxx.cn/dingtalkLogin&response_type=code
//&client_id=xxxxx&scope=openid&state=saml_idp_xxxxxx&prompt=consent

https://login.dingtalk.com/oauth2/confirm_auth
post

//payload資料為:
redirect_uri: https://idp.pre.xxxx.cn/dingtalkLogin
response_type: code
client_id: xxxxxxxx
scope: openid
state: saml_idp_xxxxxxx
prompt: consent
corpId: 
secondaryValidationResult: 
pdmTitle: Windows  Web
pdmModel: Windows 
pdmToken: T2gAuk9w68QMeg5zmmi2e46Pun3D00T9BkAGb1uK5kDoPPqhJK3-bqNYJJm69pYlLcA=

正常的掃碼授權登入,

授權後:

https://xxxx/callback?code=fb6a88dc09e843b33f 釘釘透過此回撥通知伺服器 給一個臨時code;

伺服器透過臨時code ,POST 請求 獲取令牌 token/當獲取到授權碼 code 後,客戶端需要用它獲取訪問令牌:

// 獲取登入使用者的訪問憑證; 獲取使用者token; 獲取使用者資訊
	dingTalkUserToken, err := GetDingTalkTokenByCode(idenSource.SyncUser, idenSource.SyncPwd, code)
	if err != nil {
		log.Errorf("[LoginWithDingtalkCode]authByDingtalk failed, code:%s, idenSource:%+v, err:%+v", code, idenSource, err)
		return nil, err
	}

	var dingtalkGetAppAccessTokenResp dingtalkGetAppAccessTokenRespInfo
	var dingtalkGetAppAccessTokenUrl = "https://oapi.dingtalk.com/gettoken"
	err = gout.GET(dingtalkGetAppAccessTokenUrl).SetQuery(gout.H{
		"appkey":    idenSource.SyncUser,
		"appsecret": idenSource.SyncPwd,
	}).BindJSON(&dingtalkGetAppAccessTokenResp).SetTimeout(30 * time.Second).Do()
	if err != nil {
		log.Errorf("[LoginWithDingtalkCodeNac] appkey:%s, appsecret:%s, error %+v", idenSource.SyncUser, idenSource.SyncPwd, err)
		return nil, errors.New(ErrAppSsoUserAuthForDingtalk)
	}
	userId, err := GetUserByCode(dingtalkGetAppAccessTokenResp.AccessToken, dingTalkUserToken)

// 掃碼免登Demo回撥,完整的訪問路徑:http://127.0.0.1:3000/callback-for-dingtalk
router.get('/callback-for-dingtalk', function(req, res, next) {

    // Step 1:參考如下文件,獲取當前登入使用者的授權Token
    // 文件:https://open.dingtalk.com/document/orgapp-server/obtain-user-token
    DingtalkClient.request({
        path:'/v1.0/oauth2/userAccessToken',
        method:'POST',
        body:{
            clientId:'<這裡替換成你自己應用的App Key>',
            clientSecret: '<這裡替換成你自己應用的App Secret>',
            code:req.query.authCode,
            grantType:'authorization_code'
        }
    }).then(data => {
        // Step 2:參考如下文件,獲取當前登入使用者的個人資訊
        // 文件:https://open.dingtalk.com/document/orgapp-server/dingtalk-retrieve-user-information
        return DingtalkClient.request({
            path:'/v1.0/contact/users/me',
            headers: {
                'x-acs-dingtalk-access-token': data.accessToken
            }
        });
    }).then(data => {
        // Step 3:根據獲取到的使用者授權資訊,做相關業務操作
        res.setHeader("Content-Type", "application/json;charset=utf-8");
        res.end(JSON.stringify({
            message:'下面這些內容就是透過免登獲取到的個人詳細資訊了,如果當前已經有賬號登入了,可以直接獲取到資料',
            data:data   // 這裡就是拿到的結果了!!
        },null,4));
    });
});
你可能注意到了上門示例程式碼中的DingtalkClient.request ,不知道這是什麼,這實際上是釘釘OpenApi的簡單訪問工具,工具定義如下:

 
// 釘釘OpenApi的訪問工具
// 釘釘OpenApi的訪問工具
// github: https://github.com/zxlie/dingtalk-client
let DingtalkClient = {
    /**
     * 釘釘OpenAPI的統一呼叫入口
     * @param {object} configs 請求配置
     * @p-config {boolean}  isOldApi 是否老版本介面,預設 false
     * @p-config {string}   method   請求方式,GET或者POST
     * @p-config {string}   path     介面地址,如:/v1.0/sso/getToken
     * @p-config {object}   params   需要在介面地址上進行URL拼接的引數
     * @p-config {object}   body     請求方式為POST時,指定提交的內容
     * @p-config {object}   headers  請求頭
     * @p-config {function} callback 請求結果的處理回撥:function(error,data)
     * @returns  {promise}  返回一個Promise物件
     **/
    request : function(configs) {
        var url = ((isOldApi, path, params) => {
            let _hostConfig = {
                topHost: 'https://oapi.dingtalk.com', // 老版API
                popHost: 'https://api.dingtalk.com'   // 新版API
            };
            let oUrl = new URL((isOldApi ? _hostConfig.topHost : _hostConfig.popHost) + path);

            if (Object.keys(params || {}).length > 0) {
                Object.keys(params, key => oUrl.searchParams.append(key,params[key]));
            }
            return oUrl.toString();
        })(configs.isOldApi, configs.path, configs.params);
        var obj = {
            method:  (configs.method || 'GET').toUpperCase(),
            url: url,
            json: true
        };
        if(obj.method === 'POST' && configs.body) {
            obj.body = configs.body;
        }
        if(configs.headers) {
            obj.headers = configs.headers;
        }
        return new Promise((resolve,reject) => {
            request(obj, (err, response, body) => {
                resolve(body);
            });
        });
    }
};

portal掃碼登入重構----對外版本

也就是伺服器可以拿到此時一些資料

此時瀏覽器定時的向伺服器查詢是否授權成功; 授權成功會返回ticket給前端;

前端將ticket給准入portal server,

portal server 透過ac radius 到管控走認證流程。最後管控拿ticket呼叫查詢介面獲取使用者資訊去過策略;獲取使用者資訊的介面:idpHost:/idp/app/get_user_info

過策略成功 給radius返回200ok---->radius-----ac----portal-server-顯示成功

部分參考:https://help.aliyun.com/practice_detail/419774

相關文章