您的位置:首页 > 移动开发 > 微信开发

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

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

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



本来只有一个应用的话,也是不必要在来发文章讨论的,直接跟着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



图片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;
        }


经测试,返回参数如下:

内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: