在《釘釘開發系列(八)二維碼掃描登錄的實現》介紹了一種掃碼登錄的方式,該方式是自己產生二維碼,二維碼中的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="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請參照前面系列文章的代碼。
相關的其他類如下
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的數據。下面是結果圖。


歡迎打描左側二維碼打賞。
轉載請注明出處。
