钉钉开发系列(十一)钉钉网页扫码登录

在《钉钉开发系列(八)二维码扫描登录的实现》介绍了一种扫码登录的方式,该方式是自己产生二维码,二维码中的URL指到自身的服务器页面,在该页面中以JSSDK的方式来获取钉钉用户的信息。钉钉官方提供了另外两种扫码登录的方式,可以参见钉钉官网

先申请获取相应的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,以便于调整窗体的大小。


同时设置DocumentCompleted事件,以便在扫描成功后,读取返回的数据,代码如下。

   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代码如下

<%@ 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=" 611px; height: 600px; background-color: #2F4F4F;position:absolute;">
            <div style="margin-left: 123px; margin-top: 74px;  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请参照前面系列文章的代码。

相关的其他类如下

DDAppAccessToken

 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代码

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的数据。下面是结果图。



欢迎打描左侧二维码打赏。

转载请注明出处。

原文地址:https://www.cnblogs.com/sparkleDai/p/7604918.html