微信公众号开发--.net core接入
阅读原文时间:2023年07月08日阅读:4

  .net进行微信公众号开发的例子好像比较少,这里做个笔记

  首先,我们需要让微信能访问到我们的项目,所以要么需要有一个可以部署项目的连接到公网下的服务器,要么可以通过端口转发将请求转发到我们的项目,总之,就是要让微信服务器能访问到我们的项目。

  另外,需要注意一下,微信回调通知的地址目前只支持80端口和443端口,所以一般的,我们都需要做个虚拟路径

  其他的就不多说了,具体配置可以在微信公众号的开发文档中接入:https://developers.weixin.qq.com/doc/offiaccount/Basic_Information/Access_Overview.html

  接入过程中会遇到很多坑,什么Url超时,Token验证错误等等,反正就是一些触不及防,当我们接入开发完后,发现我们使用的是明文传送,那当然就不行了,改成密文又要使用AES加密,反正接入不知道遇到多少坑

  下面贴出我接入的代码,复制一下,稍稍修改就可以用了,可直接验证接入,支持明文密文传输:

  一个加密解密辅助类:  

using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net;
using System.Security.Cryptography;
using System.Text;
using System.Threading.Tasks;
using System.Xml;

namespace DemoApi
{
public sealed class EncryptHelper
{
private EncryptHelper() { }

    /// <summary>  
    /// Md5加密  
    /// </summary>  
    /// <param name="text"></param>  
    /// <returns></returns>  
    public static string Md5Encrypt(string text)  
    {  
        //MD5加密  
        var md5 = MD5.Create();  
        var bs = md5.ComputeHash(Encoding.UTF8.GetBytes(text));  
        var sb = new StringBuilder();  
        foreach (byte b in bs)  
        {  
            sb.Append(b.ToString("x2"));  
        }  
        //所有字符转为大写  
        return sb.ToString().ToUpper();  
    }  
    /// <summary>  
    /// HMAC-SHA1加密算法  
    /// </summary>  
    /// <param name="str">加密字符串</param>  
    /// <returns></returns>  
    public static string Sha1Encrypt(string str)  
    {  
        var sha1 = SHA1.Create();  
        var hash = sha1.ComputeHash(Encoding.Default.GetBytes(str));  
        //return BitConverter.ToString(hash).Replace("-", "");  
        string byte2String = null;  
        for (int i = 0; i < hash.Length; i++)  
        {  
            byte2String += hash\[i\].ToString("x2");  
        }  
        return byte2String;  
    }  
    /// <summary>  
    /// AES加密  
    /// </summary>  
    /// <param name="text">加密字符</param>  
    /// <param name="encodingAESKey">加密的密码</param>  
    /// <param name="appid">appId</param>  
    /// <returns></returns>  
    public static string AESEncrypt(string text, string encodingAESKey, string appid)  
    {  
        if (string.IsNullOrEmpty(text))  
        {  
            return text;  
        }

        byte\[\] key;  
        key = Convert.FromBase64String(encodingAESKey + "=");  
        byte\[\] iv = new byte\[16\];  
        Array.Copy(key, iv, 16);

        #region 生成随机值  
        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";  
        string\[\] arr = codeSerial.Split(',');  
        string code = "";  
        int randValue = -1;  
        Random rand = new Random(unchecked((int)DateTime.Now.Ticks));  
        for (int i = 0; i < 16; i++)  
        {  
            randValue = rand.Next(0, arr.Length - 1);  
            code += arr\[randValue\];  
        }  
        #endregion

        byte\[\] bRand = Encoding.UTF8.GetBytes(code);  
        byte\[\] bAppid = Encoding.UTF8.GetBytes(appid);  
        byte\[\] btmpMsg = Encoding.UTF8.GetBytes(text);

        int outval = 0, inval = btmpMsg.Length;  
        for (int i = 0; i < 4; i++)  
            outval = (outval << 8) + ((inval >> (i \* 8)) & 255);

        byte\[\] bMsgLen = BitConverter.GetBytes(outval);  
        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);

        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;  
        byte\[\] msg = new byte\[bMsg.Length + 32 - bMsg.Length % 32\];  
        Array.Copy(bMsg, msg, bMsg.Length);

        #region 自己进行PKCS7补位,用系统自己带的不行,微信加密要使用这个  
        int block\_size = 32;  
        // 计算需要填充的位数  
        int amount\_to\_pad = block\_size - (bMsg.Length % block\_size);  
        if (amount\_to\_pad == 0)  
        {  
            amount\_to\_pad = block\_size;  
        }  
        // 获得补位所用的字符  
        char pad\_chr = (char)(byte)(amount\_to\_pad & 0xFF);  
        string tmp = "";  
        for (int index = 0; index < amount\_to\_pad; index++)  
        {  
            tmp += pad\_chr;  
        }  
        byte\[\] pad = Encoding.UTF8.GetBytes(tmp);

        Array.Copy(pad, 0, msg, bMsg.Length, pad.Length);

        using (var ms = new MemoryStream())  
        {  
            using (var cs = new CryptoStream(ms, encrypt, CryptoStreamMode.Write))  
            {  
                cs.Write(msg, 0, msg.Length);  
            }  
            xBuff = ms.ToArray();  
        }  
        #endregion

        #region 注释的也是一种方法,效果一样,微信加密不能使用这个!!!!  
        //ICryptoTransform transform = aes.CreateEncryptor();  
        //xBuff = transform.TransformFinalBlock(msg, 0, msg.Length);  
        #endregion

        string output = Convert.ToBase64String(xBuff);  
        return output;  
    }  
    /// <summary>  
    /// AES解密  
    /// </summary>  
    /// <param name="encryptText">密文</param>  
    /// <param name="encodingAESKey">秘钥</param>  
    /// <param name="appid"></param>  
    /// <returns></returns>  
    public static string AESDecrypt(string encryptText, string encodingAESKey, out string appid)  
    {  
        if (string.IsNullOrEmpty(encryptText))  
        {  
            appid = "";  
            return encryptText;  
        }

        byte\[\] key;  
        key = Convert.FromBase64String(encodingAESKey + "=");  
        byte\[\] iv = new byte\[16\];  
        Array.Copy(key, iv, 16);  
        byte\[\] btmpMsg = null;

        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);  
        using (var ms = new MemoryStream())  
        {  
            using (var cs = new CryptoStream(ms, decrypt, CryptoStreamMode.Write))  
            {  
                byte\[\] xXml = Convert.FromBase64String(encryptText);  
                byte\[\] msg = new byte\[xXml.Length + 32 - xXml.Length % 32\];  
                Array.Copy(xXml, msg, xXml.Length);  
                cs.Write(xXml, 0, xXml.Length);  
            }  
            var decrypted = ms.ToArray();  
            int pad = (int)decrypted\[decrypted.Length - 1\];  
            if (pad < 1 || pad > 32)  
            {  
                pad = 0;  
            }  
            btmpMsg = new byte\[decrypted.Length - pad\];  
            Array.Copy(decrypted, 0, btmpMsg, 0, decrypted.Length - pad);  
        }

        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;  
    }  
    /// <summary>  
    /// AES解密  
    /// </summary>  
    /// <param name="encryptText">密文</param>  
    /// <param name="encodingAESKey">秘钥</param>  
    /// <param name="appid"></param>  
    /// <returns></returns>  
    public static string AESDecrypt(string encryptText, string encodingAESKey)  
    {  
        return AESDecrypt(encryptText, encodingAESKey, out \_);  
    }  
}

public class CharSort : IComparer  
{  
    public int Compare(object left, object right)  
    {  
        string sLeft = left as string;  
        string sRight = right as string;  
        int leftLength = sLeft.Length;  
        int rightLength = sRight.Length;  
        int index = 0;  
        while (index < leftLength && index < rightLength)  
        {  
            if (sLeft\[index\] < sRight\[index\])  
                return -1;  
            else if (sLeft\[index\] > sRight\[index\])  
                return 1;  
            else  
                index++;  
        }  
        return leftLength - rightLength;  
    }  
}  

}

EncryptHelper

  接入接口核心代码: 

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net.Http;
using System.Threading.Tasks;
using System.Web;
using System.Xml.Linq;
using Microsoft.AspNetCore.Mvc;

namespace DemoApi.Controllers
{
[Route("api/[controller]")]
[ApiController]
public class WxController : ControllerBase
{
///

/// 生成签名 ///
///
///
[NonAction]
private string MakeSign(params string[] args)
{
//字典排序
Array.Sort(args);
string tmpStr = string.Join("", args);
//字符加密
var sha1 = EncryptHelper.Sha1Encrypt(tmpStr);
return sha1;
}
/// /// 生成消息签名 ///
///
///
[NonAction]
private string MakeMsgSign(params string[] args)
{
//字典排序
Array.Sort(args, new CharSort());
string tmpStr = string.Join("", args);
//字符加密
var sha1 = EncryptHelper.Sha1Encrypt(tmpStr);
return sha1;
}
/// /// 微信回调统一接口 ///
///
[HttpGet, HttpPost]
public string Service()
{
//获取配置文件中的数据
var token = "";
var encodingAESKey = "";
var appId = "";

        bool isGet = string.Equals(HttpContext.Request.Method, HttpMethod.Get.Method, StringComparison.OrdinalIgnoreCase);  
        bool isPost = string.Equals(HttpContext.Request.Method, HttpMethod.Post.Method, StringComparison.OrdinalIgnoreCase);  
        if (!isGet && !isPost)  
        {  
            return "";  
        }

        bool isEncrypt = false;  
        try  
        {  
            var query = HttpContext.Request.QueryString.ToString();  
            string msg\_signature = "", nonce = "", timestamp = "", encrypt\_type = "", signature = "", echostr = "";

            if (!string.IsNullOrEmpty(query))//需要验证签名  
            {  
                var collection = HttpUtility.ParseQueryString(query);  
                msg\_signature = collection\["msg\_signature"\]?.Trim();  
                nonce = collection\["nonce"\]?.Trim();  
                timestamp = collection\["timestamp"\]?.Trim();  
                encrypt\_type = collection\["encrypt\_type"\]?.Trim();  
                signature = collection\["signature"\]?.Trim();  
                echostr = collection\["echostr"\]?.Trim();

                if (!string.IsNullOrEmpty(encrypt\_type))//有使用加密  
                {  
                    if (!string.Equals(encrypt\_type, "aes", StringComparison.OrdinalIgnoreCase))//只支持AES加密方式  
                    {  
                        return "";  
                    }  
                    isEncrypt = true;  
                }  
            }

            //先验证签名  
            if (!string.IsNullOrEmpty(signature))  
            {  
                //字符加密  
                var sha1 = MakeSign(nonce, timestamp, token);  
                if (!sha1.Equals(signature, StringComparison.OrdinalIgnoreCase))//验证不通过  
                {  
                    return "";  
                }

                if (isGet)//是否Get请求,如果true,那么就认为是修改服务器回调配置信息  
                {  
                    return echostr;  
                }  
            }  
            else  
            {  
                return "";//没有签名,请求直接返回  
            }

            var body = new StreamReader(HttpContext.Request.Body).ReadToEnd();

            if (isEncrypt)  
            {  
                XDocument doc = XDocument.Parse(body);  
                var encrypt = doc.Element("xml").Element("Encrypt");

                //验证消息签名  
                if (!string.IsNullOrEmpty(msg\_signature))  
                {  
                    //消息加密  
                    var sha1 = MakeMsgSign(nonce, timestamp, encrypt.Value, token);  
                    if (!sha1.Equals(msg\_signature, StringComparison.OrdinalIgnoreCase))//验证不通过  
                    {  
                        return "";  
                    }  
                }

                body = EncryptHelper.AESDecrypt(encrypt.Value, encodingAESKey);//解密  
            }

            if (!string.IsNullOrEmpty(body))  
            {  
                //  
                //在这里根据body中的MsgType和Even来区分消息,然后来处理不同的业务逻辑  
                //  
                //

                //result是上面逻辑处理完成之后的待返回结果,如返回文本消息:  
                var result = @"<xml>  
                                  <ToUserName><!\[CDATA\[toUser\]\]></ToUserName>  
                                  <FromUserName><!\[CDATA\[fromUser\]\]></FromUserName>  
                                  <CreateTime>12345678</CreateTime>  
                                  <MsgType><!\[CDATA\[text\]\]></MsgType>  
                                  <Content><!\[CDATA\[你好\]\]></Content>  
                                </xml>";  
                if (!string.IsNullOrEmpty(result))  
                {  
                    if (isEncrypt)  
                    {  
                        result = EncryptHelper.AESEncrypt(result, encodingAESKey, appId);  
                        var \_msg\_signature = MakeMsgSign(nonce, timestamp, result, token);  
                        result = $@"<xml>  
                                                <Encrypt><!\[CDATA\[{result}\]\]></Encrypt>  
                                                <MsgSignature>{\_msg\_signature}</MsgSignature>  
                                                <TimeStamp>{timestamp}</TimeStamp>  
                                                <Nonce>{nonce}</Nonce>  
                                            </xml>";  
                    }  
                    return result;  
                }

                //如果这里我们的处理逻辑需要花费较长时间,可以这里先返回空(""),然后使用异步去处理业务逻辑,  
                //异步处理完后,调用微信的客服消息接口通知微信服务器  
            }  
        }  
        catch (Exception ex)  
        {  
            //记录异常日志  
        }

        return "";  
    }

}  

}

WxController

  如果只是接入,只需复制上面的接口代码,修改一下token,encodingAESKey,appId的值,然后使用接入的回调Url:http://XXX.XXXX.com/api/Wx就可以了

  如果要处理微信通知过来的消息,可根据上面的接口自行处理业务逻辑,不过需要注意的是,微信回调接口只会等待5秒,5秒后断开,并重复3次,所以如果这里业务逻辑处理时间比较长的话,建议使用异步,在异步处理完之后,使用客服消息接口进行结果通知

  另外开发过程中,可以使用微信公众平台接口调试工具进行调试:https://mp.weixin.qq.com/debug/cgi-bin/apiinfo?t=index

  有一点注意一下,一般的,我们写接口时,为了保证接口返回的数据有一定格式,所以会写一些过滤器,对接口进行一个包装,比如我们会将结果包装成以下结构:  

{
"result": "success",
"success": true,
"error": null,
"message": ""
}

  但是这个微信调用的接口不能包装,要返回微信指定的格式才行,这个切记!!!否则会返回Token验证失败的错误!