- 在Java中,集成微信支付,十分的简单,主要分为如下几步
- 一、导入依赖
- 二、编写微信支付的配置
- 三、编写下单
- 3.1 编写支付工具类 WxPayUtil
- 3.2 编写Controller,调用封装的工具类方法
- 微信支付接口签名校验工具 来进行验证,如果验证成功,此时就靠前端朋友,来测试微信支付了,看能否成功了">如果,二次签名返回的数据中,都是不为null的,说明,有很大可能,是签名成功的,或者,可以通过 微信支付接口签名校验工具 来进行验证,如果验证成功,此时就靠前端朋友,来测试微信支付了,看能否成功了
- 四、APP进行支付测试
本文没有与申请微信支付相关的内容(申请微信支付过于复杂…..) 本文前提: 已经申请好了对应的微信支付商户号,并且已经签约了对应的支付产品
在Java中,集成微信支付,十分的简单,主要分为如下几步
1. 导入依赖
2. 编写微信支付的配置
3. 编写下单,进行测试
一、导入依赖
<dependency>
<groupId>com.github.wxpay</groupId>
<artifactId>wxpay-sdk</artifactId>
<version>0.0.3</version>
</dependency>
二、编写微信支付的配置
这里,通过一个配置类,读取application.yml 中的配置,再通过 @Configuration+@Bean的方式,注入到容器中。
public class WxPayScanConfig implements WXPayConfig {
private PayInfoConfig payInfoConfig;
private byte[] certData;
/**
* 使用类加载的方式,取出证书,否则,当发布打成Jar包的时候,会读取不到Jar包中的东西
*/
public WxPayScanConfig(PayInfoConfig payInfoConfig) {
this.payInfoConfig = payInfoConfig;
try {
InputStream certStream = this.getClass().getResourceAsStream("/apiclient_cert.p12");
this.certData = new byte[certStream.available()];
certStream.read(this.certData);
certStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
@Override
public String getAppID() {
return payInfoConfig.getWxpay().getAppId();
}
@Override
public String getMchID() {
return payInfoConfig.getWxpay().getMchId();
}
@Override
public String getKey() {
return payInfoConfig.getWxpay().getMchKey();
}
@Override
public InputStream getCertStream() {
return new ByteArrayInputStream(this.certData);
}
@Override
public int getHttpConnectTimeoutMs() {
return 8000;
}
@Override
public int getHttpReadTimeoutMs() {
return 100000;
}
}
三、编写下单
3.1 编写支付工具类 WxPayUtil
@Slf4j
@Component
public class WxPayUtil {
@Autowired
private WXPay wxPay;
@Autowired
private PayInfoConfig payInfoConfig;
@Autowired
private PayCommonUtil payCommonUtil;
public Map<String,String> appNotify(String body,String price){
SortedMap<String, String> data = new TreeMap<>();
String outTradeNo = getRandomString(32);
data.put("body", body);
data.put("out_trade_no", outTradeNo);
data.put("total_fee", price);
data.put("spbill_create_ip", "58.23.48.202");
data.put("notify_url", "http://localhost:8080/");
data.put("trade_type", "APP");
Map<String, String> resp = null;
try {
resp = wxPay.unifiedOrder(data);
resp.put("out_trade_no",outTradeNo);
} catch (Exception e) {
log.info("微信支付出现错误!" +e.getMessage());
e.printStackTrace();
}
return resp;
}
public Map<String,String> refund(String outTradeNo){
SortedMap<String,String> data = new TreeMap<>();
data.put("appid", payInfoConfig.getWxpay().getAppId());
data.put("mch_id", payInfoConfig.getWxpay().getMchId());
data.put("nonce_str", getRandomString(32));
data.put("out_trade_no",outTradeNo);
data.put("out_refund_no", getRandomString(32));
Map<String, String> orderQuery = queryOrder(outTradeNo);
data.put("total_fee",orderQuery.get("total_fee"));
data.put("refund_fee",orderQuery.get("total_fee"));
data.put("sign", payCommonUtil.createSign("UTF-8",data));
try {
Map<String, String> resp = wxPay.refund(data);
System.out.println(resp);
return resp;
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
public Map<String,String> queryRefund(String outTradeNo){
Map<String, String> data = new HashMap<>(0);
data.put("out_trade_no", outTradeNo);
try {
return wxPay.refundQuery(data);
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
/**
* 获取app二次签名数据
* @param resp 微信统一下单之后返回的data
* @return Map<String,String>
*/
public Map<String,String> appPaySignDouble(Map<String,String> resp){
log.info("response=" + resp);
SortedMap<String, String> signParam = new TreeMap<>();
//app_id
String prepayid = resp.get("prepay_id");
signParam.put("appid", resp.get("appid"));
signParam.put("partnerid", resp.get("mch_id"));
signParam.put("prepayid", prepayid);
signParam.put("package", "Sign=WXPay");
signParam.put("noncestr", resp.get("nonce_str"));
//北京时间时间戳
signParam.put("timestamp", System.currentTimeMillis()/1000 + "");
signParam.put("paySign", payCommonUtil.createSign("UTF-8", signParam));
signParam.put("outTradeNo",resp.get("out_trade_no"));
return signParam;
}
public Map<String,String> queryOrder(String outTradeNo){
Map<String, String> data = new HashMap<>(0);
data.put("out_trade_no", outTradeNo);
try {
return wxPay.orderQuery(data);
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
public String getRandomString(int length){
//定义一个字符串(A-Z,a-z,0-9)即62位;
String str="zxcvbnmlkjhgfdsaqwertyuiopQWERTYUIOPASDFGHJKLZXCVBNM1234567890";
//由Random生成随机数
Random random=new Random();
StringBuffer sb=new StringBuffer();
//长度为几就循环几次
for(int i=0; i<length; ++i){
//产生0-61的数字
int number=random.nextInt(62);
//将产生的数字通过length次承载到sb中
sb.append(str.charAt(number));
}
//将承载的字符转换成字符串
return sb.toString();
}
}
其中用到的工具类:
PayCommonUtil 主要作用为生成签名以及解析xml
@Component
public class PayCommonUtil {
@Autowired
private PayInfoConfig payInfoConfig;
/**
* 定义签名,微信根据参数字段的ASCII码值进行排序 加密签名,故使用SortMap进行参数排序
* @param characterEncoding 字符编码
* @param parameters 需要签名的参数
* @return sign string。class
*/
public String createSign(String characterEncoding,SortedMap<String,String> parameters){
StringBuilder sb = new StringBuilder();
Set<Map.Entry<String, String>> es = parameters.entrySet();
for (Map.Entry<String, String> entry : es) {
String k = entry.getKey();
Object v = entry.getValue();
if (null != v && !"".equals(v)
&& !"sign".equals(k) && !"key".equals(k)) {
sb.append(k).append("=").append(v).append("&");
}
}
//最后加密时添加商户密钥,由于key值放在最后,所以不用添加到SortMap里面去,单独处理,编码方式采用UTF-8
sb.append("key=").append(payInfoConfig.getWxpay().getMchKey());
return MD5Util.MD5Encode(sb.toString(), characterEncoding).toUpperCase();
}
/**
* 将封装好的参数转换成Xml格式类型的字符串
* @param parameters 封装好的参数
* @return String xml类型的字符串
*/
public String getRequestXml(SortedMap<String,String> parameters){
StringBuilder sb = new StringBuilder();
sb.append("<xml>");
Set<Map.Entry<String, String>> es = parameters.entrySet();
for (Map.Entry<String, String> entry : es) {
String k = entry.getKey();
String v = entry.getValue();
if ("sign".equalsIgnoreCase(k)) {
} else if ("attach".equalsIgnoreCase(k) || "body".equalsIgnoreCase(k)) {
sb.append("<").append(k).append(">").append("<![CDATA[").append(v).append("]]></").append(k).append(">");
} else {
sb.append("<").append(k).append(">").append(v).append("</").append(k).append(">");
}
}
sb.append("<" + "sign" + ">" + "<![CDATA[").append(parameters.get("sign")).append("]]></").append("sign").append(">");
sb.append("</xml>");
return sb.toString();
}
/**
* 验证回调签名
* @return Boolean, 是否正确
*/
public boolean isTenpaySign(Map<String, String> map) {
String characterEncoding="utf-8";
String charset = "utf-8";
String signFromAPIResponse = map.get("sign");
if (signFromAPIResponse == null || "".equals(signFromAPIResponse)) {
System.out.println("API返回的数据签名数据不存在,有可能被第三方篡改!!!");
return false;
}
System.out.println("服务器回包里面的签名是:" + signFromAPIResponse);
//过滤空 设置 TreeMap
SortedMap<String,String> packageParams = new TreeMap<>();
for (String parameter : map.keySet()) {
String parameterValue = map.get(parameter);
String v = "";
if (null != parameterValue) {
v = parameterValue.trim();
}
packageParams.put(parameter, v);
}
StringBuilder sb = new StringBuilder();
Set<Map.Entry<String, String>> es = packageParams.entrySet();
for (Object e1 : es) {
@SuppressWarnings("unchecked")
Map.Entry<String, String> entry = (Map.Entry<String, String>) e1;
String k = entry.getKey();
String v = entry.getValue();
if (!"sign".equals(k) && null != v && !"".equals(v)) {
sb.append(k).append("=").append(v).append("&");
}
}
sb.append("key=").append(payInfoConfig.getWxpay().getMchKey());
//将API返回的数据根据用签名算法进行计算新的签名,用来跟API返回的签名进行比较
//算出签名
String resultSign = "";
String tobesign = sb.toString();
if (null == charset || "".equals(charset)) {
resultSign = MD5Util.MD5Encode(tobesign, characterEncoding).toUpperCase();
}else{
try{
resultSign = MD5Util.MD5Encode(tobesign, characterEncoding).toUpperCase();
}catch (Exception e) {
resultSign = MD5Util.MD5Encode(tobesign, characterEncoding).toUpperCase();
}
}
String tenpaySign = (packageParams.get("sign")).toUpperCase();
return tenpaySign.equals(resultSign);
}
public String httpsRequest(String requestUrl, String requestMethod, String outputStr)
{
try
{
// 创建SSLContext对象,并使用我们指定的信任管理器初始化
TrustManager[] tm =
{ null };
SSLContext sslContext = SSLContext.getInstance("SSL", "SunJSSE");
sslContext.init(null, tm, new java.security.SecureRandom());
// 从上述SSLContext对象中得到SSLSocketFactory对象
SSLSocketFactory ssf = sslContext.getSocketFactory();
URL url = new URL(requestUrl);
HttpsURLConnection conn = (HttpsURLConnection) url.openConnection();
conn.setDoOutput(true);
conn.setDoInput(true);
conn.setUseCaches(false);
// 设置请求方式(GET/POST)
conn.setRequestMethod(requestMethod);
conn.setRequestProperty("content-type", "application/x-www-form-urlencoded");
// 当outputStr不为null时向输出流写数据
if (null != outputStr)
{
OutputStream outputStream = conn.getOutputStream();
// 注意编码格式
outputStream.write(outputStr.getBytes(StandardCharsets.UTF_8));
outputStream.close();
}
// 从输入流读取返回内容
InputStream inputStream = conn.getInputStream();
InputStreamReader inputStreamReader = new InputStreamReader(inputStream, StandardCharsets.UTF_8);
BufferedReader bufferedReader = new BufferedReader(inputStreamReader);
String str = null;
StringBuilder buffer = new StringBuilder();
while ((str = bufferedReader.readLine()) != null)
{
buffer.append(str);
}
// 释放资源
bufferedReader.close();
inputStreamReader.close();
inputStream.close();
inputStream = null;
conn.disconnect();
return buffer.toString();
} catch (ConnectException ce) {
// log.error("连接超时:{}", ce);
} catch (Exception e) {
}
return null;
}
}
MD5Util 主要作用,用于数据的加密,默认方式为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.2 编写Controller,调用封装的工具类方法
这里需要注意的是,需要进行两步操作,分别为
- 统一下单(appNotify)
- 二次签名(将统一下单中的一部分数据,进行二次的签名,详见 微信支付开发文档 )
需要注意的是,wxpay和alipay不同的是,wxpay的price,是按照分来计算的(即1=1分,输入为100,代表收取1块钱),且不能包含小数点,所以在传入值的时候,需要将Double强转成int
@Autowired
private WxPayUtil wxPayUtil;
@GetMapping("/test")
public ResponseModel test(String title, Double price) {
Map<String, String> response = wxPayUtil.appNotify(title, ((int)(price * 100)) + "");
Map<String, String> result = wxPayUtil.appPaySignDouble(response);
return ResponseModel.success("签名成功!", result);
}
二次签名之后返回的数据如下所示
{
"appid": "",
"noncestr": "s3rziO04IYbvhcvU",
"outTradeNo": "a14As0d1zqmnVWvN1FMriQmPNu3lPW5P",
"package": "Sign=WXPay",
"partnerid": "",
"paySign": "F549A5ADE9E767C49A8432AA2F9297E8",
"prepayid": "wx09165100326315c5XXXXXXXXXXXXXXXXXX",
"timestamp": "1599641460"
},
如果,二次签名返回的数据中,都是不为null的,说明,有很大可能,是签名成功的,或者,可以通过 微信支付接口签名校验工具 来进行验证,如果验证成功,此时就靠前端朋友,来测试微信支付了,看能否成功了
如下,验证应该的sign值为 F549A5ADE9E767C49A8432AA2F9297E8,实际返回给前端的paySign也是 F549A5ADE9E767C49A8432AA2F9297E8,即代表签名成功,可以考虑发起支付了
四、APP进行支付测试
APP 端的测试,详见另一篇文章