在《钉钉开发系列(八)二维码扫描登录的实现》介绍了一种扫码登录的方式,该方式是自己产生二维码,二维码中的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" %>
服务端后台代码如下
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 全局变量
///
/// 基于appid获取的票据
///
public static DDAppAccessToken AppAccessToken = DDAppAccessToken.GetInstance();
#endregion
#region UpdateQRAccessToken
///
/// 更新应用票据
///
///
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(apiurl);
if (tokenResult.IsOK())
{
AppAccessToken.Value = tokenResult.Access_token;
AppAccessToken.Begin = DateTime.Now;
}
}
#endregion
#region FetchPersistentCode Function
///
/// 获取持久授权码
///
///
///
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(apiUrl, data.ToJSON());
return result;
}
#endregion
#region FetchSnsToken Function
///
/// 获取SNS票据
///
///
///
///
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(apiUrl, data.ToJSON());
if (result.IsOK())
{
SnsToken.ExpiresIn = result.ExpiresIn;
SnsToken.Value = result.Value;
SnsToken.Begin = DateTime.Now;
}
return SnsToken;
}
#endregion
#region FetchUserInfo Function
///
/// 基于临时获权码获取用户信息
///
/// 临时授权码
///
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(apiUrl);
return snsUserInfoPackage;
}
#endregion
#region FetchUserInfo Function
///
/// 基于临时获取DDUserId
///
/// 临时授权码
///
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
///
/// 基于UnionId获取DDUserId
///
/// 用户在当前钉钉开放平台账号范围内的唯一标识,同一个钉钉开放平台账号可以包含多个开放应用,同时也包含ISV的套件应用及企业应用
///
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(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
///
/// 获取实例(单例)
///
///
public static DDAppAccessToken GetInstance()
{
if (_instance != null)
{
return _instance;
}
lock (_lockObj)
{
if (_instance == null)
{
_instance = new DDAppAccessToken();
}
}
return _instance;
}
#endregion
#region IsExpired
///
/// 是否过期
///
///
public bool IsExpired()
{
if (Begin.AddSeconds(ConstVars.APP_ACCESS_TOKEN_CACHE_TIME) >= DateTime.Now)
{
return false;
}
return true;
}
#endregion
}
其中DDAccessToken可以参看前面系列的代码。
DDPersistenCode.cs
///
/// 持久授权码
///
public class DDPersistentCode : DDBaseResult
{
///
/// 用户在当前开放应用内的唯一标识
///
[JsonProperty("openid")]
public String OpenId { get; set; }
///
/// 用户给开放应用授权的持久授权码,此码目前无过期时间
///
[JsonProperty("persistent_code")]
public string PersistentCode { get; set; }
///
/// 用户在当前钉钉开放平台账号范围内的唯一标识,同一个钉钉开放平台账号可以包含多个开放应用,同时也包含ISV的套件应用及企业应用
///
[JsonProperty("unionid")]
public string UnionId { get; set; }
}
其中JsonProperty是JSON库Newtonsoft的。
DDSnsToken.cs
public class DDSnsToken : DDBaseResult
{
///
/// sns_token的过期时间
///
[JsonProperty("expires_in")]
public int ExpiresIn { get; set; }
///
///用户授权的token
///
[JsonProperty("sns_token")]
public string Value { get; set; }
///
/// 票据的开始时间
///
public DateTime Begin { get; set; }
#region IsExpired
///
/// 是否过期
///
///
public bool IsExpired()
{
if (Begin.AddSeconds(ExpiresIn) >= DateTime.Now)
{
return false;
}
return true;
}
#endregion
}
DDSnsUserInfo.cs
public class DDSnsUserInfo : DDBaseResult
{
///
/// 企业信息(默认不返回)
///
public SnsCorpInfo[] corp_info { get; set; }
///
/// 用户信息
///
public SnsUserInfo user_info { get; set; }
}
#region SnsCorpInfo
///
/// 企业信息(默认不返回)
///
public class SnsCorpInfo
{
///
/// 企业名称(默认不返回)
///
public string corp_name { get; set; }
///
/// 企业是否经过钉钉认证(默认不返回)
///
public bool is_auth { get; set; }
///
/// 当前用户是否为该企业的管理人员(默认不返回)
///
public bool is_manager { get; set; }
///
/// 该企业的权益等级(默认不返回)
///
public int rights_level { get; set; }
}
#endregion
#region SnsUserInfo
///
/// 用户信息
///
public class SnsUserInfo
{
///
/// 经过处理的手机号(默认不返回)
///
public string maskedMobile { get; set; }
///
/// 用户在钉钉上面的昵称
///
public string nick { get; set; }
///
/// 用户在当前开放应用内的唯一标识
///
public string openid { get; set; }
///
/// 用户在当前开放应用所属的钉钉开放平台账号内的唯一标识
///
public string unionid { get; set; }
///
///钉钉Id
///
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";
///
/// 根据unionid获取成员的userid
///
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
{
///
/// 缓存时间
///
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
///
/// 获取AppId
///
///
public static String FetchAppId()
{
return FetchValue("appId");
}
#endregion
#region FetchAppSecret Function
///
/// 获取appSecret
///
///
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
经过这样的处理后,扫码成功时将能够获取dduserid的数据。下面是结果图。
欢迎打描左侧二维码打赏。
转载请注明出处。