前一阵子,想着给我的站点集成一个微信登录,因为我之前从未有过微信相关的开发,所以我自己跟着网上的资料,一步一步的慢慢的摸索,过程不免的遇到了许多坑,才把我的网站微信登录集成完成,所以这里分享一下我的摸索的过程。因为我的是订阅号,所以一下的内容均针对订阅号而言的。
一、了解微信的交互流程
这里假设我们都已经申请号了微信开发的相关信息,那么我们就要配置微信认证服务器地址(api地址),这一步是必须的,微信与我们交互都是都是通过这一个地址,开始我一直不知道,我以为自己要对每一个功能都要写一个api,其实不需要的。我们只需要完成我们的微信认证服务器地址,然后里面处理我们相关的业务逻辑。
用我的话就是
假设我的认证api是:/api/check
1.用户发送消息”你好“到微信公众号
2.微信公众号发起Get请求调用 /api/check 进行认证
3.认证成功时微信公众号再次Post请求调用/api/check,并且携带”你好“信息
4./api/check接口里面处理相关业务逻辑,或者返回特定信息给用户
二、集成相关sdk
对于微信的消息处理,其实有点复杂,这里我网上搜了一下,大部分推荐的是盛派微信sdk,博客园也有相关的教程https://www.cnblogs.com/szw/archive/2013/05/20/3089479.html,个人用起来我觉得还是可以的,可以省去我们大部分工作,专注处理业务逻辑。
封装CustomMessageHandler
首先我是要对盛派微信sdk的封装,所以我新建了一个CustomMessageHandler,然后继承自MessageHandler
public override IResponseMessageBase DefaultResponseMessage(IRequestMessageBase requestMessage)
{
var responseMessage = base.CreateResponseMessage
return responseMessage;
}
处理用户关注事件
当微信用户关注公众号时我需要发送问候语给用户,那么这里就需要重写OnEvent_SubscribeRequestAsync
public override Task
{
var responseMessage = base.CreateResponseMessage
responseMessage.Content = “欢迎关注”;
return Task.FromResult(responseMessage as IResponseMessageBase);
}
处理用户关键字
当微信用户给公众号发送特点消息(关键字)时,我需要回复用户,那么就重写OnTextRequestAsync
public override Task
{
var responseMessage = base.CreateResponseMessage
return Task.FromResult(responseMessage as IResponseMessageBase);
}
抽离业务逻辑方法
在开发中,我们往往希望封装一个功能时,不需要牵扯太多业务逻辑,就例如不想在CustomMessageHandler里去写业务逻辑,那么我这里采用的委托的形式,新建一个CustomParam,然后里面定义一个两个委托
public class CustomParam{/// <summary>/// 普通文本事件处理/// </summary>public Func<RequestMessageText, ResponseMessageText, string, ResponseMessageText> OnTextFunc;/// <summary>/// 订阅事件处理/// </summary>public Func<string> OnSubscribeFunc;}
然后通过CustomMessageHandler的构造函数传入
private CustomParam customParam;public CustomMessageHandler(CustomParam customParam){this.customParam = customParam;}
然后在具体的处理事件里面调用这个委托
通过接口调用微信CustomMessageHandler的方法
我接着在建一个接口,里面包含了认证,消息处理,创建CustomMessageHandler的方法,来给业务调用
using Core.WeXin.WxOfficial;
using Senparc.Weixin.AspNet.MvcExtension;
using Senparc.Weixin.MP.Entities;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;namespace Core.WeXin
{public interface IWxCommonHandler{/// <summary>/// 校验服务/// </summary>/// <returns></returns>bool CheckSign(string signature, string timestamp, string nonce, string echostr);/// <summary>/// 创建消息处理器/// </summary>/// <param name="inputStream"></param>/// <param name="messageAbsService"></param>void CreateMessageHandler(Stream inputStream, CustomParam param);/// <summary>/// 公众号消息处理/// </summary>/// <param name="inputStream"></param>/// <returns></returns>Task<WeixinResult> ExecuteMessageHandler();/// <summary>/// 获取当前opentid/// </summary>/// <returns></returns>string GetOpenId();/// <summary>/// 获取微信消息/// </summary>/// <returns></returns>string GetMessgeText();}
}
using Core.Log;
using Core.WeXin.WxOfficial;
using Microsoft.Extensions.Configuration;
using Senparc.NeuChar;
using Senparc.NeuChar.Entities;
using Senparc.Weixin.AspNet.MvcExtension;
using Senparc.Weixin.MP;
using Senparc.Weixin.MP.Entities;
using System;namespace Core.WeXin
{internal class WxCommonHandler:IWxCommonHandler{private CustomMessageHandler customMessageHandler = null;private IConfiguration configuration; public WxCommonHandler(IConfiguration configuration){this.configuration = configuration;}public bool CheckSign(string signature, string timestamp, string nonce, string echostr){string token = configuration.GetSection("SenparcWeixinSetting:Token").Value;return CheckSignature.Check(signature, timestamp, nonce, token);}public void CreateMessageHandler(Stream inputStream,CustomParam customParam){customMessageHandler = new CustomMessageHandler(inputStream, null, customParam);customMessageHandler.OmitRepeatedMessage = true;}public async Task<WeixinResult> ExecuteMessageHandler(){await customMessageHandler.ExecuteAsync(new CancellationToken());string result = "";if (customMessageHandler.ResponseDocument != null){result = customMessageHandler.ResponseDocument.ToString();}return new WeixinResult(result);}public string GetOpenId(){return customMessageHandler.OpenId;}public string GetMessgeText(){var requestMsg= customMessageHandler.RequestMessage;if (requestMsg.MsgType == RequestMsgType.Text){RequestMessageText requestText = requestMsg as RequestMessageText;return requestText.Content;}return string.Empty;}}
}
注册微信sdk相关内容
using Core.Log;
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Newtonsoft.Json;
using Senparc.CO2NET;
using Senparc.CO2NET.AspNet;
using Senparc.Weixin.AspNet;
using Senparc.Weixin.Entities;
using Senparc.Weixin.MP;
using Senparc.Weixin.RegisterServices;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;namespace Core.WeXin
{public static class ConfigureWxService{public static IServiceCollection AddWxOfficialServices(this IServiceCollection services, IConfiguration configuration){services.AddScoped<IWxCommonHandler, WxCommonHandler>();services.AddSenparcWeixinServices(configuration);return services;}public static void UseWxOfficial(this WebApplication app, ConfigurationManager configuration){var registerService = app.UseSenparcWeixin(app.Environment, null, null, register => { },(register, weixinSetting) =>{register.RegisterMpAccount(weixinSetting, configuration.GetSection("WxOfficialName").Value);});}}
}
二、新建认证api(Get请求)
前面说到,我们和微信打交道始终是这一个微信,所以我们需要用一个api既能接收认证又能接收消息,所以我先建一个Get请求的方法
[HttpGet("handler")]public ContentResult Handler(){string signature = Request.Query["signature"];string timestamp = Request.Query["timestamp"];string nonce = Request.Query["nonce"];string echostr = Request.Query["echostr"];if (wxCommonHandler.CheckSign(signature, timestamp, nonce, echostr)){return new ContentResult(){Content = echostr};}else{return new ContentResult(){Content = "false"};}}
三、新建微信处理的Service
关注事件业务
我想在用户关注时发送对应的问候语,则我直接新建一个方法OnSubscribeEvent
public string OnSubscribeEvent(){StringBuilder sb = new StringBuilder();sb.Append("您好!欢迎关注【闲蛋】💖\r\n");sb.Append("回复[登录]即可获取验证码登录网页<a href=\"https://www.xiandanplay.com\">闲蛋</a>🌟\r\n");sb.Append("更好的网站体验请用电脑登录😂😂\r\n");sb.Append("关于闲蛋,点这里👉:<a href=\"https://mp.weixin.qq.com/s/TNBr9XiLbPJU3ZFRT3cCbg\">关于闲蛋</a>\r\n");sb.Append("关于站长,点这里👉:<a href=\"https://mp.weixin.qq.com/s/4KvydAbogv2KZGBfNmRveA\">关于站长</a>\r\n");sb.Append("祝大家生活平平安安,不求多财但求无疾😜😜");return sb.ToString();}
关键字登录
因为我的是订阅号,无法获取实现微信登录,于是我只好通过回复关键字的形式,获取登录验证码,然后获取到openid来注册用户,所以我的处理步骤如下:
1.匹配到关键字“登录”
2.根据当前的当前的openid判断redis的Set是否已有对应的验证吗,如果已存在直接返回“请勿重复获取验证码”
3.为了防止生成重复的有效的验证码,所以我新建一个Key,值为loginCode的Set类型的缓存,当生成的验证码可以从redis取到,则从新生成。(目前木有想到更好的办法,请赐教...)
4.将Set的Key、 验证码loginCode,放入redis,存为Set类型,过期时间为1分钟,此处是为了步骤3的校验
5.再新建一个Set的Key、 openid,放入redis,过期时间为1分钟,此处是为了步骤2的校验,
6.再新建一个HashSet的Key、loginCode、 openid,放入redis,过期时间为1分钟,然后输入验证码登录的时候直接根据这个缓存来
public ResponseMessageText OnTextEvent(RequestMessageText requestMessageText,ResponseMessageText responseMessageText,string openId){try{requestMessageText.StartHandler().Keyword("登录", () =>{if (cacheClient.ExistsSet(CheckRepeatGetLoginCodeKey, openId))responseMessageText.Content = $"您好:请勿重复获取验证码";else{string verCode = OnLoginVerCode(openId);responseMessageText.Content = $"您好:您的网站登录验证码是 {verCode} 有效期60秒";}return responseMessageText;});return responseMessageText;}catch (Exception ex){LogUtils.LogError(ex);throw;}}private string OnLoginVerCode(string openId){string loginCode = StringUtil.RandomNumber(4);string cacheKey = "login_wx_code";bool existCode = false;do{existCode = cacheClient.ExistsSet(cacheKey, loginCode);if (!existCode){TimeSpan expire = TimeSpan.FromMinutes(1);cacheClient.AddSet(cacheKey, loginCode, expire);cacheClient.AddSet(CheckRepeatGetLoginCodeKey, openId, expire);cacheClient.AddHash(CacheKey.WxLoginCodeKey, loginCode, openId, expire);}}while (existCode);return loginCode;}
四、新建认证api(Post请求)
[HttpPost("handler")]public async Task<ContentResult> Handler(string signature, string timestamp, string nonce, string echostr){if (!wxCommonHandler.CheckSign(signature, timestamp, nonce,echostr)){return new ContentResult(){Content = "参数错误"};}CustomParam customParam = new CustomParam(){OnSubscribeFunc = wxService.OnSubscribeEvent,OnTextFunc = wxService.OnTextEvent};wxCommonHandler.CreateMessageHandler(Request.Body, customParam);return await wxCommonHandler.ExecuteMessageHandler();}
wxService就是前面的封装的方法
五、登录验证
比较简单,就是根据验证码,能否从缓存取到对应的信息
Regex regex = new Regex(RegexPattern.IsNumberPattern);if (!regex.IsMatch(code.Trim()) || code.Length != 4)throw new ValidationException("code 无效");CacheClient cacheClient = CacheClient.CreateClient();string openId= cacheClient.GetHashValue("Wx_Login_Hash_Key", code);if (string.IsNullOrEmpty(openId))throw new AuthException("验证码无效");WeXinUserInfo weXinUserInfo = new WeXinUserInfo();weXinUserInfo.OpenAuthEnum = OpenAuthEnum.Wexin;weXinUserInfo.Nickname = "wx_" + StringUtil.RandomNumber(10);weXinUserInfo.OpenID = openId;weXinUserInfo.Sex = "男";cacheClient.DeleteHashField("Wx_Login_Hash_Key", code);return await Task.FromResult(weXinUserInfo);
六、成果展示
目前为止,微信公众号已经开发完成,因为我没有过相关开发的经验,所以如果有不合适的地方,大家可以指出来。
同时我有个疑问请教一下大家,就是发布到服务器上后我用明文模式可以正常处理微信消息,但是安全模式却不行了,也正确的配置了相关的EncodingAESKey
作者:程序员奶牛
个人开源网站:https://www.xiandanplay.com
源码地址:https://gitee.com/MrHanchichi/xian-dan