坑你没商量之c#微信回调中核实订单的签名验证

        最近公司app项目的开发,有幸参与到该项目中,并且由我独立负责服务端的开发。本文主要讲下在微信回调开发遇到的问题以及解决方案!而在前端开发方面都是由另外一位同事主导,我只负责返回请求参数,因此这里不做讨论。说到这里不得不吐槽下微信文档的说明,大部分都是云里雾里的。


      首先从官方开放平台下载服务端c#开发sdk文件。然后找到里头一个 “支付结果通知回调处理类” ResultNotify.cs文件。主要处理流程都在下面这个方法里头。从这个方法里头衍生出其他的方法来。

坑你没商量之c#微信回调中核实订单的签名验证_第1张图片


      本来只有一个应用的话,也是不必要在来发文章讨论的,直接跟着sdk写好的方法走就成,但是我们项目最终是要有两个app的,也因此申请了两个微信移动应用,充值分别充入对应的商户里头。官方文档表明了 “通知url必须为直接可访问的url,不能携带参数。示例:notify_url:“https://pay.weixin.qq.com/wxpay/pay.action”” 因此这导致回调都是分开的。一开始我想要直接使用一个配置文件【config.cs】,但是验证一直通不过。最后没办法,只得分开了两份代码,都具备各自的配置文件和其他方法。


    后来实在想不通,既然他的config.cs可以通过WxPayData进行重新赋值,没道理需要分开写。因此为了测试,我把以前充值的单号找出来,重新写了两个查询订单的方法,反复写测试返回参数比对查看。最终才发现问题的关键。


      1、首先不得不说下官方封装的HttpService.Post(xml, url, false, timeOut)//调用HTTP通信接口提交数据

                        图片1 config.cs

坑你没商量之c#微信回调中核实订单的签名验证_第2张图片

                           图片2 HttpService.cs

你说你这里晓得传递一个证书验证启用开关,怎么代理服务器的设置,就不晓得也整个开关,让我们自由选择呢。

结果我每次运行这个方法就提示超时。刚开始也没明白怎么整,直接找到config.cs的PROXY_URL端口配置为自己项目的服务器端口【/*默认IP和端口号分别为0.0.0.0和0,此时不开启代理(如有需要才设置)*/官方文档里头的注释,关键你这里写不开启,可是怎么到了方法里头默认的竟然是开启的,而且一点说明都木有!!】还是不行,都要疯了,大量查阅资料,终于找到有网友提出的解决方案,直接把这几行代码注释掉。


      2、是在验证签名过程,这还在自己本身,文档没有阅读仔细,

一开始以为签名都只需要【appid、mch_id、nonce_str、transaction_id+key】的,然后我在MakeSign()方法里头直接封装str参数串,在运行,返回的还是签名错误。

没办法只得在重写下几个用到的方法,然后到处打印出字符串,最后才发现在我们从远程获取到正确的数据后,是整个对象里头的参数都又进行了签名的。【appid、bank_type、cash_fee、fee_type......transaction_id+key】,然后又回头去看官网,文档里头提示 “微信接口可能增加字段,验证签名时必须支持增加的扩展字段”。果然文档提示的都要小心要注意!!


      最后,我想到的方案是修改MakeSign(string key)增加个key秘钥的传递。因为远程返回的xml里头都有appid和mch_id,因此只要稍微调整几个方法即可。完整代码如下:

        //回调方法 
        public string Wx_Notify_url()
        {
            string logid = Guid.NewGuid().ToString();
            const string key="......";//(获取商户api秘钥 )//////重点是这里这里这里,两个回调这里的key对应的是商户里头设置的秘钥 这里是常量常量常量
            
            //对返回的支付结果通知的内容做签名验证
            WxPayAPI.WxPayData notifyData = GetNotifyData(key);
            //检查支付结果中transaction_id是否存在
            if (!notifyData.IsSet("transaction_id"))
            {
                //若transaction_id不存在,则立即返回结果给微信支付后台
                WxPayAPI.WxPayData res = new WxPayAPI.WxPayData();
                res.SetValue("return_code", "FAIL");
                res.SetValue("return_msg", "支付结果中微信订单号不存在");
                //Log.Error(this.GetType().ToString(), "The Pay result is error : " + res.ToXml());                
                return res.ToXml();
            }

            string transaction_id = notifyData.GetValue("transaction_id").ToString();
            //先写入记事本对账
            if (transaction_id != "") {
                //写入记事本
                DbHelperSQL.ExecuteSql("insert into [log] values('" + logid + "', 'transaction_id: " + transaction_id + ", 订单号:" + notifyData.GetValue("out_trade_no").ToString() + "', getdate(), 'wx')");
            }

            //支付结果中查询订单,判断订单真实性   判断条件!QueryOrder(transaction_id)    不判断transaction_id == ""            
            if (!QueryOrder(transaction_id, notifyData, key))
            {
                //若订单查询失败,则立即返回结果给微信支付后台
                WxPayAPI.WxPayData res = new WxPayAPI.WxPayData();
                res.SetValue("return_code", "FAIL");
                res.SetValue("return_msg", "订单查询失败");
                return res.ToXml();
            }
            //查询订单成功
            else{
                //业务逻辑处理start...
                //////////////////////////////////////////////////////////////
                //更改订单状态
                if(.....){
                    //插入币值记录              

                }
                /////////////////////////////////////////////////////////////                
                //业务逻辑处理end...
                WxPayAPI.WxPayData res = new WxPayAPI.WxPayData();
                res.SetValue("return_code", "SUCCESS");
                res.SetValue("return_msg", "OK");
                return res.ToXml();
            }
        }
        /// <summary>
        /// 接收从微信支付后台发送过来的数据并验证签名
        /// </summary>
        /// <returns>微信支付后台返回的数据</returns>
        private WxPayAPI.WxPayData GetNotifyData(string KEY)
        {
            //接收从微信后台POST过来的数据
            System.IO.Stream s = Request.InputStream;
            int count = 0;
            byte[] buffer = new byte[1024];
            StringBuilder builder = new StringBuilder();
            while ((count = s.Read(buffer, 0, 1024)) > 0)
            {
                builder.Append(Encoding.UTF8.GetString(buffer, 0, count));
            }
            s.Flush();
            s.Close();
            s.Dispose();

            //Log.Info(this.GetType().ToString(), "Receive data from WeChat : " + builder.ToString());
            //转换数据格式并验证签名
            WxPayAPI.WxPayData data = new WxPayAPI.WxPayData();
            try
            {
                data.FromXml(builder.ToString(), KEY);
            }
            catch (WxPayAPI.WxPayException ex)
            {
                //若签名错误,则立即返回结果给微信支付后台
                WxPayAPI.WxPayData res = new WxPayAPI.WxPayData();
                res.SetValue("return_code", "FAIL");
                res.SetValue("return_msg", ex.Message);
                //Log.Error(this.GetType().ToString(), "Sign check error : " + res.ToXml());
                Response.Write(res.ToXml());
            }

            //Log.Info(this.GetType().ToString(), "Check sign success");
            return data;
        }
        //查询订单  这里我重写了下,把远程返回的data和加密key直接传入
        public bool QueryOrder(string transaction_id, WxPayData inputObj, string key)
        {
            WxPayAPI.WxPayData req = new WxPayAPI.WxPayData();
            //增加对req进行赋值//////////////////////////////////////            
            req.SetValue("appid", inputObj.GetValue("appid"));           

            req.SetValue("mch_id", inputObj.GetValue("mch_id"));

            req.SetValue("key", key);
            ////////////////////////////////////////////////////////
            req.SetValue("transaction_id", transaction_id);
            WxPayAPI.WxPayData res = WxPayAPI.WxPayApi.OrderQuery(req);
            if (res.GetValue("return_code").ToString() == "FAIL")
                return false;
            if (res.GetValue("return_code").ToString() == "SUCCESS" &&
                res.GetValue("result_code").ToString() == "SUCCESS")
            {
                return true;
            }
            else
            {
                return false;
            }
        }


     然后,在QueryOrder()方法里头进行一些列sign验证,只要把这几个方法在重新调整下,即可!修改WxPayApi文件:

using System;
using System.Collections.Generic;
using System.Web;
using System.Net;
using System.IO;
using System.Text;

namespace WxPayAPI
{
    public class WxPayApi
    {                        
        /**
        *    
        * 查询订单
        * @param WxPayData inputObj 提交给查询订单API的参数
        * @param int timeOut 超时时间
        * @throws WxPayException
        * @return 成功时返回订单查询结果,其他抛异常
        */
        public static WxPayData OrderQuery(WxPayData inputObj, int timeOut = 6)
        {
            string url = "https://api.mch.weixin.qq.com/pay/orderquery";
            //检测必填参数
            if (!inputObj.IsSet("out_trade_no") && !inputObj.IsSet("transaction_id"))
            {
                throw new WxPayException("订单查询接口中,out_trade_no、transaction_id至少填一个!");
            }
            //1、我这里定义了临时WxPayData对象,因为inputObj把key也装进去了,而这里签名并不需要key,不然会出错,而且这个key是要到处使用的,  
            //2、另外的解决方法就是string key = inputObj.GetValue("KEY").ToString();  然后移除key值,不过微信 WxPayData对象并没有封装这个方法,Directory字典有内部方法可以使用【m_values.Remove("key");//这里就可以直接根据他们文档来写个方法了】,偷懒了下,我选择方案1
            WxPayData tmp = new WxPayData();
            tmp.SetValue("transaction_id", inputObj.GetValue("transaction_id"));
            tmp.SetValue("appid", inputObj.GetValue("appid"));//公众账号ID
            tmp.SetValue("mch_id", inputObj.GetValue("mch_id"));//商户号
            tmp.SetValue("nonce_str", WxPayApi.GenerateNonceStr());//随机字符串
            tmp.SetValue("sign", tmp.MakeSign(inputObj.GetValue("key").ToString()));//签名

            string xml = tmp.ToXml();
            
            var start = DateTime.Now;

            Log.Debug("WxPayApi", "OrderQuery request : " + xml);
            string response = HttpService.Post(xml, url, false, timeOut);//调用HTTP通信接口提交数据
            Log.Debug("WxPayApi", "OrderQuery response : " + response);

            var end = DateTime.Now;
            int timeCost = (int)((end - start).TotalMilliseconds);//获得接口耗时

            //将xml格式的数据转化为对象以返回
            WxPayData result = new WxPayData();
            result.FromXml(response, inputObj.GetValue("key").ToString());//这里增加传递秘钥key

            ReportCostTime(url, timeCost, result, inputObj.GetValue("key").ToString());//测速上报   //不知道这个注销有没有影响,这个倒是没测

            return result;
        }

        /**
	    * 
	    * 测速上报
	    * @param string interface_url 接口URL
	    * @param int timeCost 接口耗时
	    * @param WxPayData inputObj参数数组
	    */
        private static void ReportCostTime(string interface_url, int timeCost, WxPayData inputObj, string key)
	    {
		    //如果不需要进行上报
		    if(WxPayConfig3.REPORT_LEVENL == 0)
            {
			    return;
		    } 

		    //如果仅失败上报
		    if(WxPayConfig3.REPORT_LEVENL == 1 && inputObj.IsSet("return_code") && inputObj.GetValue("return_code").ToString() == "SUCCESS" &&
			 inputObj.IsSet("result_code") && inputObj.GetValue("result_code").ToString() == "SUCCESS")
            {
		 	    return;
		    }
		 
		    //上报逻辑
		    WxPayData3 data = new WxPayData3();
            data.SetValue("interface_url",interface_url);
		    data.SetValue("execute_time_",timeCost);
		    //返回状态码
		    if(inputObj.IsSet("return_code"))
            {
			    data.SetValue("return_code",inputObj.GetValue("return_code"));
		    }
		    //返回信息
            if(inputObj.IsSet("return_msg"))
            {
			    data.SetValue("return_msg",inputObj.GetValue("return_msg"));
		    }
		    //业务结果
            if(inputObj.IsSet("result_code"))
            {
			    data.SetValue("result_code",inputObj.GetValue("result_code"));
		    }
		    //错误代码
            if(inputObj.IsSet("err_code"))
            {
			    data.SetValue("err_code",inputObj.GetValue("err_code"));
		    }
		    //错误代码描述
            if(inputObj.IsSet("err_code_des"))
            {
			    data.SetValue("err_code_des",inputObj.GetValue("err_code_des"));
		    }
		    //商户订单号
            if(inputObj.IsSet("out_trade_no"))
            {
			    data.SetValue("out_trade_no",inputObj.GetValue("out_trade_no"));
		    }
		    //设备号
            if(inputObj.IsSet("device_info"))
            {
			    data.SetValue("device_info",inputObj.GetValue("device_info"));
		    }
		
		    try
            {
			    Report(data, key);///增加key值
		    }
            catch (WxPayException ex)
            {
			    //不做任何处理
		    }
	    }


        /**
	    * 
	    * 测速上报接口实现
	    * @param WxPayData inputObj 提交给测速上报接口的参数
	    * @param int timeOut 测速上报接口超时时间
	    * @throws WxPayException
	    * @return 成功时返回测速上报接口返回的结果,其他抛异常
	    */
        public static WxPayData Report(WxPayData inputObj, string key, int timeOut = 1)
	    {
		    string url = "https://api.mch.weixin.qq.com/payitil/report";
		    //检测必填参数
		    if(!inputObj.IsSet("interface_url"))
            {
			    throw new WxPayException("接口URL,缺少必填参数interface_url!");
		    } 
            if(!inputObj.IsSet("return_code"))
            {
			    throw new WxPayException("返回状态码,缺少必填参数return_code!");
		    } 
            if(!inputObj.IsSet("result_code"))
            {
			    throw new WxPayException("业务结果,缺少必填参数result_code!");
		    } 
            if(!inputObj.IsSet("user_ip"))
            {
			    throw new WxPayException("访问接口IP,缺少必填参数user_ip!");
		    } 
            if(!inputObj.IsSet("execute_time_"))
            {
			    throw new WxPayException("接口耗时,缺少必填参数execute_time_!");
		    }

		    inputObj.SetValue("appid",WxPayConfig3.APPID);//公众账号ID
		    inputObj.SetValue("mch_id",WxPayConfig3.MCHID);//商户号
		    inputObj.SetValue("user_ip",WxPayConfig3.IP);//终端ip
		    inputObj.SetValue("time",DateTime.Now.ToString("yyyyMMddHHmmss"));//商户上报时间	 
		    inputObj.SetValue("nonce_str",GenerateNonceStr());//随机字符串
            inputObj.SetValue("sign", inputObj.MakeSign(key));//签名
		    string xml = inputObj.ToXml();

            Log.Info("WxPayApi", "Report request : " + xml);

            string response = HttpService.Post(xml, url, false, timeOut);

            Log.Info("WxPayApi", "Report response : " + response);

            WxPayData result = new WxPayData();
            result.FromXml(response, key);///增加key值
		    return result;
	    }

    }
}

    然后,修改Data.cs文件:

/**
        * @将xml转为WxPayData对象并返回对象内部的数据
        * @param string 待转换的xml串
        * @return 经转换得到的Dictionary
        * @throws WxPayException
        */
        public SortedDictionary<string, object> FromXml(string xml, string key/*传递秘钥*/)
        {
            if (string.IsNullOrEmpty(xml))
            {
                Log.Error(this.GetType().ToString(), "将空的xml串转换为WxPayData不合法!");
                throw new WxPayException("将空的xml串转换为WxPayData不合法!");
            }

            XmlDocument xmlDoc = new XmlDocument();
            xmlDoc.LoadXml(xml);
            XmlNode xmlNode = xmlDoc.FirstChild;//获取到根节点<xml>
            XmlNodeList nodes = xmlNode.ChildNodes;
            foreach (XmlNode xn in nodes)
            {
                XmlElement xe = (XmlElement)xn;
                m_values[xe.Name] = xe.InnerText;//获取xml的键值对到WxPayData内部的数据中
            }

            try
            {
                //2015-06-29 错误是没有签名
                if (m_values["return_code"].ToString() != "SUCCESS")
                {
                    return m_values;
                }
                CheckSign(key);//验证签名,不通过会抛异常  //在把秘钥传入验证签名的方法里头
            }
            catch (WxPayException ex)
            {
                throw new WxPayException(ex.Message);
            }

            return m_values;
        }
       /**
        * 
        * 检测签名是否正确
        * 正确返回true,错误抛异常
        */
        public bool CheckSign(string key)
        {
            //如果没有设置签名,则跳过检测
            if (!IsSet("sign"))
            {
               Log.Error(this.GetType().ToString(), "WxPayData签名存在但不合法!");
               throw new WxPayException("WxPayData签名存在但不合法!");
            }
            //如果设置了签名但是签名为空,则抛异常
            else if(GetValue("sign") == null || GetValue("sign").ToString() == "")
            {
                Log.Error(this.GetType().ToString(), "WxPayData签名存在但不合法!");
                throw new WxPayException("WxPayData签名存在但不合法!");
            }

            //获取接收到的签名
            string return_sign = GetValue("sign").ToString();

            //在本地计算新的签名
            string cal_sign = MakeSign(key);//生成sign直接使用传入的key值,而不是config.cs配置好的默认的key值

            if (cal_sign == return_sign)
            {
                return true;
            }

            Log.Error(this.GetType().ToString(), "WxPayData签名验证错误!");
            throw new WxPayException("WxPayData签名验证错误!");
        }

        /**
        * @生成签名,详见签名生成算法
        * @return 签名, sign字段不参加签名
        */
        public string MakeSign(string key)
        {
            //转url格式
            string str = ToUrl();
            //在string后加入API KEY
            //str += "&key=" + WxPayConfig.KEY;//不适用写好的方法
            str += "&key=" + key;
            //MD5加密
            var md5 = MD5.Create();
            var bs = md5.ComputeHash(Encoding.UTF8.GetBytes(str));
            var sb = new StringBuilder();
            foreach (byte b in bs)
            {
                sb.Append(b.ToString("x2"));
            }
            //所有字符转为大写
            return sb.ToString().ToUpper();
        }


      最后,补充下上面OrderQuery()说的方案二删除key的方法,已经测试过,有效!!

            //这个是WxPayApi.cs的调整
            string key = inputObj.GetValue("KEY").ToString();
            inputObj.RemoveTkey("KEY");

            //Data.cs文件增加方法RemoveTkey            
            /**
             * 根据字段名删除某个字段的值
             * @param key 字段名
             * @return key对应的字段值
            */
            public bool RemoveTkey(string Tkey)
            {
                bool e = m_values.Remove(Tkey);
                return e;
            }

        经测试,返回参数如下:

坑你没商量之c#微信回调中核实订单的签名验证_第3张图片




你可能感兴趣的:(微信支付回调)