| | |
| | | using Aop.Api.Domain; |
| | | using ApiTools.Core; |
| | | using Furion; |
| | | using Furion.HttpRemote; |
| | | using log4net.Core; |
| | | using MediatR; |
| | | using Microsoft.AspNetCore.Http; |
| | |
| | | public class WxmpSubscribMessageCommandHandler( |
| | | ILogger<WxmpSubscribMessageCommandHandler> logger, |
| | | WxmpUtils utils, |
| | | IHttpContextAccessor httpContextAccessor |
| | | IHttpRemoteService httpRemoteService |
| | | ) : |
| | | IRequestHandler<SendWxmpSubscribMessageCommand, Guid>, |
| | | IRequestHandler<WxmpSubscribMessageNotifyCommand, bool> |
| | | { |
| | | private readonly ILogger<WxmpSubscribMessageCommandHandler> logger = logger; |
| | | private readonly WxmpUtils utils = utils; |
| | | private readonly IHttpContextAccessor httpContextAccessor = httpContextAccessor; |
| | | private readonly IHttpRemoteService httpRemoteService = httpRemoteService; |
| | | |
| | | /// <summary> |
| | | /// 微信小程序发送订阅消息 |
| | |
| | | /// <returns></returns> |
| | | public async Task<bool> Handle(WxmpSubscribMessageNotifyCommand request, CancellationToken cancellationToken) |
| | | { |
| | | var req = httpContextAccessor.HttpContext.Request; |
| | | logger.LogInformation($"微信小程序订阅消息通知query:{req.QueryString.Value}"); |
| | | var env = App.GetConfig<string>("Environment"); |
| | | if (env == "Product") |
| | | { |
| | | try |
| | | { |
| | | var json = request.ToJson(); |
| | | await httpRemoteService.PostAsStringAsync("http://118.178.252.28:8780/api/common/wxmp/wxmpSubscribMessageNotify", |
| | | builder => builder.SetJsonContent(json)); |
| | | } |
| | | catch |
| | | { |
| | | |
| | | req.EnableBuffering(); |
| | | req.Body.Position = 0; |
| | | using var reader = new StreamReader(req.Body, Encoding.UTF8, leaveOpen: true); |
| | | var body = await reader.ReadToEndAsync(); |
| | | logger.LogInformation($"微信小程序订阅消息通知body:{body}"); |
| | | req.Body.Position = 0; |
| | | } |
| | | } |
| | | return true; |
| | | } |
| | | } |
| | |
| | | </None> |
| | | </ItemGroup> |
| | | |
| | | <ItemGroup> |
| | | <Folder Include="Utils\WxmpUtils\Crypto\" /> |
| | | </ItemGroup> |
| | | |
| | | </Project> |
| | |
| | | 微信小程序订阅消息通知 |
| | | </summary> |
| | | </member> |
| | | <member name="P:ApiTools.Core.WxmpSubscribMessageNotifyRequestQuery.Signature"> |
| | | <member name="P:ApiTools.Core.WxmpSubscribMessageNotifyCommand.Code"> |
| | | <summary> |
| | | 小程序代码 |
| | | </summary> |
| | | </member> |
| | | <member name="P:ApiTools.Core.WxmpSubscribMessageNotifyCommand.OpenId"> |
| | | <summary> |
| | | 用户开放Id |
| | | </summary> |
| | | </member> |
| | | <member name="P:ApiTools.Core.WxmpSubscribMessageNotifyCommand.CreateTime"> |
| | | <summary> |
| | | 创建时间 |
| | | </summary> |
| | | </member> |
| | | <member name="P:ApiTools.Core.WxmpSubscribMessageNotifyCommand.MsgType"> |
| | | <summary> |
| | | 消息类型 |
| | | </summary> |
| | | </member> |
| | | <member name="P:ApiTools.Core.WxmpSubscribMessageNotifyCommand.Event"> |
| | | <summary> |
| | | 事件 |
| | | </summary> |
| | | </member> |
| | | <member name="P:ApiTools.Core.WxmpSubscribMessageNotifyCommand.Content"> |
| | | <summary> |
| | | 内容 |
| | | </summary> |
| | | </member> |
| | | <member name="P:ApiTools.Core.WxmpSubscribMessageNotifyRequestQuery.signature"> |
| | | <summary> |
| | | 签名 |
| | | </summary> |
| | | </member> |
| | | <member name="P:ApiTools.Core.WxmpSubscribMessageNotifyRequestQuery.Timestamp"> |
| | | <member name="P:ApiTools.Core.WxmpSubscribMessageNotifyRequestQuery.timestamp"> |
| | | <summary> |
| | | 时间戳 |
| | | </summary> |
| | | </member> |
| | | <member name="P:ApiTools.Core.WxmpSubscribMessageNotifyRequestQuery.Nonce"> |
| | | <member name="P:ApiTools.Core.WxmpSubscribMessageNotifyRequestQuery.nonce"> |
| | | <summary> |
| | | 随机数 |
| | | </summary> |
| | | </member> |
| | | <member name="P:ApiTools.Core.WxmpSubscribMessageNotifyRequestQuery.Echostr"> |
| | | <member name="P:ApiTools.Core.WxmpSubscribMessageNotifyRequestQuery.echostr"> |
| | | <summary> |
| | | 随机字符串 |
| | | </summary> |
| | |
| | | <param name="xmlDoc"></param> |
| | | <returns></returns> |
| | | </member> |
| | | <member name="M:Tencent.Cryptography.AES_decrypt(System.String,System.String,System.String@)"> |
| | | <summary> |
| | | 解密方法 |
| | | </summary> |
| | | <param name="Input">密文</param> |
| | | <param name="EncodingAESKey"></param> |
| | | <returns></returns> |
| | | |
| | | </member> |
| | | <member name="M:Tencent.Cryptography.chr(System.Int32)"> |
| | | 将数字转化成ASCII码对应的字符,用于对明文进行补码 |
| | | |
| | | @param a 需要转化的数字 |
| | | @return 转化得到的字符 |
| | | </member> |
| | | </members> |
| | | </doc> |
| | |
| | | using MediatR; |
| | | using Newtonsoft.Json; |
| | | using System; |
| | | using System.Collections.Generic; |
| | | using System.Linq; |
| | |
| | | /// <summary> |
| | | /// 微信小程序订阅消息通知 |
| | | /// </summary> |
| | | [Resource([EnumResourceController.CommonServerWxmpUtils], Method = EnumResourceMethod.Get)] |
| | | [Resource([EnumResourceController.CommonServerWxmpUtils], AllowAnonymous = true)] |
| | | public class WxmpSubscribMessageNotifyCommand : IRequest<bool> |
| | | { |
| | | |
| | | |
| | | /// <summary> |
| | | /// 小程序代码 |
| | | /// </summary> |
| | | public string Code { get; set; } |
| | | /// <summary> |
| | | /// 用户开放Id |
| | | /// </summary> |
| | | public string OpenId { get; set; } |
| | | public string ToUserName { get; set; } |
| | | public string FromUserName { get; set; } |
| | | public DateTime CreateTime { get; set; } |
| | | /// <summary> |
| | | /// 创建时间 |
| | | /// </summary> |
| | | public int CreateTime { get; set; } |
| | | /// <summary> |
| | | /// 消息类型 |
| | | /// </summary> |
| | | public string MsgType { get; set; } |
| | | /// <summary> |
| | | /// 事件 |
| | | /// </summary> |
| | | public string Event { get; set; } |
| | | /// <summary> |
| | | /// 内容 |
| | | /// </summary> |
| | | [JsonProperty("debug_str")] |
| | | public string Content { get; set; } |
| | | } |
| | | } |
| | |
| | | /// <summary> |
| | | /// 签名 |
| | | /// </summary> |
| | | public string Signature { get; set; } |
| | | public string signature { get; set; } |
| | | |
| | | /// <summary> |
| | | /// 时间戳 |
| | | /// </summary> |
| | | public string Timestamp { get; set; } |
| | | public string timestamp { get; set; } |
| | | |
| | | /// <summary> |
| | | /// 随机数 |
| | | /// </summary> |
| | | public string Nonce { get; set; } |
| | | public string nonce { get; set; } |
| | | |
| | | /// <summary> |
| | | /// 随机字符串 |
| | | /// </summary> |
| | | public string Echostr { get; set; } |
| | | public string echostr { get; set; } |
| | | |
| | | public string openid { get; set; } |
| | | |
| | | public string encrypt_type { get; set; } |
| | | |
| | | public string msg_signature { get; set; } |
| | | } |
| | | |
| | | public class WxmpSubscribMessageNotifyRequestBody |
| | | { |
| | | public string ToUserName { get; set; } |
| | | public string Encrypt { get; set; } |
| | | } |
| | | } |
| New file |
| | |
| | | using System; |
| | | using System.Collections.Generic; |
| | | using System.Linq; |
| | | using System.Text; |
| | | using System.Security.Cryptography; |
| | | using System.IO; |
| | | using System.Net; |
| | | namespace Tencent |
| | | { |
| | | class Cryptography |
| | | { |
| | | public static UInt32 HostToNetworkOrder(UInt32 inval) |
| | | { |
| | | UInt32 outval = 0; |
| | | for (int i = 0; i < 4; i++) |
| | | outval = (outval << 8) + ((inval >> (i * 8)) & 255); |
| | | return outval; |
| | | } |
| | | |
| | | public static Int32 HostToNetworkOrder(Int32 inval) |
| | | { |
| | | Int32 outval = 0; |
| | | for (int i = 0; i < 4; i++) |
| | | outval = (outval << 8) + ((inval >> (i * 8)) & 255); |
| | | return outval; |
| | | } |
| | | /// <summary> |
| | | /// 解密方法 |
| | | /// </summary> |
| | | /// <param name="Input">密文</param> |
| | | /// <param name="EncodingAESKey"></param> |
| | | /// <returns></returns> |
| | | /// |
| | | public static string AES_decrypt(String Input, string EncodingAESKey, ref string appid) |
| | | { |
| | | byte[] Key; |
| | | Key = Convert.FromBase64String(EncodingAESKey + "="); |
| | | byte[] Iv = new byte[16]; |
| | | Array.Copy(Key, Iv, 16); |
| | | byte[] btmpMsg = AES_decrypt(Input, Iv, Key); |
| | | |
| | | int len = BitConverter.ToInt32(btmpMsg, 16); |
| | | len = IPAddress.NetworkToHostOrder(len); |
| | | |
| | | |
| | | byte[] bMsg = new byte[len]; |
| | | byte[] bAppid = new byte[btmpMsg.Length - 20 - len]; |
| | | Array.Copy(btmpMsg, 20, bMsg, 0, len); |
| | | Array.Copy(btmpMsg, 20+len , bAppid, 0, btmpMsg.Length - 20 - len); |
| | | string oriMsg = Encoding.UTF8.GetString(bMsg); |
| | | appid = Encoding.UTF8.GetString(bAppid); |
| | | |
| | | |
| | | return oriMsg; |
| | | } |
| | | |
| | | public static String AES_encrypt(String Input, string EncodingAESKey, string appid) |
| | | { |
| | | byte[] Key; |
| | | Key = Convert.FromBase64String(EncodingAESKey + "="); |
| | | byte[] Iv = new byte[16]; |
| | | Array.Copy(Key, Iv, 16); |
| | | string Randcode = CreateRandCode(16); |
| | | byte[] bRand = Encoding.UTF8.GetBytes(Randcode); |
| | | byte[] bAppid = Encoding.UTF8.GetBytes(appid); |
| | | byte[] btmpMsg = Encoding.UTF8.GetBytes(Input); |
| | | byte[] bMsgLen = BitConverter.GetBytes(HostToNetworkOrder(btmpMsg.Length)); |
| | | byte[] bMsg = new byte[bRand.Length + bMsgLen.Length + bAppid.Length + btmpMsg.Length]; |
| | | |
| | | Array.Copy(bRand, bMsg, bRand.Length); |
| | | Array.Copy(bMsgLen, 0, bMsg, bRand.Length, bMsgLen.Length); |
| | | Array.Copy(btmpMsg, 0, bMsg, bRand.Length + bMsgLen.Length, btmpMsg.Length); |
| | | Array.Copy(bAppid, 0, bMsg, bRand.Length + bMsgLen.Length + btmpMsg.Length, bAppid.Length); |
| | | |
| | | return AES_encrypt(bMsg, Iv, Key); |
| | | |
| | | } |
| | | private static string CreateRandCode(int codeLen) |
| | | { |
| | | string codeSerial = "2,3,4,5,6,7,a,c,d,e,f,h,i,j,k,m,n,p,r,s,t,A,C,D,E,F,G,H,J,K,M,N,P,Q,R,S,U,V,W,X,Y,Z"; |
| | | if (codeLen == 0) |
| | | { |
| | | codeLen = 16; |
| | | } |
| | | string[] arr = codeSerial.Split(','); |
| | | string code = ""; |
| | | int randValue = -1; |
| | | Random rand = new Random(unchecked((int)DateTime.Now.Ticks)); |
| | | for (int i = 0; i < codeLen; i++) |
| | | { |
| | | randValue = rand.Next(0, arr.Length - 1); |
| | | code += arr[randValue]; |
| | | } |
| | | return code; |
| | | } |
| | | |
| | | private static String AES_encrypt(String Input, byte[] Iv, byte[] Key) |
| | | { |
| | | var aes = new RijndaelManaged(); |
| | | //秘钥的大小,以位为单位 |
| | | aes.KeySize = 256; |
| | | //支持的块大小 |
| | | aes.BlockSize = 128; |
| | | //填充模式 |
| | | aes.Padding = PaddingMode.PKCS7; |
| | | aes.Mode = CipherMode.CBC; |
| | | aes.Key = Key; |
| | | aes.IV = Iv; |
| | | var encrypt = aes.CreateEncryptor(aes.Key, aes.IV); |
| | | byte[] xBuff = null; |
| | | |
| | | using (var ms = new MemoryStream()) |
| | | { |
| | | using (var cs = new CryptoStream(ms, encrypt, CryptoStreamMode.Write)) |
| | | { |
| | | byte[] xXml = Encoding.UTF8.GetBytes(Input); |
| | | cs.Write(xXml, 0, xXml.Length); |
| | | } |
| | | xBuff = ms.ToArray(); |
| | | } |
| | | String Output = Convert.ToBase64String(xBuff); |
| | | return Output; |
| | | } |
| | | |
| | | private static String AES_encrypt(byte[] Input, byte[] Iv, byte[] Key) |
| | | { |
| | | var aes = new RijndaelManaged(); |
| | | //秘钥的大小,以位为单位 |
| | | aes.KeySize = 256; |
| | | //支持的块大小 |
| | | aes.BlockSize = 128; |
| | | //填充模式 |
| | | //aes.Padding = PaddingMode.PKCS7; |
| | | aes.Padding = PaddingMode.None; |
| | | aes.Mode = CipherMode.CBC; |
| | | aes.Key = Key; |
| | | aes.IV = Iv; |
| | | var encrypt = aes.CreateEncryptor(aes.Key, aes.IV); |
| | | byte[] xBuff = null; |
| | | |
| | | #region 自己进行PKCS7补位,用系统自己带的不行 |
| | | byte[] msg = new byte[Input.Length + 32 - Input.Length % 32]; |
| | | Array.Copy(Input, msg, Input.Length); |
| | | byte[] pad = KCS7Encoder(Input.Length); |
| | | Array.Copy(pad, 0, msg, Input.Length, pad.Length); |
| | | #endregion |
| | | |
| | | #region 注释的也是一种方法,效果一样 |
| | | //ICryptoTransform transform = aes.CreateEncryptor(); |
| | | //byte[] xBuff = transform.TransformFinalBlock(msg, 0, msg.Length); |
| | | #endregion |
| | | |
| | | using (var ms = new MemoryStream()) |
| | | { |
| | | using (var cs = new CryptoStream(ms, encrypt, CryptoStreamMode.Write)) |
| | | { |
| | | cs.Write(msg, 0, msg.Length); |
| | | } |
| | | xBuff = ms.ToArray(); |
| | | } |
| | | |
| | | String Output = Convert.ToBase64String(xBuff); |
| | | return Output; |
| | | } |
| | | |
| | | private static byte[] KCS7Encoder(int text_length) |
| | | { |
| | | int block_size = 32; |
| | | // 计算需要填充的位数 |
| | | int amount_to_pad = block_size - (text_length % block_size); |
| | | if (amount_to_pad == 0) |
| | | { |
| | | amount_to_pad = block_size; |
| | | } |
| | | // 获得补位所用的字符 |
| | | char pad_chr = chr(amount_to_pad); |
| | | string tmp = ""; |
| | | for (int index = 0; index < amount_to_pad; index++) |
| | | { |
| | | tmp += pad_chr; |
| | | } |
| | | return Encoding.UTF8.GetBytes(tmp); |
| | | } |
| | | /** |
| | | * 将数字转化成ASCII码对应的字符,用于对明文进行补码 |
| | | * |
| | | * @param a 需要转化的数字 |
| | | * @return 转化得到的字符 |
| | | */ |
| | | static char chr(int a) |
| | | { |
| | | |
| | | byte target = (byte)(a & 0xFF); |
| | | return (char)target; |
| | | } |
| | | private static byte[] AES_decrypt(String Input, byte[] Iv, byte[] Key) |
| | | { |
| | | RijndaelManaged aes = new RijndaelManaged(); |
| | | aes.KeySize = 256; |
| | | aes.BlockSize = 128; |
| | | aes.Mode = CipherMode.CBC; |
| | | aes.Padding = PaddingMode.None; |
| | | aes.Key = Key; |
| | | aes.IV = Iv; |
| | | var decrypt = aes.CreateDecryptor(aes.Key, aes.IV); |
| | | byte[] xBuff = null; |
| | | using (var ms = new MemoryStream()) |
| | | { |
| | | using (var cs = new CryptoStream(ms, decrypt, CryptoStreamMode.Write)) |
| | | { |
| | | byte[] xXml = Convert.FromBase64String(Input); |
| | | byte[] msg = new byte[xXml.Length + 32 - xXml.Length % 32]; |
| | | Array.Copy(xXml, msg, xXml.Length); |
| | | cs.Write(xXml, 0, xXml.Length); |
| | | } |
| | | xBuff = decode2(ms.ToArray()); |
| | | } |
| | | return xBuff; |
| | | } |
| | | private static byte[] decode2(byte[] decrypted) |
| | | { |
| | | int pad = (int)decrypted[decrypted.Length - 1]; |
| | | if (pad < 1 || pad > 32) |
| | | { |
| | | pad = 0; |
| | | } |
| | | byte[] res = new byte[decrypted.Length - pad]; |
| | | Array.Copy(decrypted, 0, res, 0, decrypted.Length - pad); |
| | | return res; |
| | | } |
| | | } |
| | | } |
| New file |
| | |
| | | 注意事项 |
| | | 1.Cryptography.cs文件封装了AES加解密过程,用户无须关心具体实现。WXBizMsgCrypt.cs文件提供了用户接入企业微信的两个接口,Sample.cs文件提供了如何使用这两个接口的示例。 |
| | | 2.WXBizMsgCrypt.cs封装了DecryptMsg, EncryptMsg两个接口,分别用于收到用户回复消息的解密以及开发者回复消息的加密过程。使用方法可以参考Sample.cs文件。 |
| | | 3.加解密协议请参考微信公众平台官方文档。 |
| New file |
| | |
| | | using System; |
| | | using System.Collections.Generic; |
| | | using System.Linq; |
| | | using System.Text; |
| | | using System.Xml; |
| | | namespace MsgCryptTest |
| | | { |
| | | class Sample |
| | | { |
| | | |
| | | static void Main(string[] args) |
| | | { |
| | | //公众平台上开发者设置的token, appID, EncodingAESKey |
| | | string sToken = "QDG6eK"; |
| | | string sAppID = "wx5823bf96d3bd56c7"; |
| | | string sEncodingAESKey = "jWmYm7qr5nMoAUwZRjGtBxmz3KA1tkAj3ykkR6q2B2C"; |
| | | |
| | | Tencent.WXBizMsgCrypt wxcpt = new Tencent.WXBizMsgCrypt(sToken, sEncodingAESKey, sAppID); |
| | | |
| | | /* 1. 对用户回复的数据进行解密。 |
| | | * 用户回复消息或者点击事件响应时,企业会收到回调消息,假设企业收到的推送消息: |
| | | * POST /cgi-bin/wxpush? msg_signature=477715d11cdb4164915debcba66cb864d751f3e6×tamp=1409659813&nonce=1372623149 HTTP/1.1 |
| | | Host: qy.weixin.qq.com |
| | | Content-Length: 613 |
| | | * |
| | | * <xml> |
| | | <ToUserName><![CDATA[wx5823bf96d3bd56c7]]></ToUserName> |
| | | <Encrypt><![CDATA[RypEvHKD8QQKFhvQ6QleEB4J58tiPdvo+rtK1I9qca6aM/wvqnLSV5zEPeusUiX5L5X/0lWfrf0QADHHhGd3QczcdCUpj911L3vg3W/sYYvuJTs3TUUkSUXxaccAS0qhxchrRYt66wiSpGLYL42aM6A8dTT+6k4aSknmPj48kzJs8qLjvd4Xgpue06DOdnLxAUHzM6+kDZ+HMZfJYuR+LtwGc2hgf5gsijff0ekUNXZiqATP7PF5mZxZ3Izoun1s4zG4LUMnvw2r+KqCKIw+3IQH03v+BCA9nMELNqbSf6tiWSrXJB3LAVGUcallcrw8V2t9EL4EhzJWrQUax5wLVMNS0+rUPA3k22Ncx4XXZS9o0MBH27Bo6BpNelZpS+/uh9KsNlY6bHCmJU9p8g7m3fVKn28H3KDYA5Pl/T8Z1ptDAVe0lXdQ2YoyyH2uyPIGHBZZIs2pDBS8R07+qN+E7Q==]]></Encrypt> |
| | | </xml> |
| | | */ |
| | | string sReqMsgSig = "477715d11cdb4164915debcba66cb864d751f3e6"; |
| | | string sReqTimeStamp = "1409659813"; |
| | | string sReqNonce = "1372623149"; |
| | | string sReqData = "<xml><ToUserName><![CDATA[wx5823bf96d3bd56c7]]></ToUserName><Encrypt><![CDATA[RypEvHKD8QQKFhvQ6QleEB4J58tiPdvo+rtK1I9qca6aM/wvqnLSV5zEPeusUiX5L5X/0lWfrf0QADHHhGd3QczcdCUpj911L3vg3W/sYYvuJTs3TUUkSUXxaccAS0qhxchrRYt66wiSpGLYL42aM6A8dTT+6k4aSknmPj48kzJs8qLjvd4Xgpue06DOdnLxAUHzM6+kDZ+HMZfJYuR+LtwGc2hgf5gsijff0ekUNXZiqATP7PF5mZxZ3Izoun1s4zG4LUMnvw2r+KqCKIw+3IQH03v+BCA9nMELNqbSf6tiWSrXJB3LAVGUcallcrw8V2t9EL4EhzJWrQUax5wLVMNS0+rUPA3k22Ncx4XXZS9o0MBH27Bo6BpNelZpS+/uh9KsNlY6bHCmJU9p8g7m3fVKn28H3KDYA5Pl/T8Z1ptDAVe0lXdQ2YoyyH2uyPIGHBZZIs2pDBS8R07+qN+E7Q==]]></Encrypt></xml>"; |
| | | string sMsg = ""; //解析之后的明文 |
| | | int ret = 0; |
| | | ret = wxcpt.DecryptMsg(sReqMsgSig, sReqTimeStamp, sReqNonce, sReqData, ref sMsg); |
| | | if (ret != 0) |
| | | { |
| | | System.Console.WriteLine("ERR: Decrypt fail, ret: " + ret); |
| | | return; |
| | | } |
| | | System.Console.WriteLine(sMsg); |
| | | |
| | | |
| | | /* |
| | | * 2. 企业回复用户消息也需要加密和拼接xml字符串。 |
| | | * 假设企业需要回复用户的消息为: |
| | | * <xml> |
| | | * <ToUserName><![CDATA[mycreate]]></ToUserName> |
| | | * <FromUserName><![CDATA[wx5823bf96d3bd56c7]]></FromUserName> |
| | | * <CreateTime>1348831860</CreateTime> |
| | | <MsgType><![CDATA[text]]></MsgType> |
| | | * <Content><![CDATA[this is a test]]></Content> |
| | | * <MsgId>1234567890123456</MsgId> |
| | | * </xml> |
| | | * 生成xml格式的加密消息过程为: |
| | | */ |
| | | string sRespData = "<xml><ToUserName><![CDATA[mycreate]]></ToUserName><FromUserName><![CDATA[wx582测试一下中文的情况,消息长度是按字节来算的396d3bd56c7]]></FromUserName><CreateTime>1348831860</CreateTime><MsgType><![CDATA[text]]></MsgType><Content><![CDATA[this is a test]]></Content><MsgId>1234567890123456</MsgId></xml>"; |
| | | string sEncryptMsg = ""; //xml格式的密文 |
| | | ret = wxcpt.EncryptMsg(sRespData, sReqTimeStamp, sReqNonce, ref sEncryptMsg); |
| | | System.Console.WriteLine("sEncryptMsg"); |
| | | System.Console.WriteLine(sEncryptMsg); |
| | | |
| | | /*测试: |
| | | * 将sEncryptMsg解密看看是否是原文 |
| | | * */ |
| | | XmlDocument doc = new XmlDocument(); |
| | | doc.LoadXml(sEncryptMsg); |
| | | XmlNode root = doc.FirstChild; |
| | | string sig = root["MsgSignature"].InnerText; |
| | | string enc = root["Encrypt"].InnerText; |
| | | string timestamp = root["TimeStamp"].InnerText; |
| | | string nonce = root["Nonce"].InnerText; |
| | | string stmp = ""; |
| | | ret = wxcpt.DecryptMsg(sig, timestamp, nonce, sEncryptMsg, ref stmp); |
| | | System.Console.WriteLine("stemp"); |
| | | System.Console.WriteLine(stmp + ret); |
| | | return; |
| | | } |
| | | } |
| | | } |
| New file |
| | |
| | | using System; |
| | | using System.Collections.Generic; |
| | | using System.Linq; |
| | | using System.Text; |
| | | using System.Xml; |
| | | using System.Collections; |
| | | //using System.Web; |
| | | using System.Security.Cryptography; |
| | | //-40001 : 签名验证错误 |
| | | //-40002 : xml解析失败 |
| | | //-40003 : sha加密生成签名失败 |
| | | //-40004 : AESKey 非法 |
| | | //-40005 : appid 校验错误 |
| | | //-40006 : AES 加密失败 |
| | | //-40007 : AES 解密失败 |
| | | //-40008 : 解密后得到的buffer非法 |
| | | //-40009 : base64加密异常 |
| | | //-40010 : base64解密异常 |
| | | namespace Tencent |
| | | { |
| | | public class WXBizMsgCrypt |
| | | { |
| | | string m_sToken; |
| | | string m_sEncodingAESKey; |
| | | string m_sAppID; |
| | | enum WXBizMsgCryptErrorCode |
| | | { |
| | | WXBizMsgCrypt_OK = 0, |
| | | WXBizMsgCrypt_ValidateSignature_Error = -40001, |
| | | WXBizMsgCrypt_ParseXml_Error = -40002, |
| | | WXBizMsgCrypt_ComputeSignature_Error = -40003, |
| | | WXBizMsgCrypt_IllegalAesKey = -40004, |
| | | WXBizMsgCrypt_ValidateAppid_Error = -40005, |
| | | WXBizMsgCrypt_EncryptAES_Error = -40006, |
| | | WXBizMsgCrypt_DecryptAES_Error = -40007, |
| | | WXBizMsgCrypt_IllegalBuffer = -40008, |
| | | WXBizMsgCrypt_EncodeBase64_Error = -40009, |
| | | WXBizMsgCrypt_DecodeBase64_Error = -40010 |
| | | }; |
| | | |
| | | //构造函数 |
| | | // @param sToken: 公众平台上,开发者设置的Token |
| | | // @param sEncodingAESKey: 公众平台上,开发者设置的EncodingAESKey |
| | | // @param sAppID: 公众帐号的appid |
| | | public WXBizMsgCrypt(string sToken, string sEncodingAESKey, string sAppID) |
| | | { |
| | | m_sToken = sToken; |
| | | m_sAppID = sAppID; |
| | | m_sEncodingAESKey = sEncodingAESKey; |
| | | } |
| | | |
| | | |
| | | // 检验消息的真实性,并且获取解密后的明文 |
| | | // @param sMsgSignature: 签名串,对应URL参数的msg_signature |
| | | // @param sTimeStamp: 时间戳,对应URL参数的timestamp |
| | | // @param sNonce: 随机串,对应URL参数的nonce |
| | | // @param sPostData: 密文,对应POST请求的数据 |
| | | // @param sMsg: 解密后的原文,当return返回0时有效 |
| | | // @return: 成功0,失败返回对应的错误码 |
| | | public int DecryptMsg(string sMsgSignature, string sTimeStamp, string sNonce, string sPostData, ref string sMsg) |
| | | { |
| | | if (m_sEncodingAESKey.Length!=43) |
| | | { |
| | | return (int)WXBizMsgCryptErrorCode.WXBizMsgCrypt_IllegalAesKey; |
| | | } |
| | | XmlDocument doc = new XmlDocument(); |
| | | XmlNode root; |
| | | string sEncryptMsg; |
| | | try |
| | | { |
| | | doc.LoadXml(sPostData); |
| | | root = doc.FirstChild; |
| | | sEncryptMsg = root["Encrypt"].InnerText; |
| | | } |
| | | catch (Exception) |
| | | { |
| | | return (int)WXBizMsgCryptErrorCode.WXBizMsgCrypt_ParseXml_Error; |
| | | } |
| | | //verify signature |
| | | int ret = 0; |
| | | ret = VerifySignature(m_sToken, sTimeStamp, sNonce, sEncryptMsg, sMsgSignature); |
| | | if (ret != 0) |
| | | return ret; |
| | | //decrypt |
| | | string cpid = ""; |
| | | try |
| | | { |
| | | sMsg = Cryptography.AES_decrypt(sEncryptMsg, m_sEncodingAESKey, ref cpid); |
| | | } |
| | | catch (FormatException) |
| | | { |
| | | return (int)WXBizMsgCryptErrorCode.WXBizMsgCrypt_DecodeBase64_Error; |
| | | } |
| | | catch (Exception) |
| | | { |
| | | return (int)WXBizMsgCryptErrorCode.WXBizMsgCrypt_DecryptAES_Error; |
| | | } |
| | | if (cpid != m_sAppID) |
| | | return (int)WXBizMsgCryptErrorCode.WXBizMsgCrypt_ValidateAppid_Error; |
| | | return 0; |
| | | } |
| | | |
| | | //将企业号回复用户的消息加密打包 |
| | | // @param sReplyMsg: 企业号待回复用户的消息,xml格式的字符串 |
| | | // @param sTimeStamp: 时间戳,可以自己生成,也可以用URL参数的timestamp |
| | | // @param sNonce: 随机串,可以自己生成,也可以用URL参数的nonce |
| | | // @param sEncryptMsg: 加密后的可以直接回复用户的密文,包括msg_signature, timestamp, nonce, encrypt的xml格式的字符串, |
| | | // 当return返回0时有效 |
| | | // return:成功0,失败返回对应的错误码 |
| | | public int EncryptMsg(string sReplyMsg, string sTimeStamp, string sNonce, ref string sEncryptMsg) |
| | | { |
| | | if (m_sEncodingAESKey.Length!=43) |
| | | { |
| | | return (int)WXBizMsgCryptErrorCode.WXBizMsgCrypt_IllegalAesKey; |
| | | } |
| | | string raw = ""; |
| | | try |
| | | { |
| | | raw = Cryptography.AES_encrypt(sReplyMsg, m_sEncodingAESKey, m_sAppID); |
| | | } |
| | | catch (Exception) |
| | | { |
| | | return (int)WXBizMsgCryptErrorCode.WXBizMsgCrypt_EncryptAES_Error; |
| | | } |
| | | string MsgSigature = ""; |
| | | int ret = 0; |
| | | ret = GenarateSinature(m_sToken, sTimeStamp, sNonce, raw, ref MsgSigature); |
| | | if (0 != ret) |
| | | return ret; |
| | | sEncryptMsg = ""; |
| | | |
| | | string EncryptLabelHead = "<Encrypt><![CDATA["; |
| | | string EncryptLabelTail = "]]></Encrypt>"; |
| | | string MsgSigLabelHead = "<MsgSignature><![CDATA["; |
| | | string MsgSigLabelTail = "]]></MsgSignature>"; |
| | | string TimeStampLabelHead = "<TimeStamp><![CDATA["; |
| | | string TimeStampLabelTail = "]]></TimeStamp>"; |
| | | string NonceLabelHead = "<Nonce><![CDATA["; |
| | | string NonceLabelTail = "]]></Nonce>"; |
| | | sEncryptMsg = sEncryptMsg + "<xml>" + EncryptLabelHead + raw + EncryptLabelTail; |
| | | sEncryptMsg = sEncryptMsg + MsgSigLabelHead + MsgSigature + MsgSigLabelTail; |
| | | sEncryptMsg = sEncryptMsg + TimeStampLabelHead + sTimeStamp + TimeStampLabelTail; |
| | | sEncryptMsg = sEncryptMsg + NonceLabelHead + sNonce + NonceLabelTail; |
| | | sEncryptMsg += "</xml>"; |
| | | return 0; |
| | | } |
| | | |
| | | public class DictionarySort : System.Collections.IComparer |
| | | { |
| | | public int Compare(object oLeft, object oRight) |
| | | { |
| | | string sLeft = oLeft as string; |
| | | string sRight = oRight as string; |
| | | int iLeftLength = sLeft.Length; |
| | | int iRightLength = sRight.Length; |
| | | int index = 0; |
| | | while (index < iLeftLength && index < iRightLength) |
| | | { |
| | | if (sLeft[index] < sRight[index]) |
| | | return -1; |
| | | else if (sLeft[index] > sRight[index]) |
| | | return 1; |
| | | else |
| | | index++; |
| | | } |
| | | return iLeftLength - iRightLength; |
| | | |
| | | } |
| | | } |
| | | //Verify Signature |
| | | private static int VerifySignature(string sToken, string sTimeStamp, string sNonce, string sMsgEncrypt, string sSigture) |
| | | { |
| | | string hash = ""; |
| | | int ret = 0; |
| | | ret = GenarateSinature(sToken, sTimeStamp, sNonce, sMsgEncrypt, ref hash); |
| | | if (ret != 0) |
| | | return ret; |
| | | //System.Console.WriteLine(hash); |
| | | if (hash == sSigture) |
| | | return 0; |
| | | else |
| | | { |
| | | return (int)WXBizMsgCryptErrorCode.WXBizMsgCrypt_ValidateSignature_Error; |
| | | } |
| | | } |
| | | |
| | | public static int GenarateSinature(string sToken, string sTimeStamp, string sNonce, string sMsgEncrypt ,ref string sMsgSignature) |
| | | { |
| | | ArrayList AL = new ArrayList(); |
| | | AL.Add(sToken); |
| | | AL.Add(sTimeStamp); |
| | | AL.Add(sNonce); |
| | | AL.Add(sMsgEncrypt); |
| | | AL.Sort(new DictionarySort()); |
| | | string raw = ""; |
| | | for (int i = 0; i < AL.Count; ++i) |
| | | { |
| | | raw += AL[i]; |
| | | } |
| | | |
| | | SHA1 sha; |
| | | ASCIIEncoding enc; |
| | | string hash = ""; |
| | | try |
| | | { |
| | | sha = new SHA1CryptoServiceProvider(); |
| | | enc = new ASCIIEncoding(); |
| | | byte[] dataToHash = enc.GetBytes(raw); |
| | | byte[] dataHashed = sha.ComputeHash(dataToHash); |
| | | hash = BitConverter.ToString(dataHashed).Replace("-", ""); |
| | | hash = hash.ToLower(); |
| | | } |
| | | catch (Exception) |
| | | { |
| | | return (int)WXBizMsgCryptErrorCode.WXBizMsgCrypt_ComputeSignature_Error; |
| | | } |
| | | sMsgSignature = hash; |
| | | return 0; |
| | | } |
| | | } |
| | | } |
| | |
| | | using ApiTools.Core; |
| | | using Aop.Api.Domain; |
| | | using ApiTools.Core; |
| | | using Furion.DataEncryption; |
| | | using Furion.DynamicApiController; |
| | | using Furion.FriendlyException; |
| | | using MediatR; |
| | | using Microsoft.AspNetCore.Authorization; |
| | | using Microsoft.AspNetCore.Mvc; |
| | | using Microsoft.Extensions.Options; |
| | | using Newtonsoft.Json; |
| | | using Org.BouncyCastle.Ocsp; |
| | | using System.Buffers.Binary; |
| | | using System.Security.Cryptography; |
| | | using System.Text; |
| | | using System.Threading.Tasks; |
| | | |
| | | namespace ApiTools.Web.Entry.Controllers |
| | | { |
| | | [Route("api/common/wxmp")] |
| | | public class WxmpController( |
| | | WxmpUtils utils, |
| | | IOptions<WxmpOptions> options |
| | | IOptions<WxmpOptions> options, |
| | | IMediator mediator |
| | | ) : ControllerBase |
| | | { |
| | | private readonly WxmpUtils utils = utils; |
| | | private readonly IOptions<WxmpOptions> options = options; |
| | | private readonly IMediator mediator = mediator; |
| | | |
| | | [HttpGet("subscribMessageNotify")] |
| | | [AllowAnonymous] |
| | | [NonUnify] |
| | | public IActionResult SubscribMessageNotify([FromQuery] WxmpSubscribMessageNotifyRequestQuery request) |
| | | public IActionResult SubscribMessageNotify([FromQuery] WxmpSubscribMessageNotifyRequestQuery query) |
| | | { |
| | | var @params = new[] |
| | | { |
| | | options.Value.SubscribMessage.Token, |
| | | request.Timestamp, |
| | | request.Nonce |
| | | query.timestamp, |
| | | query.nonce |
| | | } |
| | | .OrderBy(p => p) |
| | | .ToArray(); |
| | | var text = string.Concat(@params); |
| | | if (SHA1Encryption.Compare(text, request.Signature, true)) |
| | | if (SHA1Encryption.Compare(text, query.signature, true)) |
| | | { |
| | | return Content(request.Echostr); |
| | | return Content(query.echostr); |
| | | } |
| | | else |
| | | { |
| | | return Unauthorized("验签失败"); |
| | | } |
| | | } |
| | | |
| | | [HttpPost("subscribMessageNotify/{code}")] |
| | | [AllowAnonymous] |
| | | [NonUnify] |
| | | public async Task<IActionResult> SubscribMessageNotify([FromRoute] string code, [FromQuery] WxmpSubscribMessageNotifyRequestQuery query, [FromBody] WxmpSubscribMessageNotifyRequestBody body) |
| | | { |
| | | var appId = options.Value.Items.FirstOrDefault(it => it.Code == code).AppId; |
| | | Tencent.WXBizMsgCrypt wxcpt = new Tencent.WXBizMsgCrypt(options.Value.SubscribMessage.Token, options.Value.SubscribMessage.EncodingAESKey, appId); |
| | | var data = $"<xml><ToUserName><![CDATA[{body.ToUserName}]]></ToUserName><Encrypt><![CDATA[{body.Encrypt}]]></Encrypt></xml>"; |
| | | var content = ""; |
| | | var error = wxcpt.DecryptMsg(query.msg_signature, query.timestamp, query.nonce, data, ref content); |
| | | if (error != 0) return Unauthorized("验签失败"); |
| | | var command = content.JsonTo<WxmpSubscribMessageNotifyCommand>(); |
| | | command.Code = code; |
| | | command.OpenId = query.openid; |
| | | await mediator.Send(command); |
| | | return Content(query.echostr); |
| | | } |
| | | } |
| | | } |
| | |
| | | "EnvVersion": "trial" |
| | | }, |
| | | { |
| | | "Code": "Supplier", |
| | | "AppId": "wxc47d6f255e7d0566", |
| | | "AppSecret": "9e02d66cf005fa2f4aefb2e4dc1fced5", |
| | | "EnvVersion": "trial" |
| | | }, |
| | | { |
| | | "Code": "Public", |
| | | "AppId": "wxf940ff1d35a98493", |
| | | "AppSecret": "9a132eda735bc925200b0e215cffe20a", |
| | |
| | | } |
| | | ], |
| | | "SubscribMessage": { |
| | | "Url": "http://118.178.252.28:8780/api/common/wxmp/subscribMessageNotify", |
| | | "Token": "8Uu6CZ9KM2CAr3Q3O0YdWUYPfcXFhgMK", |
| | | "EncodingAESKey": "tbBkUB7nCgZlfton3aKMlfzHSm7QdWgnpKFibl6sjn7" |
| | | }, |
| | | } |
| | | }, |
| | | "Task": { |
| | | "SettlementTime": "T0" |