實現支援多公眾號的微信公眾號掃碼登入服務
最近,在公司的通行證專案開發過程中,需求方提出了支援微信公眾號掃碼登入,並且可以支援多公眾號接入的需求。研究了一下微信公眾號的開發文件,實現微信公眾號掃碼登入並不難,但是要支援多公眾號接入就得好好斟酌一下了。
理清思路,微信公眾號掃碼登入的實現關鍵就是appid、openid獲取,appid用來識別公眾號,openid用來識別使用者,能理解這兩點需求就應該不難實現了。
流程
我們先整理一下流程,使用者在前端頁面點選掃描登入,後端服務接收到前端頁面請求之後呼叫微信官方api建立二維碼,並將二維碼的ticket和url返回給前端頁面,前端頁面展示二維碼,然後使用者用手機微信掃描二維碼,微信官方後臺監聽到掃描事件,將事件推送給後端服務,後端服務快取ticket和openid,前端頁面輪詢後端服務判斷快取中是否存在ticket對應的openid,有則表示掃描成功,如果openid沒有繫結使用者,則跳轉至繫結頁面,否則直接跳轉到登入成功頁面。
實戰
我們主要有兩個開發步驟:
- 生成二維碼
- 掃碼登入
1、生成二維碼
參考微信官方文件生成帶引數的二維碼的說明。
- 建立二維碼ticket
每次建立二維碼ticket需要提供一個開發者自行設定的引數(scene_id),分別介紹臨時二維碼和永久二維碼的建立二維碼ticket過程。臨時二維碼請求說明
- 臨時二維碼請求說明
http請求方式: POST URL: https://api.weixin.qq.com/cgi-bin/qrcode/create?access_token=TOKEN POST資料格式:json POST資料例子:{"expire_seconds": 604800, "action_name": "QR_SCENE", "action_info": {"scene": {"scene_id": 123}}} 或者也可以使用以下POST資料建立字串形式的二維碼引數:{"expire_seconds": 604800, "action_name": "QR_STR_SCENE", "action_info": {"scene": {"scene_str": "test"}}}
廢話不多說,直接擼程式碼。先獲取AccessToken,再建立二維碼。ticket是新生成的二維碼的唯一標識,可以用來判斷二維碼是否被掃描。另外,快取returnUrl用於登入成功後重定向。
/// <summary>
/// 生成二維碼
/// </summary>
/// <param name="returnUrl"></param>
/// <param name="appid"></param>
/// <param name="secret"></param>
/// <returns></returns>
[HttpGet("/api/mpwechat/qrcode"), AllowAnonymous]
public async Task<IActionResult> QrCodeAsync(string returnUrl, string appid, string secret)
{
var accessToken = await GetAccessTokenAsync(appid, secret);
var jsonContent = await CreateQrCodeAsync(accessToken);
var ticket = jsonContent["ticket"].Value<string>();
// 快取returnUrl
var returnUrlCacheKey = MpwechatLoginReturnUrlCacheKey(ticket);
if (!(await _cache.ExistsAsync(returnUrlCacheKey)))
await _cache.AddAsync(returnUrlCacheKey, returnUrl, TimeSpan.FromMinutes(30));
return Ok(ResponseResult.Execute(new { Url = $"https://mp.weixin.qq.com/cgi-bin/showqrcode?ticket={UrlEncoder.Default.Encode(ticket)}", Ticket = ticket }));
}
/// <summary>
/// 獲取AccessToken
/// </summary>
/// <param name="appid"></param>
/// <param name="secret"></param>
/// <returns></returns>
private async Task<string> GetAccessTokenAsync(string appid, string secret)
{
// 從快取獲取AccessToken
var cacheKey = $"mpwechat:{appid}";
if ((await _cache.ExistsAsync(cacheKey)))
return (await _cache.GetAsync(cacheKey)).ToString();
var response = await _httpClient.GetAsync($"https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid={appid}&secret={secret}");
response.EnsureSuccessStatusCode();
var content = await response.Content.ReadAsStringAsync();
var jsonContent = JObject.Parse(content);
var accessToken = jsonContent["access_token"].Value<string>();
var expiresIn = jsonContent["expires_in"].Value<int>();
// 快取AccessToken
await _cache.AddAsync($"mpwechat:{appid}", accessToken, TimeSpan.FromSeconds(expiresIn - 60));
return accessToken;
}
/// <summary>
/// 建立二維碼
/// </summary>
/// <param name="accessToken"></param>
/// <param name="sceneStr"></param>
/// <returns></returns>
private async Task<JObject> CreateQrCodeAsync(string accessToken, string sceneStr = null)
{
var stringContent = new StringContent(JsonConvert.SerializeObject(
new
{
expire_seconds = 600,
action_name = "QR_STR_SCENE",
action_info = new
{
scene = new
{
scene_str = sceneStr
}
}
}), Encoding.UTF8, "application/json");
var response = await _httpClient.PostAsync($"https://api.weixin.qq.com/cgi-bin/qrcode/create?access_token={accessToken}", stringContent);
response.EnsureSuccessStatusCode();
var content = await response.Content.ReadAsStringAsync();
return JObject.Parse(content);
}
private string MpwechatLoginReturnUrlCacheKey(string ticket) => $"mpwechat_login_returnUrl:{ticket}";
2、掃碼登入
微信公眾號的掃碼登入實現方式與微信的掃碼登入實現方式不同,它是採用訂閱通知的方式實現的。參考微信官方文件事件推送的說明。
- 首先我們要準備兩個RestApi,路由地址相同,一個Get方法,一個Post方法。Get方法用於微信官方檢測伺服器配置,Post方法用於接收事件推送。為了識別通知是從哪個微信公眾號傳送的,我們將Url定義為
api/mpwechat/{appid}
,用動態路由接收appid。敲黑板,這裡是關鍵。另外,如有事件需要其他處理(如自動回覆),可轉發事件到EventBus,其他應用可自行訂閱EventBus的訊息作處理。
/// <summary>
/// 驗證微信公眾號簽名(微信公眾號呼叫)
/// </summary>
[HttpGet("/api/mpwechat/{appid}"), AllowAnonymous]
public Task<string> CheckMpwechatSignature(string appid, string signature, string timestamp, string nonce, string echostr)
{
if (!CheckMpwechatSignature(signature, timestamp, nonce))
throw new Exception("簽名驗證不通過");
return Task.FromResult(echostr);
}
/// <summary>
/// 訂閱微信公眾號事件(微信公眾號呼叫)
/// </summary>
[HttpPost("/api/mpwechat/{appid}"), AllowAnonymous]
public async Task<IActionResult> SubscribeMpwechatEvent(string appid, string signature, string timestamp, string nonce)
{
if (!CheckMpwechatSignature(signature, timestamp, nonce))
throw new Exception("簽名驗證不通過");
using StreamReader sr = new(Request.Body, Encoding.UTF8);
var data = await sr.ReadToEndAsync();
var xmlDoc = new XmlDocument();
xmlDoc.LoadXml(data);
// 如果是密文則需要解密
var encryptNode = xmlDoc.DocumentElement.SelectSingleNode("Encrypt");
if (encryptNode != null)
{
var encrypt = encryptNode.InnerText;
data = DecryptMpwechatMsg(appid, data, signature, timestamp, nonce);
if (data == null)
throw new Exception("密文解密異常");
xmlDoc.LoadXml(data);
}
// todo 推送訊息到EventBus,如有事件需要其他處理(如自動回覆),可訂閱EventBus的訊息。
// 掃碼登入
var openId = xmlDoc.DocumentElement.SelectSingleNode("FromUserName").InnerText;
var eventType = xmlDoc.DocumentElement.SelectSingleNode("Event").InnerText;
var ticketNode = xmlDoc.DocumentElement.SelectSingleNode("Ticket");
if (ticketNode != null)
{
var ticket = ticketNode.InnerText;
if (eventType == "subscribe" || eventType == "SCAN")
{
// 快取openid,標記掃碼登入
var cacheKey = MpwechatLoginOpenIdCacheKey(ticket);
if (!(await _cache.ExistsAsync(cacheKey)))
await _cache.AddAsync(cacheKey, openId, TimeSpan.FromMinutes(10));
}
}
return Ok();
}
/// <summary>
/// 輪詢檢查掃碼狀態(前端呼叫)
/// </summary>
/// <param name="ticket"></param>
/// <returns></returns>
[HttpGet("/api/mpwechat/checkscan"), AllowAnonymous]
public async Task<IActionResult> MpwechatCheckscanAsync(string ticket)
{
var openIdCacheKey = MpwechatLoginOpenIdCacheKey(ticket);
if ((await _cache.ExistsAsync(openIdCacheKey)))
{
var openId = (await _cache.GetAsync(openIdCacheKey)).ToString();
var returnUrlCacheKey = MpwechatLoginReturnUrlCacheKey(ticket);
var returnUrl = (await _cache.GetAsync(returnUrlCacheKey)).ToString();
return Ok(ResponseResult.Execute(new { ReturnUrl = returnUrl, OpenId = openId }));
}
return Ok(ResponseResult.Execute("-1", "未掃碼"));
}
/// <summary>
/// 微信公眾號基本設定中設定的Token
/// </summary>
private const string Token = "Token";
/// <summary>
/// 微信公眾號基本設定中設定的EncodingAESKey
/// </summary>
private const string EncodingAESKey = "zJULaJfu8NVIXvmKVMYfvdM2inlh4YrKkO3BvCmDOt8";
/// <summary>
/// 驗證微信公眾號簽名
/// </summary>
/// <param name="signature"></param>
/// <param name="timestamp"></param>
/// <param name="nonce"></param>
/// <returns></returns>
private bool CheckMpwechatSignature(string signature, string timestamp, string nonce)
{
// 拼接排序Sha1加密
var orderJoinString = string.Join("", new string[] { Token, timestamp, nonce }.OrderBy(t => t));
return signature == Encrypt.Sha1(orderJoinString);
}
/// <summary>
/// 解密微信公眾號內容
/// </summary>
/// <param name="appId"></param>
/// <param name="data"></param>
/// <param name="signature"></param>
/// <param name="timestamp"></param>
/// <param name="nonce"></param>
/// <returns></returns>
private string DecryptMpwechatMsg(string appId, string data, string signature, string timestamp, string nonce)
{
// 利用微信官方示例程式碼
Tencent.WXBizMsgCrypt wxcpt = new(Token, EncodingAESKey, appId);
var content = "";
var ret = wxcpt.DecryptMsg(signature, timestamp, nonce, data, ref content);
if (ret == 0)
return content;
return null;
}
private string MpwechatLoginOpenIdCacheKey(string ticket) => $"mpwechat_login_openId:{ticket}";
- 在微信公眾號的基本配置裡面配置Url、Token、EncodingAESKey和訊息加密方式。Token用來驗證微信公眾號簽名,EncodingAESKey用來解密訊息內容,配置必須與程式碼一致。
這樣後端程式碼就完成了,前端程式碼請各位看官自行腦補!:)測試一下,完美通過!
最後
總體來說微信的官方文件和示例還是不錯的,按照它一步步來很容易實現掃碼登入功能。另外,由於時間倉促,寫得不太細緻,但是核心的思想和程式碼都在上面,希望可以給大家帶來幫助!