釘釘開發系列(十一)釘釘網頁掃碼登陸

在《釘釘開發系列(八)二維碼掃描登陸的實現》介紹了一種掃碼登陸的方式,該方式是本身產生二維碼,二維碼中的URL指到自身的服務器頁面,在該頁面中以JSSDK的方式來獲取釘釘用戶的信息。釘釘官方提供了另外兩種掃碼登陸的方式,能夠參見釘釘官網javascript

先申請獲取相應的appid和appsecret,而後架設一個服務端,好比有頁面ddqrlogin.aspx,而後將該頁面的URL使用URL編碼,對應到https://oapi.dingtalk.com/connect/qrconnect?appid=APPID&response_type=code&scope=snsapi_login&state=STATE&redirect_uri=REDIRECT_URI中的REDIRECT_URI,即用該URL編碼後的值替代REDIRECT_URI。而後將該URL嵌入到web頁面中。若是是winform的,能夠直接用webbrowser,將其URL設置爲前面拼成的一長串URL。同時將ScriptErrorsSuppressed設置爲false,以屏蔽JS錯誤時的彈窗,設置ScrollBarsEnabled爲false,以便於調整窗體的大小。css


同時設置DocumentCompleted事件,以便在掃描成功後,讀取返回的數據,代碼以下。html

   private void webBrowser1_DocumentCompleted(object sender, WebBrowserDocumentCompletedEventArgs e)
        {
            if (webBrowser1.Document.Url.AbsolutePath.Contains("ddqrlogin"))
            {
                var dduseridPackageJson = $"{webBrowser1.Document.InvokeScript("GetDDUserId")}";
 MessageBox.Show(dduseridPackageJson );
}
}
其中webBrowser1.Document.InvokeScript("GetDDUserId")調用的是ddqrlogin.aspx的JS函數GetDDUserId.

在服務端ddqrlogin.aspx代碼以下java

<%@ Page Language="C#" AutoEventWireup="true" CodeBehind="ddqrlogin.aspx.cs" Inherits="DingDingQRLogin.ddqrlogin" %>

<!DOCTYPE html>

<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
    <title></title>
    <script type="text/javascript">
        function GetDDUserId() {
            try {
                var hiddenField = document.getElementById("<%=HiddenFieldDDUserId.ClientID%>");
                var ddUserId = hiddenField.value;
                return ddUserId;
            } catch (e) {
                alert(e.message);
            }
        }
    </script>
</head>
<body>
    <form id="form1" runat="server">
        <asp:HiddenField ID="HiddenFieldDDUserId" runat="server" />
        <div style="width: 611px; height: 600px; background-color: #2F4F4F;position:absolute;">
            <div style="margin-left: 123px; margin-top: 74px; width: 365px; height: 292px; background-color: #F9F9F9; text-align: center; position: absolute;">
                <div id="loginResultInfo" style="position: absolute; top: 50%; left: 50%;"
                    runat="server">
                </div>
            </div>
        </div>
    </form>
</body>
</html>
服務端後臺代碼以下

 protected void Page_Load(object sender, EventArgs e)
        {
            if (!this.IsPostBack)
            {
                var tempAuthCode = Request.QueryString["code"];
                var state = Request.QueryString["state"];
                var userIdPackage = SdkTool_QRLogin.FetchDDUserIdTempAuthCode(tempAuthCode);
                HiddenFieldDDUserId.Value = userIdPackage.ToJSON();
                loginResultInfo.InnerText = (userIdPackage.IsOK()) ? "登陸成功" : userIdPackage.ErrMsg;
            }
        }
其中FetchDDUserIdTempAuthCode是獲取釘釘的用戶id,具體代碼以下。

public static class SdkTool_QRLogin
    {
        #region 全局變量
        /// <summary>
        /// 基於appid獲取的票據
        /// </summary>
        public static DDAppAccessToken AppAccessToken = DDAppAccessToken.GetInstance();
        #endregion

        #region UpdateQRAccessToken
        /// <summary>
        /// 更新應用票據
        /// </summary>
        /// <returns></returns>
        public static void UpdateAppAccessToken(bool forceUpdate = false)
        {
            if (!forceUpdate && !AppAccessToken.IsExpired())
            {//沒有強制更新,而且沒有超過緩存時間  
                return;
            }

            string appId = ConfigTool.FetchAppId();
            string appSecret = ConfigTool.FetchAppSecret();
            string TokenUrl = QRUrls.SNS_GET_TOKEN;
            string apiurl = $"{TokenUrl}?{QRKeys.appid}={appId}&{QRKeys.appsecret}={appSecret}";
            DDTokenPackage tokenResult = DDRequestAnalyzer.Get<DDTokenPackage>(apiurl);
            if (tokenResult.IsOK())
            {
                AppAccessToken.Value = tokenResult.Access_token;
                AppAccessToken.Begin = DateTime.Now;
            }
        }
        #endregion

        #region FetchPersistentCode Function   
        /// <summary>
        /// 獲取持久受權碼
        /// </summary>
        /// <param name="tempAuthCode"></param>
        /// <returns></returns>
        public static DDPersistentCode FetchPersistentCode(string tempAuthCode)
        {
            string apiUrl = FormatApiUrlWithAppToken(QRUrls.SNS_GET_PERSISTENT_CODE);
            var data = new { tmp_auth_code = tempAuthCode };
            DDPersistentCode result = DDRequestAnalyzer.Post<DDPersistentCode>(apiUrl, data.ToJSON());
            return result;
        }
        #endregion

        #region FetchSnsToken Function   
        /// <summary>
        /// 獲取SNS票據
        /// </summary>
        /// <param name="openId"></param>
        /// <param name="persistentCode"></param>
        /// <param name="forceUpdate"></param>
        public static DDSnsToken FetchSnsToken(string openId, string persistentCode, bool forceUpdate)
        {
            string apiUrl = FormatApiUrlWithAppToken(QRUrls.SNS_GET_SNS_TOKEN);
            var data = new
            {
                openid = openId,
                persistent_code = persistentCode
            };
            DDSnsToken SnsToken = new DDSnsToken();
            var result = DDRequestAnalyzer.Post<DDSnsToken>(apiUrl, data.ToJSON());
            if (result.IsOK())
            {
                SnsToken.ExpiresIn = result.ExpiresIn;
                SnsToken.Value = result.Value;
                SnsToken.Begin = DateTime.Now;
            }
            return SnsToken;
        }
        #endregion


        #region FetchUserInfo Function
        /// <summary>
        /// 基於臨時獲權碼獲取用戶信息
        /// </summary>
        /// <param name="tempAuthCode">臨時受權碼</param>
        /// <returns></returns>
        public static DDSnsUserInfo FetchUserInfo(string tempAuthCode)
        {
            var persistentCodePackage = FetchPersistentCode(tempAuthCode);
            DDSnsUserInfo snsUserInfoPackage = new DDSnsUserInfo();
            if (!persistentCodePackage.IsOK())
            {
                snsUserInfoPackage.ErrCode = DDErrCodeEnum.Unknown;
                snsUserInfoPackage.ErrMsg = $"使用tempAuthCode({tempAuthCode})獲取";
                return snsUserInfoPackage;
            }

            var snsToken = FetchSnsToken(persistentCodePackage.OpenId, persistentCodePackage.PersistentCode, false);
            string apiUrl = $"{QRUrls.SNS_GET_USER_INFO}?{QRKeys.sns_token}={snsToken.Value}";
            snsUserInfoPackage = DDRequestAnalyzer.Get<DDSnsUserInfo>(apiUrl);
            return snsUserInfoPackage;
        }
        #endregion

        #region FetchUserInfo Function
        /// <summary>
        /// 基於臨時獲取DDUserId
        /// </summary>
        /// <param name="tempAuthCode">臨時受權碼</param>
        /// <returns></returns>
        public static DDUserIdPackage FetchDDUserIdTempAuthCode(string tempAuthCode)
        {
            var snsUserInfoPackage = FetchUserInfo(tempAuthCode);
            DDUserIdPackage userIdPackage = new DDUserIdPackage();
            if (!snsUserInfoPackage.IsOK())
            {
                userIdPackage.ErrCode = snsUserInfoPackage.ErrCode;
                userIdPackage.ErrMsg = snsUserInfoPackage.ErrMsg;
                return userIdPackage;
            }
            userIdPackage = FetchDDUserIdByUnionId(snsUserInfoPackage.user_info.unionid);
            return userIdPackage;
        }
        #endregion

        #region FetchDDUserIdByUnionId Function
        /// <summary>
        /// 基於UnionId獲取DDUserId
        /// </summary>
        /// <param name="unionid">用戶在當前釘釘開放平臺帳號範圍內的惟一標識,同一個釘釘開放平臺帳號能夠包含多個開放應用,同時也包含ISV的套件應用及企業應用</param>
        /// <returns></returns>
        public static DDUserIdPackage FetchDDUserIdByUnionId(string unionid)
        {
            DDUserIdPackage userIdPackage = new DDUserIdPackage();
            var accessTokenPackage = AuthService.GetAccessToken();
            if (!accessTokenPackage.IsOK())
            {
                userIdPackage.ErrCode = DDErrCodeEnum.Unknown;
                userIdPackage.ErrMsg = accessTokenPackage.Message;
                return userIdPackage;
            }
            DDAccessToken accessTokenOfCorpId = accessTokenPackage.Data;
            if (accessTokenOfCorpId == null)
            {
                userIdPackage.ErrCode = DDErrCodeEnum.Unknown;
                userIdPackage.ErrMsg = "accessTokenOfCorpId is null";
                return userIdPackage;
            }
            string apiUrl = $"{QRUrls.USER_GET_USERID_BY_UNIONID}?{QRKeys.access_token}={accessTokenOfCorpId.Value}";
            apiUrl += $"&{QRKeys.access_token}={AppAccessToken.Value}&{QRKeys.unionid}={unionid}";
            userIdPackage = DDRequestAnalyzer.Get<DDUserIdPackage>(apiUrl);
            return userIdPackage;
        }
        #endregion

        #region FormatApiUrlWithAppToken Function
        public static String FormatApiUrlWithAppToken(String url, bool forceUpdate = false)
        {
            UpdateAppAccessToken(forceUpdate);
            string apiurl = $"{url}?{QRKeys.access_token}={AppAccessToken.Value}";
            return apiurl;
        }
        #endregion

    }

DDRequestAnalyzer請參照前面系列文章的代碼。

相關的其餘類以下web

DDAppAccessTokenapi

 public class DDAppAccessToken : DDAccessToken
    {
        #region 內部變量

        private static readonly object _lockObj = new object();

        private static DDAppAccessToken _instance = null;

        #endregion
        private DDAppAccessToken()
        {

        }

        #region GetInstance
        /// <summary>
        /// 獲取實例(單例)
        /// </summary>
        /// <returns></returns>
        public static DDAppAccessToken GetInstance()
        {
            if (_instance != null)
            {
                return _instance;
            }

            lock (_lockObj)
            {
                if (_instance == null)
                {
                    _instance = new DDAppAccessToken();
                }
            }

            return _instance;
        }
        #endregion

        #region IsExpired
        /// <summary>
        /// 是否過時
        /// </summary>
        /// <returns></returns>
        public bool IsExpired()
        {
            if (Begin.AddSeconds(ConstVars.APP_ACCESS_TOKEN_CACHE_TIME) >= DateTime.Now)
            {
                return false;
            }
            return true;
        }
        #endregion
    }
其中DDAccessToken能夠參看前面系列的代碼。

DDPersistenCode.cs緩存

 /// <summary>
    /// 持久受權碼
    /// </summary>
    public class DDPersistentCode : DDBaseResult
    {
        /// <summary>
        /// 用戶在當前開放應用內的惟一標識
        /// </summary>
        [JsonProperty("openid")]
        public String OpenId { get; set; }

        /// <summary>
        /// 用戶給開放應用受權的持久受權碼,此碼目前無過時時間
        /// </summary>
        [JsonProperty("persistent_code")]
        public string PersistentCode { get; set; }

        /// <summary>
        /// 用戶在當前釘釘開放平臺帳號範圍內的惟一標識,同一個釘釘開放平臺帳號能夠包含多個開放應用,同時也包含ISV的套件應用及企業應用
        /// </summary>
        [JsonProperty("unionid")]
        public string UnionId { get; set; }
    }
其中JsonProperty是JSON庫Newtonsoft的。

DDSnsToken.cs服務器

public class DDSnsToken : DDBaseResult
    {
        /// <summary>
        /// sns_token的過時時間
        /// </summary>
        [JsonProperty("expires_in")]
        public int ExpiresIn { get; set; }

        /// <summary>
        ///用戶受權的token
        /// </summary>
        [JsonProperty("sns_token")]
        public string Value { get; set; }

        /// <summary>
        /// 票據的開始時間
        /// </summary>
        public DateTime Begin { get; set; }

        #region IsExpired
        /// <summary>
        /// 是否過時
        /// </summary>
        /// <returns></returns>
        public bool IsExpired()
        {
            if (Begin.AddSeconds(ExpiresIn) >= DateTime.Now)
            {
                return false;
            }
            return true;
        }
        #endregion
    }
DDSnsUserInfo.cs

 public class DDSnsUserInfo : DDBaseResult
    {
        /// <summary>
        /// 企業信息(默認不返回)
        /// </summary>
        public SnsCorpInfo[] corp_info { get; set; }

        /// <summary>
        /// 用戶信息
        /// </summary>
        public SnsUserInfo user_info { get; set; }
    }

    #region SnsCorpInfo
    /// <summary>
    /// 企業信息(默認不返回)
    /// </summary>
    public class SnsCorpInfo
    {
        /// <summary>
        /// 企業名稱(默認不返回)
        /// </summary>
        public string corp_name { get; set; }

        /// <summary>
        /// 企業是否通過釘釘認證(默認不返回)
        /// </summary>
        public bool is_auth { get; set; }

        /// <summary>
        /// 當前用戶是否爲該企業的管理人員(默認不返回)
        /// </summary>
        public bool is_manager { get; set; }

        /// <summary>
        /// 該企業的權益等級(默認不返回)
        /// </summary>
        public int rights_level { get; set; }
    }
    #endregion

    #region SnsUserInfo
    /// <summary>
    /// 用戶信息
    /// </summary>
    public class SnsUserInfo
    {
        /// <summary>
        /// 通過處理的手機號(默認不返回)
        /// </summary>
        public string maskedMobile { get; set; }

        /// <summary>
        /// 用戶在釘釘上面的暱稱
        /// </summary>
        public string nick { get; set; }

        /// <summary>
        /// 用戶在當前開放應用內的惟一標識
        /// </summary>
        public string openid { get; set; }

        /// <summary>
        /// 用戶在當前開放應用所屬的釘釘開放平臺帳號內的惟一標識
        /// </summary>
        public string unionid { get; set; }

        /// <summary>
        ///釘釘Id
        /// </summary>
        public string dingId { get; set; }
    }
    #endregion

QRUrl.cs

 public sealed class QRUrls
    {
        public const string SNS_GET_TOKEN = "https://oapi.dingtalk.com/sns/gettoken";

        public const string SNS_GET_PERSISTENT_CODE = "https://oapi.dingtalk.com/sns/get_persistent_code";

        public const string SNS_GET_SNS_TOKEN = "https://oapi.dingtalk.com/sns/get_sns_token";

        public const string SNS_GET_USER_INFO = "https://oapi.dingtalk.com/sns/getuserinfo";

        /// <summary>
        /// 根據unionid獲取成員的userid
        /// </summary>
        public const string USER_GET_USERID_BY_UNIONID = "https://oapi.dingtalk.com/user/getUseridByUnionid";
    }
QRKeys.cs
   public class QRKeys
    {
        public const string appid = "appid";

        public const string appsecret = "appsecret";

        public const string tmp_auth_code = "tmp_auth_code";

        public const string sns_token = "sns_token";

        public const string unionid = "unionid";

        public const string access_token = "access_token";
    }
ConstVars.cs

 public class ConstVars
    {
        /// <summary>
        /// 緩存時間
        /// </summary>
        public const int APP_ACCESS_TOKEN_CACHE_TIME = 5000;
    }

在掃碼成功後,會跳轉到ddqrlogin.aspx,同時後面會帶上code和state,好比

http://XXX.com/ddqrlogin.aspx?code=9ccba352e7043c3face9da66ddba7a5f&state=STATE

其中code就是臨時受權碼tmpAuthCode。

附上ConfigTool.cs代碼
markdown

public class ConfigTool
    {
        #region FetchAppId Function 
        /// <summary>
        /// 獲取AppId
        /// </summary>
        /// <returns></returns>
        public static String FetchAppId()
        {
            return FetchValue("appId");
        }
        #endregion

        #region FetchAppSecret Function 
        /// <summary>
        /// 獲取appSecret
        /// </summary>
        /// <returns></returns>
        public static String FetchAppSecret()
        {
            return FetchValue("appSecret");
        }
        #endregion

        #region FetchValue Function              
        public static String FetchValue(String key)
        {
            String value = ConfigurationManager.AppSettings[key];
            if (value == null)
            {
                throw new Exception($"{key} is null.請確認配置文件中是否已配置.");
            }
            return value;
        }
        #endregion
    }
在Web.config上配置appid和appsecrect

<appSettings>
    <add key="appId" value="XX" />
    <add key="appSecret" value="XXXXXXXXXXXXXXXXXXXXXX" />
  </appSettings>

通過這樣的處理後,掃碼成功時將可以獲取dduserid的數據。下面是結果圖。



歡迎打描左側二維碼打賞。
app

轉載請註明出處。

相關文章
相關標籤/搜索