實現支持多公眾號的微信公眾號掃碼登錄服務


實現支持多公眾號的微信公眾號掃碼登錄服務

最近,在公司的通行證項目開發過程中,需求方提出了支持微信公眾號掃碼登錄,並且可以支持多公眾號接入的需求。研究了一下微信公眾號的開發文檔,實現微信公眾號掃碼登錄並不難,但是要支持多公眾號接入就得好好斟酌一下了。

理清思路,微信公眾號掃碼登錄的實現關鍵就是appid、openid獲取,appid用來識別公眾號,openid用來識別用戶,能理解這兩點需求就應該不難實現了。

流程

我們先整理一下流程,用戶在前端頁面點擊掃描登錄,后端服務接收到前端頁面請求之后調用微信官方api創建二維碼,並將二維碼的ticket和url返回給前端頁面,前端頁面展示二維碼,然后用戶用手機微信掃描二維碼,微信官方后台監聽到掃描事件,將事件推送給后端服務,后端服務緩存ticket和openid,前端頁面輪詢后端服務判斷緩存中是否存在ticket對應的openid,有則表示掃描成功,如果openid沒有綁定用戶,則跳轉至綁定頁面,否則直接跳轉到登錄成功頁面。
1635127551

實戰

我們主要有兩個開發步驟:

  • 生成二維碼
  • 掃碼登錄

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用來解密消息內容,配置必須與代碼一致。
    1635127594

這樣后端代碼就完成了,前端代碼請各位看官自行腦補!:)測試一下,完美通過!

最后

總體來說微信的官方文檔和示例還是不錯的,按照它一步步來很容易實現掃碼登錄功能。另外,由於時間倉促,寫得不太細致,但是核心的思想和代碼都在上面,希望可以給大家帶來幫助!

福祿·研發中心 福小皮


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM