zoukankan      html  css  js  c++  java
  • 实现支持多公众号的微信公众号扫码登录服务

    实现支持多公众号的微信公众号扫码登录服务

    最近,在公司的通行证项目开发过程中,需求方提出了支持微信公众号扫码登录,并且可以支持多公众号接入的需求。研究了一下微信公众号的开发文档,实现微信公众号扫码登录并不难,但是要支持多公众号接入就得好好斟酌一下了。

    理清思路,微信公众号扫码登录的实现关键就是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

    这样后端代码就完成了,前端代码请各位看官自行脑补!:)测试一下,完美通过!

    最后

    总体来说微信的官方文档和示例还是不错的,按照它一步步来很容易实现扫码登录功能。另外,由于时间仓促,写得不太细致,但是核心的思想和代码都在上面,希望可以给大家带来帮助!

    福禄·研发中心 福小皮
  • 相关阅读:
    函数的声明
    数组遍历的方法
    运算符
    变量命名规则
    js条件判断
    vuex使用
    高盛伦敦分部面试
    野村证券伦敦分部面试
    Linux Performance Monitoring Commands
    Linux server上too many open files问题
  • 原文地址:https://www.cnblogs.com/fulu/p/15464425.html
Copyright © 2011-2022 走看看