先来一张微信支付官方的的流程图
官网在该图的最上方有这么一句话:统一下单API、支付结果通知API和查询订单API等都涉及签名过程,调用都必须在商户服务器端完成,这句话比较重要,接下来我将严格按照这个规则展开说明,不太严谨的有把上面的过程放在客户端的,这可能会有一定的安全隐患。
第一步:商户后端统一下单API(对应于该流程图的第4,5,6步骤)
①商户后端请求微信支付后端:对应第4步
参数详情如下:
字段名 | 变量名 | 必填 | 类型 | 示例值 | 描述 |
---|---|---|---|---|---|
应用ID | appid | 是 | String(32) | wxd678efh567hg6787 | 微信开放平台审核通过的应用APPID |
商户号 | mch_id | 是 | String(32) | 1230000109 | 微信支付分配的商户号 |
商品描述 | body | 是 | String(128) | 腾讯充值中心-QQ会员充值 | 商品描述交易字段格式根据不同的应用场景按照以下格式:APP——需传入应用市场上的APP名字-实际商品名称,天天爱消除-游戏充值。 |
商户号 | mch_id | 是 | String(32) | 1230000109 | 微信支付分配的商户号 |
随机字符串 | nonce_str | 是 | String(32) | 5K8264ILTKCH16CQ2502SI8ZNMTM67VS | 随机字符串,不长于32位。推荐随机数生成算法 |
商户订单号 | out_trade_no | 是 | String(32) | 20150806125346 | 商户系统内部的订单号,32个字符内、可包含字母, 其他说明见商户订单号 |
总金额 | total_fee | 是 | Int | 888 | 订单总金额,单位为分,详见支付金额 |
终端IP | spbill_create_ip | 是 | String(16) | 123.12.12.123 | 用户端实际ip |
通知地址 | notify_url | 是 | String(256) | http://www.weixin.qq.com/wxpay/pay.php | 接收微信支付异步通知回调地址,通知url必须为直接可访问的url,不能携带参数。 |
交易类型 | trade_type | 是 | String(16) | APP | 支付类型 |
签名 | sign | 是 | String(32) | C380BEC2BFD727A4B6845133519F3AD6 | 签名,详见签名生成算法 |
以上为必须发送给微信服务端的参数,会由StortedMap进行排序(主要是为了接下来的sign生成),其中,APP_body、total_fee、spbill_create_ip、为APP端要传过来的参数,用于生成订单信息,其他参数都是在后台的,一般都为固定的常数或者可以在服务端生成。
SortedMap oparams = new SortedMap();
private void createSingParams(String ip, String orderId,
int price) {
oparams .setParameter("appid", ConfigUtil.APPID)//应用号;
oparams .setParameter("body", WeixinConstant.PRODUCT_BODY)// 商品描述;
oparams .setParameter("mch_id", ConfigUtil.MCH_ID)// 商户号;
oparams .setParameter("nonce_str", PayCommonUtil.CreateNoncestr())// 16随机字符串(大小写字母加数字)
oparams .setParameter("out_trade_no", orderId)// 商户订单号;
oparams .setParameter("total_fee", "1")// 银行币种支付的钱钱啦;
oparams .setParameter("spbill_create_ip", ip)// IP地址;
oparams .setParameter("notify_url", ConfigUtil.NOTIFY_URL) // 微信回调地址;
oparams .setParameter("trade_type", ConfigUtil.TRADE_TYPE)// 支付类型 APP;
}
public void setParameter(String parameter, String parameterValue) {
String v = "";
if(null != parameterValue) {
v = parameterValue.trim();
}
this.params.put(parameter, v);
}
可以看到上边必需传递的10个参数还有一个sign没有,这个sign是要根据前面的参数来生成的,当然了,如果你需要给微信传递其他的参数可以,可以参考标准预订单请求参数。
String signStr = createSign(signParams);
//这个是加签的
protected void createSign(Map signParams) {
StringBuffer sb = new StringBuffer();
Set es = signParams.entrySet();
Iterator it = es.iterator();
while(it.hasNext()) {
Map.Entry entry = (Map.Entry)it.next();
String k = (String)entry.getKey();
String v = (String)entry.getValue();
if(null != v && !"".equals(v)
&& !"sign".equals(k) && !"key".equals(k)) {
sb.append(k + "=" + v + "&");
}
}
sb.append("key=" + this.getKey());
String sign = MD5Util.MD5Encode(sb.toString(), enc).toUpperCase();
return sign;
}
public class MD5Util {
private static String byteArrayToHexString(byte b[]) {
StringBuffer resultSb = new StringBuffer();
for (int i = 0; i < b.length; i++)
resultSb.append(byteToHexString(b[i]));
return resultSb.toString();
}
private static String byteToHexString(byte b) {
int n = b;
if (n < 0)
n += 256;
int d1 = n / 16;
int d2 = n % 16;
return hexDigits[d1] + hexDigits[d2];
}
public static String MD5Encode(String origin, String charsetname) {
String resultString = null;
try {
resultString = new String(origin);
MessageDigest md = MessageDigest.getInstance("MD5");
if (charsetname == null || "".equals(charsetname))
resultString = byteArrayToHexString(md.digest(resultString
.getBytes()));
else
resultString = byteArrayToHexString(md.digest(resultString
.getBytes(charsetname)));
} catch (Exception exception) {
}
return resultString;
}
private static final String hexDigits[] = { "0", "1", "2", "3", "4", "5",
"6", "7", "8", "9", "a", "b", "c", "d", "e", "f" };
}
大概意思就是:将非空参数按照ASCII从小到大,以键值对进行拼接,然后最后拼接上key(应用对应的密钥)生成了str1,然后对str1进行MD5加密,并且转化为大写,就得到了sign签名的值。
可能有几点注意事项请参考:签名算法
params.setParameter(“sign”, signStr );
然后将params转化为xml格式,如下:
wx2421b1c4370ec43b
支付测试
APP支付测试
10000100
1add1a30ac87aa2db72f57a2375d8fec
http://wxpay.wxutil.com/pub_v2/pay/notify.v2.php
1415659990
14.23.150.211
1
APP
0CB01533B8C1EF103065174F50BCA001
把生成的xml字符串作为实体以post方式发送给微信服务端接口地址:https://api.mch.weixin.qq.com/pay/unifiedorder
②微信支付后端的回复:对应于步骤5
具体返回格式参考:返回结果
提取回复中的 prepayid:回复的字符串为xml的,需要进行解析,然后取出来然后进行签名的验证,验证通过即可以取出来prepayId。
Map resParams = WCPayUtils.getParamsMapFromXml(xmlstrRes); //将xml解析为map
resParams .put("key",pContect.getWcPayKey());
if(resParams .containsKey("sign")&& resParams .get("prepay_id") != null &&
!"".equals(resParams .get("prepay_id"))&& !"null".equals(resParams .get("prepay_id"))){
if(WCPayUtils.checkSign(resParams )){//签名认证成功
for(Map.Entry param : resParams .entrySet()) {
if("appid".equals(resParams .getKey()))
System.out.println("prepayid="+resParams .getValue());
}
}
}
/**
* 从xml字符串中解析参数
* @param xml
* @return
* @throws Exception
*/
public static Map getParamsMapFromXml(InputStream xml) throws Exception {
Map params = new HashMap(0);
SAXReader saxReader = new SAXReader();
Document read = saxReader.read(xml);
Element node = read.getRootElement();
listNodes(node, params);
return params;
}
@SuppressWarnings({ "unchecked" })
public static void listNodes(Element node, Map params) {
// 获取当前节点的所有属性节点
List list = node.attributes();
// 遍历属性节点
if ((list == null || list.size() == 0) && !(node.getTextTrim().equals(""))) {
if(node.getTextTrim().contains("", "");
params.put(node.getName(), split[1]);
}else{
params.put(node.getName(),node.getTextTrim());
}
}
// 当前节点下面子节点迭代器
Iterator it = node.elementIterator();
// 遍历
while (it.hasNext()) {
// 获取某个子节点对象
Element e = it.next();
// 对子节点进行遍历
listNodes(e, params);
}
/**
* 签名认证
* @param paramsMap
* @return
* @throws Exception
*/
public static boolean checkSign(Map paramsMap) throws Exception {
String sign = getSignFromParamMap(paramsMap);
return paramsMap.get("sign").equals(sign);
}
/**
* 从map中获取签名sign
* @param paramsMap
* @return
* @throws Exception
*/
public static String getSignFromParamMap(Map paramsMap) throws Exception{
if (paramsMap != null && paramsMap.size() > 0) {
Map params = new TreeMap(new Comparator() {
public int compare(String s1, String s2) {
return s1.compareTo(s2);
}
});
params.putAll(paramsMap);
StringBuffer tempStr = new StringBuffer();
for (Entry param : params.entrySet()) {
if (!"sign".equals(param.getKey()) && !"key".equals(param.getKey())
&& !"".equals(param.getValue()) && param.getValue() != null) {
tempStr.append(param.getKey() + "=" + param.getValue() + "&");
}
}
String temp = tempStr.toString().concat("key="+params.get("key"));
return MD5Utils.getMD5(temp).toUpperCase();
}
return null;
}
③商家服务端将订单信息返回给App端支付信息:对应于步骤6、7
从上面resParams 中解析出如下参数,然后再次再次签名(同上),最后返回给App端。
调起支付参数:
请求参数
字段名 | 变量名 | 类型 | 必填 | 示例值 | 描述 |
---|---|---|---|---|---|
应用ID | appid | String(32) | 是 | wx8888888888888888 | 微信开放平台审核通过的应用APPID |
商户号 | partnerid | String(32) | 是 | 1900000109 | 微信支付分配的商户号 |
预支付交易会话ID | prepayid | String(32) | 是 | WX1217752501201407033233368018 | 微信返回的支付交易会话ID |
扩展字段 | package | String(128) | 是 | Sign=WXPay | 暂填写固定值Sign=WXPay |
随机字符串 | noncestr | String(32) | 是 | 5K8264ILTKCH16CQ2502SI8ZNMTM67VS | 随机字符串,不长于32位。推荐随机数生成算法 |
时间戳 | timestamp | String(10) | 是 | 1412000000 | 时间戳,请见接口规则-参数规定 |
签名 | sign | String(32) | 是 | C380BEC2BFD727A4B6845133519F3AD6 | 签名,详见签名生成算法 |
第二步:APP端调起支付
关键代码如下:
IWXAPI api;
PayReq request = new PayReq();
request.appId = "wxd930ea5d5a258f4f";
request.partnerId = "1900000109";
request.prepayId= "1101000000140415649af9fc314aa427",;
request.packageValue = "Sign=WXPay";
request.nonceStr= "1101000000140429eb40476f8896f4c9";
request.timeStamp= "1398746574";
request.sign= "7FFECB600D7157C5AA49810D2D8F28BC2811827B";
api.sendReq(request);
支付回调参考:支付结果回调
第三步:商户服务端接受微信服务端的异步支付状态消息并处理相应业务逻辑。官方文档:支付结果通用通知
当app客户端向微信发起支付请求,并付款成功后,微信会向异步通知URL也就是notify_url上面传递支付接口信息,也是xml字符串。我们需要将之解析完成后,并再次生成sign,然后将生成的sign与传来的sign进行比对认证,认证成功则说明是微信发来的信息。然后从里面拿到result_code,如果result_code是“SUCCESS”说明支付成功。处理相应业务逻辑。
到这里基本上交互就完成了。
本文多处参考如下博客,写的很好,可以参考:第三方APP微信支付Java服务端构建步骤
最后就是提醒后来者在查android微信支付资料的时候最好过滤一下,最近一年的最好。