Java 实现JSAPI 微信支付
开发步骤如下:
1. 参考微信开发文档
把微信支付需要配置的环境都配好,参考链接 微信开发文档
2. 微信统一下单
参考链接 统一下单
import com.alibaba.fastjson.JSONObject;
import com.wholesale.mall.entity.WeiChart;
import com.wholesale.mall.mapper.systemSet.WeiChartMapper;
import com.wholesale.mall.service.statement.IPaymentService;
import com.wholesale.mall.utils.Tools;
import com.wholesale.mall.weichart.Sha1Util;
import com.wholesale.mall.weichart.WeiChatURLUtil;
import lombok.RequiredArgsConstructor;
import org.apache.commons.httpclient.HttpClient;
import org.apache.commons.httpclient.methods.PostMethod;
import org.apache.commons.httpclient.methods.RequestEntity;
import org.apache.commons.httpclient.methods.StringRequestEntity;
import org.apache.log4j.Logger;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.math.BigDecimal;
import java.util.HashMap;
import java.util.Map;
import java.util.SortedMap;
import java.util.TreeMap;
/**微信支付**/
private final String wx_partner="1277054588549562368";
private final String wx_pertnerKey="FFFF9BB656EE4430A31B356726BBC133";
private final WeiChartMapper weiChartMapper;
/**
* 微信支付
* @param request
* @param response
* @param orderNo 订单号
* @param totalMoney 订单金额
* @param openId 微信唯一标识
* @return
* @throws Exception
*/
@Override
public Map<Integer,Map<String,String>> saveWXPay(HttpServletRequest request, HttpServletResponse response,String orderNo, Double totalMoney, String openId) throws Exception{
//返回参数
Map<Integer,Map<String,String>> map=new HashMap<>();
Map<String,String> mapVal=new HashMap<>();
//金额转化为分为单位
String money=(totalMoney*100)+"";//转换
Float sessionmoney =Float.parseFloat(money);
String finalmoney=sessionmoney.toString();
finalmoney = finalmoney.replace(".0", "");
//获取openId后调用统一支付接口https://api.mch.weixin.qq.com/pay/unifiedorder
//商户号
String mch_id = wx_partner;
//10位序列号,可以自行调整。
String strReq = Tools.CreateNoncestr();
//随机数
String nonce_str = strReq;
//商品描述
String body = "商品购买";
//附加数据
String attach = "商品购买";
//商户订单号 商家内部订单号
String out_trade_no = orderNo;
//总金额以分为单位,不带小数点
String total_fee = finalmoney;
//String total_fee = "1";
//订单生成的机器 IP
String spbill_create_ip = request.getRemoteAddr();
//这里notify_url是 支付完成后微信发给该链接信息,可以判断会员是否支付成功,改变订单状态等。
String notify_url = WeiChatURLUtil.WEICHAT_BACKSTAGE_URL + "shipping-MinPay/updatePaymentState";
//交易类型
String trade_type = "JSAPI";
//查询微信公众号信息
WeiChart weiChart=weiChartMapper.findWeiChart();
SortedMap<String, String> packageParams = new TreeMap<String, String>();
packageParams.put("appid", weiChart.getAppId());
packageParams.put("mch_id", mch_id);
packageParams.put("nonce_str", nonce_str);
packageParams.put("attach", attach);
packageParams.put("body", body);
packageParams.put("notify_url", notify_url);
packageParams.put("openid", openId);
packageParams.put("out_trade_no", out_trade_no);
packageParams.put("spbill_create_ip", spbill_create_ip);
//这里写的金额为1 分到时修改
packageParams.put("total_fee", total_fee);
packageParams.put("trade_type", trade_type);
//签名
String sign = Tools.createSign2(packageParams,wx_pertnerKey);
String xml = "<xml>" +
"<appid>" + weiChart.getAppId() + "</appid>" +
"<mch_id>" + mch_id + "</mch_id>" +
"<nonce_str>" + nonce_str + "</nonce_str>" +
"<sign>" + sign + "</sign>" +
"<body><![CDATA[" + body + "]]></body>" +
"<attach>" + attach + "</attach>" +
"<out_trade_no>" + out_trade_no + "</out_trade_no>" +
//金额,这里写的1 分到时修改
"<total_fee>" + total_fee + "</total_fee>" +
"<spbill_create_ip>" + spbill_create_ip + "</spbill_create_ip>" +
"<notify_url>" + notify_url + "</notify_url>" +
"<trade_type>" + trade_type + "</trade_type>" +
"<openid>" + openId + "</openid>" +
"</xml>";
logger.info(xml);
String allParameters = "";
try {
allParameters = Tools.genPackage(packageParams,wx_pertnerKey);
} catch (Exception e) {
logger.error("获取package的签名包异常:", e);
mapVal.put("errorMsg","获取package的签名包异常:"+ e.getMessage());
map.put(1,mapVal);
return map;
}
logger.info("获取package的签名包:" + allParameters);
String createOrderURL = WeiChatURLUtil.WEICHAT_CREATEORDER_URL;
String prepay_id = "";
try {
prepay_id = Tools.getPayNo(createOrderURL, xml);
if ("".equals(prepay_id)) {
logger.info("统一支付接口获取预支付订单出错");
mapVal.put("errorMsg","统一支付接口获取预支付订单出错");
map.put(1,mapVal);
return map;
}
} catch (Exception e1) {
logger.error("统一支付接口获取预支付订单异常:", e1);
mapVal.put("errorMsg","统一支付接口获取预支付订单出错:"+e1.getMessage());
map.put(1,mapVal);
return map;
}
SortedMap<String, String> finalpackage = new TreeMap<String, String>();
String appid2 = weiChart.getAppId();
String timestamp = Sha1Util.getTimeStamp();
String nonceStr2 = nonce_str;
String packages = "prepay_id=" + prepay_id;
finalpackage.put("appId", appid2);
finalpackage.put("timeStamp", timestamp);
finalpackage.put("nonceStr", nonceStr2);
finalpackage.put("package", packages);
finalpackage.put("signType", "MD5");
String finalSign = Tools.createSign2(finalpackage,wx_pertnerKey);
logger.info("提交页面数据:appid=" + appid2 + "&timeStamp=" + timestamp + "&nonceStr=" + nonceStr2 + "&package=" + packages + "&sign=" + finalSign);
mapVal.put("orderId", prepay_id);
mapVal.put("appId",appid2);
mapVal.put("timestamp",timestamp);
mapVal.put("nonceStr",nonceStr2);
mapVal.put("packages",packages);
mapVal.put("signType","MD5");
mapVal.put("sign",finalSign);
map.put(0,mapVal);
return map;
}
说明:
1.appId和appSecret在微信公众号平台获取 微信公众平台
商户号和商户秘钥在微信支付商户后台获取 微信支付
2.随机字符串 nonce_str的长度要求在32位以内。
推荐随机数生成算法 :
/**
* 生成随机数
*
* @return
*/
public static String CreateNoncestr() {
Random random = new Random();
return MD5Util.MD5Encode(String.valueOf(random.nextInt(10000)), "UTF-8");
}
import java.security.MessageDigest;
/**
* MD5算法计算
*/
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" };
}
3.参数 total_fee,即订单总金额,单位为分,以元为单位的记得转换为分。
4.商户订单号 out_trade_no,商户系统内部订单号,要求32个字符内,只能是数字、大小写字母_-|* 且在同一个商户号下唯一。即为下单的订单号即可。
5.终端IP spbill_create_ip,支持IPV4和IPV6两种格式的IP地址。用户的客户端IP。代码如下:
//订单生成的机器 IP
String spbill_create_ip = request.getRemoteAddr();
6.签名算法生成:
参考文档:签名算法如何生成
特别注意以下重要规则:
◆ 参数名ASCII码从小到大排序(字典序);
◆ 如果参数的值为空不参与签名;
◆ 参数名区分大小写;
◆ 验证调用返回或微信主动通知签名时,传送的sign参数不参与签名,将生成的签名与该sign值作校验。
◆ 微信接口可能增加字段,验证签名时必须支持增加的扩展字段。
使用SortedMap进行有序排列:
SortedMap<String, String> packageParams = new TreeMap<String, String>();
packageParams.put("appid", weiChart.getAppId());
packageParams.put("mch_id", mch_id);
packageParams.put("nonce_str", nonce_str);
packageParams.put("attach", attach);
packageParams.put("body", body);
packageParams.put("notify_url", notify_url);
packageParams.put("openid", openId);
packageParams.put("out_trade_no", out_trade_no);
packageParams.put("spbill_create_ip", spbill_create_ip);
//这里写的金额为1 分到时修改
packageParams.put("total_fee", "1");
packageParams.put("trade_type", trade_type);
创建签名,使用哈希算法:
/**
* 创建md5摘要,规则是:将集合M内非空参数值的参数按照参数名ASCII码从小到大排序(字典序),
* 使用URL键值对的格式(即key1=value1&key2=value2…)拼接成字符串
* @param packageParams 有序参数包
* @param key 商户秘钥
* @return
*/
public static String createSign2(SortedMap<String, String> packageParams, String key) {
StringBuffer sb = new StringBuffer();
Set<Map.Entry<String, String>> es = packageParams.entrySet();
Iterator<Map.Entry<String, String>> it = es.iterator();
while (it.hasNext()) {
Map.Entry<String, String> entry = it.next();
String k = entry.getKey();
Object v = entry.getValue();
if (null != v && !"".equals(v) && !"sign".equals(k)
&& !"key".equals(k)) {
sb.append(k + "=" + v + "&");
}
}
sb.append("key=" + key);
String sign = MD5Util.MD5Encode(sb.toString(), "UTF-8").toUpperCase();
return sign;
}
最终得到最终发送的数据,以xml格式发送:
(注:参数值用XML转义即可,CDATA标签用于说明数据不被XML解析器解析。)
String xml = "<xml>" +
"<appid>" + weiChart.getAppId() + "</appid>" +
"<mch_id>" + mch_id + "</mch_id>" +
"<nonce_str>" + nonce_str + "</nonce_str>" +
"<sign>" + sign + "</sign>" +
"<body><![CDATA[" + body + "]]></body>" +
"<attach>" + attach + "</attach>" +
"<out_trade_no>" + out_trade_no + "</out_trade_no>" +
//金额,这里写的1 分到时修改
"<total_fee>" + total_fee + "</total_fee>" +
"<spbill_create_ip>" + spbill_create_ip + "</spbill_create_ip>" +
"<notify_url>" + notify_url + "</notify_url>" +
"<trade_type>" + trade_type + "</trade_type>" +
"<openid>" + openId + "</openid>" +
"</xml>";
获取签名包:
/**
* 获取package的签名包
* @param packageParams 有序参数包
* @param key 商户秘钥
* @return
* @throws UnsupportedEncodingException
*/
public static String genPackage(SortedMap<String, String> packageParams, String key)throws UnsupportedEncodingException {
String sign = createSign2(packageParams,key);
StringBuffer sb = new StringBuffer();
Set<Map.Entry<String, String>> es = packageParams.entrySet();
Iterator<Map.Entry<String, String>> it = es.iterator();
while (it.hasNext()) {
Map.Entry<String, String> entry = it.next();
String k = entry.getKey();
String v = entry.getValue();
sb.append(k + "=" + UrlEncode(v) + "&");
}
// 去掉最后一个&
String packageValue = sb.append("sign=" + sign).toString();
return packageValue;
}
7.发送post请求,返回成功后,获取预支付交易会话标识 prepay_id。
private static Logger logger = Logger.getLogger(Tools.class);
public static DefaultHttpClient httpclient;
static {
httpclient = new DefaultHttpClient();
httpclient = (DefaultHttpClient) HttpClientConnectionManager.getSSLInstance(httpclient);
}
/**
* 获取支付prepay_id
*
* @param url https://api.mch.weixin.qq.com/pay/unifiedorder
* @param xmlParam
* @return
*/
public static String getPayNo(String url, String xmlParam) {
DefaultHttpClient client = new DefaultHttpClient();
client.getParams().setParameter(ClientPNames.ALLOW_CIRCULAR_REDIRECTS,
true);
HttpPost httpost = HttpClientConnectionManager.getPostMethod(url);
String prepay_id = "";
try {
httpost.setEntity(new StringEntity(xmlParam, "UTF-8"));
HttpResponse response = httpclient.execute(httpost);
String jsonStr = EntityUtils.toString(response.getEntity(), "UTF-8");
logger.info("json是:" + jsonStr);
if (jsonStr.indexOf("FAIL") != -1) {
return null;
}
Map<String, String> map = XMLUtil.doXMLParse(jsonStr);
prepay_id = (String) map.get("prepay_id");
} catch (Exception e) {
logger.error("统一支付提交异常:", e);
return null;
}
return prepay_id;
}
package com.wholesale.mall.utils.http;
import org.apache.http.client.HttpClient;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.conn.ClientConnectionManager;
import org.apache.http.conn.scheme.Scheme;
import org.apache.http.conn.scheme.SchemeRegistry;
import org.apache.http.impl.client.DefaultHttpClient;
public class HttpClientConnectionManager {
/**
* 获取SSL验证的HttpClient
* @param httpClient
* @return
*/
public static HttpClient getSSLInstance(HttpClient httpClient){
ClientConnectionManager ccm = httpClient.getConnectionManager();
SchemeRegistry sr = ccm.getSchemeRegistry();
sr.register(new Scheme("https", MySSLSocketFactory.getInstance(), 443));
httpClient = new DefaultHttpClient(ccm, httpClient.getParams());
return httpClient;
}
/**
* 模拟浏览器post提交
*
* @param url
* @return
*/
public static HttpPost getPostMethod(String url) {
HttpPost pmethod = new HttpPost(url); // 设置响应头信息
pmethod.addHeader("Connection", "keep-alive");
pmethod.addHeader("Accept", "*/*");
pmethod.addHeader("Content-Type", "application/x-www-form-urlencoded; charset=UTF-8");
pmethod.addHeader("Host", "api.mch.weixin.qq.com");
pmethod.addHeader("X-Requested-With", "XMLHttpRequest");
pmethod.addHeader("Cache-Control", "max-age=0");
pmethod.addHeader("User-Agent", "Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 6.0) ");
return pmethod;
}
/**
* 模拟浏览器GET提交
* @param url
* @return
*/
public static HttpGet getGetMethod(String url) {
HttpGet pmethod = new HttpGet(url);
// 设置响应头信息
pmethod.addHeader("Connection", "keep-alive");
pmethod.addHeader("Cache-Control", "max-age=0");
pmethod.addHeader("User-Agent", "Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 6.0) ");
pmethod.addHeader("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,*/;q=0.8");
return pmethod;
}
}
package com.wholesale.mall.utils.http;
import com.wholesale.mall.weichart.MyX509TrustManager;
import org.apache.http.conn.ssl.SSLSocketFactory;
import javax.net.ssl.SSLContext;
import javax.net.ssl.TrustManager;
import java.security.KeyManagementException;
import java.security.NoSuchAlgorithmException;
public class MySSLSocketFactory extends SSLSocketFactory {
static {
mySSLSocketFactory = new MySSLSocketFactory(createSContext());
}
private static MySSLSocketFactory mySSLSocketFactory = null;
private static SSLContext createSContext() {
SSLContext sslcontext = null;
try {
sslcontext = SSLContext.getInstance("SSL");
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
}
try {
sslcontext.init(null,
new TrustManager[] { new MyX509TrustManager() }, null);
} catch (KeyManagementException e) {
e.printStackTrace();
return null;
}
return sslcontext;
}
@SuppressWarnings("deprecation")
private MySSLSocketFactory(SSLContext sslContext) {
super(sslContext);
this.setHostnameVerifier(ALLOW_ALL_HOSTNAME_VERIFIER);
}
public static MySSLSocketFactory getInstance() {
if (mySSLSocketFactory != null) {
return mySSLSocketFactory;
} else {
return mySSLSocketFactory = new MySSLSocketFactory(createSContext());
}
}
}
如何解析xml:
需要引入jar包:
<dependency>
<groupId>org.jdom</groupId>
<artifactId>jdom</artifactId>
<version>1.1</version>
</dependency>
解析xml代码:
package com.wholesale.mall.weichart;
import org.jdom.Document;
import org.jdom.Element;
import org.jdom.JDOMException;
import org.jdom.input.SAXBuilder;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
public class XMLUtil {
/**
* 解析xml,返回第一级元素键值对。如果第一级元素有子节点,则此节点的值是子节点的xml数据。
* @param strxml
* @return
* @throws JDOMException
* @throws IOException
*/
public static Map<String,String> doXMLParse(String strxml) throws JDOMException, IOException {
strxml = strxml.replaceFirst("encoding=\".*\"", "encoding=\"UTF-8\"");
if(null == strxml || "".equals(strxml)) {
return null;
}
Map<String,String> m = new HashMap<String,String>();
InputStream in = new ByteArrayInputStream(strxml.getBytes("UTF-8"));
SAXBuilder builder = new SAXBuilder();
Document doc = builder.build(in);
Element root = doc.getRootElement();
List list = root.getChildren();
Iterator it = list.iterator();
while(it.hasNext()) {
Element e = (Element) it.next();
String k = e.getName();
String v = "";
List children = e.getChildren();
if(children.isEmpty()) {
v = e.getTextNormalize();
} else {
v = XMLUtil.getChildrenText(children);
}
m.put(k, v);
}
//关闭流
in.close();
return m;
}
/**
* 获取子结点的xml
* @param children
* @return String
*/
@SuppressWarnings("unchecked")
public static String getChildrenText(List children) {
StringBuffer sb = new StringBuffer();
if(!children.isEmpty()) {
Iterator it = children.iterator();
while(it.hasNext()) {
Element e = (Element) it.next();
String name = e.getName();
String value = e.getTextNormalize();
List list = e.getChildren();
sb.append("<" + name + ">");
if(!list.isEmpty()) {
sb.append(XMLUtil.getChildrenText(list));
}
sb.append(value);
sb.append("</" + name + ">");
}
}
return sb.toString();
}
}
8.根据预支付标识,再次生成新的签名:
SortedMap<String, String> packageParam = new TreeMap<String, String>();
String appid2 = weiChart.getAppId();
String timestamp = Sha1Util.getTimeStamp();
String nonceStr2 = nonce_str;
String packages = "prepay_id=" + prepay_id;
packageParam.put("appId", appid2);
packageParam.put("timeStamp", timestamp);
packageParam.put("nonceStr", nonceStr2);
packageParam.put("package", packages);
packageParam.put("signType", "MD5");
String finalSign = Tools.createSign2(packageParam,wx_pertnerKey);
logger.info("提交页面数据:appid=" + appid2 + "&timeStamp=" + timestamp + "&nonceStr=" + nonceStr2 + "&package=" + packages + "&sign=" + finalSign);
9.timeStamp生成方法:
public static String getTimeStamp() {
return String.valueOf(System.currentTimeMillis() / 1000);
}
10.最后把下列数据返回给前端,调起微信支付:
mapVal.put("orderId", prepay_id);
mapVal.put("appId",appid2);
mapVal.put("timestamp",timestamp);
mapVal.put("nonceStr",nonceStr2);
mapVal.put("packages",packages);
mapVal.put("signType","MD5");
mapVal.put("sign",finalSign);
总结
微信支付对于新手来说,的确很难入手,难点在生成签名算法那,运用了MD5加密等一系列操作,如果有出现错误或异常,一定要记得加日志,查看日志信息,可以很明确的找出问题。我使用的是日志是: org.apache.log4j.Logger,还有很多其他方法,可以网上搜索下。
本篇文章使用的是传统写法,当然现在GitHub上有很多人封装了SDK,那个使用应该比较简单,都是封装好的,直接调用就行了。