前言
该文主要是手把手教你如何在SpringBoot 中集成微信扫码支付,以及集成的过程需要注意的问题事项。另外需要感谢 vbirdbest 关于微信支付和支付宝支付相关包博客总结。因为文中很多地方参考了vbirdbest的博客。 vbirdbest 博主关于支付宝和微信相关总结GitHub地址。vbirdbest总结已经很好了,那么我为什么要在写一篇呢?我想通过另一种角度带你如何看微信文档并能自己实现它。以及我们需要注意的问题。毕竟涉及支付不能太马虎。
准备工作
在开发微信支付功能之前,首先要确保你的微信公众号是服务
号,因为只有服务号才有申请微信支付的权限。
申请服务号支付权限,相当于在微信商户平台申请与之对应的商户平台账号。当商户平台帐号申请完毕后,微信会给你绑定的邮箱发送一个包含商户号的邮件,这个商户号很重要,后面微信支付 API 调用会用到。
扫码支付还需要在微信商户平台-账户设置-安全设置-api安全 设置一个32位 Key。这个Key是用来生成验证的sign使用的。具体使用后面会介绍到。
详细操作请直接参考 vbirdbest 博主的博客:Spring Boot入门教程(三十九):微信支付集成-申请服务号和微信支付:
查看微信开发文档
访问:https://pay.weixin.qq.com/wiki/doc/api/index.html 如下图所示,选择 Native 支付
扫码支付有2中模式:
- 模式一:需要设置回调 URL 使用流程比较复杂。
- 模式二:无需设置回调 URL 使用流程比较简单。
该文介绍使用的是模式二的方式,模式二业务流程说明如下:
(1)商户后台系统根据用户选购的商品生成订单。
(2)用户确认支付后调用微信支付【统一下单API】生成预支付交易;
(3)微信支付系统收到请求后生成预支付交易单,并返回交易会话的二维码链接code_url。
(4)商户后台系统根据返回的code_url生成二维码。
(5)用户打开微信“扫一扫”扫描二维码,微信客户端将扫码内容发送到微信支付系统。
(6)微信支付系统收到客户端请求,验证链接有效性后发起用户支付,要求用户授权。
(7)用户在微信客户端输入密码,确认支付后,微信客户端提交授权。
(8)微信支付系统根据用户授权完成支付交易。
(9)微信支付系统完成支付交易后给微信客户端返回交易结果,并将交易结果通过短信、微信消息提示用户。微信客户端展示支付交易结果页面。
(10)微信支付系统通过发送异步消息通知商户后台系统支付结果。商户后台系统需回复接收情况,通知微信后台系统不再发送该单的支付通知。
(11)未收到支付通知的情况,商户后台系统调用【查询订单API】。
(12)商户确认订单已支付后给用户发货。
了解完业务流程后,需要查看微信统一下单API 文档。这里需要提示的是,这个统一下单API一定要好好阅读。
统一下单API 大致信息如下:
- URL地址:https://api.mch.weixin.qq.com/pay/unifiedorder 不需要证书
- 处了总金额类型是int外,其他参数一律是String
- 必须的参数有:服务商的 appid、商户号(mch_id)、随机字符串 nonce_str、签名 sign、商品描述 body、商户订单号 out_trade_no、总金额 total_fee、终端IP spbill_create_ip、通知地址(回调地址) notify_url 、交易类型 trade_type。
需要注意的内容如下:
- 总金额是以分计算的,需要我们将金额进行转换
- sign是将参数字典排序然后最后拼接Key(微信商户平台-账户设置-安全设置-api安全 设置一个32位 Key)进行MD5或HMAC-SHA256 进行加密的结果。
- 通知地址 notify_url 必须是外网访问URL,并且不能携带参数。
签名算法和生成随机数算法规则: https://pay.weixin.qq.com/wiki/doc/api/native.php?chapter=4_3
需要使用的工具如下:
(签名校验工具): https://pay.weixin.qq.com/wiki/doc/api/native.php?chapter=20_1
根据文档编写最简代码实现扫码支付
阅读完API 我们就可以开始写调用微信支付的测试代码了。其实微信支付的调用,就是把我们的商品信息以及商品的价格数据发送给微信,然后微信在把支付的code返回。我们根据支付code生成二维码后让用户扫码,用户付款成功后通过通知地址 notify_url 回调我们的系统,通知系统支付成功。
在写代码之前,先吐槽一下微信。我记得我们公司当时在对接微信时,微信连 Java 的 SDK 都没有。有啥问题也联系不上客服,当时对接的时候出现问题只能百度。还有就是文档啥的,跟阿里比差远了。废话少说开始我们的测试代码编写了。
创建 WeiXinPayTestController 然后定义 nativePay 访问方法
public class WeiXinPayTestController {
private static final String UNIFIEDORDERURL = "https://api.mch.weixin.qq.com/pay/unifiedorder";
private static final String GETSIGNKEYURL = "https://api.mch.weixin.qq.com/sandboxnew/pay/getsignkey";
private static final String SANDBOXUNIFIEDORDERURL = "https://api.mch.weixin.qq.com/sandboxnew/pay/unifiedorder";
private static final String ORDERQUERYURL = "https://api.mch.weixin.qq.com/pay/orderquery";
@Autowired
private WeiXinPayProperties weiXinPayProperties;
@Autowired
private RestTemplate restTemplate;
/**
* 正式支付
* @return
*/
@RequestMapping("/nativePay")
public String nativePay(){
}
我尝试用沙盒进行支付结果各种密钥不对,最后只好暂时先放弃了。
WeiXinPayProperties 是通过SpringBoot @ConfigurationProperties定义的配置文件类具体内容如下:
@Component
@ConfigurationProperties(prefix="wx.pay")
public class WeiXinPayProperties {
/**合作身份者ID */
private String appid;
/** 商户号 */
private String mchId;
/** 商户号密钥 */
private String appsecret;
/** API 密钥 商户后台配置的一个32位的key 微信商户平台-账户设置-安全设置-api安全 */
private String key;
/**是否使用沙箱*/
private String useSandbox;
/** 沙箱环境API 密钥 */
private String sandboxKey;
/** 回调地址 */
private String notifyUrl;
省略get and set方法
}
微信支付Api的调用我们通过 RestTemplate 来完成。
在 nativePay 方法内配置微信的基础信息:公众账号ID、商户号,随机字符串、终端IP、交易类型
Map<String, String> requestData = new HashMap<String, String>();
requestData.put("appid", weiXinPayProperties.getAppid());//公众账号ID
requestData.put("mch_id", weiXinPayProperties.getMchId());//商户号
requestData.put("nonce_str", RandomUtil.randomString(15));//随机字符串 32位以内
requestData.put("spbill_create_ip", "15.23.160.111");
requestData.put("trade_type", "NATIVE");//交易类型 扫码支付
spbill_create_ip APP和网页支付提交用户端ip,Native支付填调用微信支付API的机器IP。
配置微信支付自定义支付信息参数
requestData.put("attach", "附加数据远洋返回");
requestData.put("body", "订单号 BW_000001");//商品简单描述
requestData.put("out_trade_no", "BW_000001");//商户订单号
requestData.put("total_fee", WeiXinUtil.getMoney("0.01"));//标价金额 按照分进行计算
requestData.put("notify_url", "www.beiwaiclass.com");//通知地址 异步接收微信支付结果通知的回调地址必须外网访问 不能携带参数
配置微信支付sign信息参数
String sign = null;
String payUrl = null;
if(Boolean.valueOf(weiXinPayProperties.getUseSandbox())){
sign = WeiXinUtil.generateSign(requestData,weiXinPayProperties.getKey());//生成签名
payUrl = UNIFIEDORDERURL;
}else{
sign = WeiXinUtil.generateSign(requestData,weiXinPayProperties.getSandboxKey());//生成签名
payUrl = SANDBOXUNIFIEDORDERURL;
}
requestData.put("sign", sign);
将配置Map信息转换成String
String mapToXmlStr = XmlUtil.mapToXmlStr(requestData, "xml");
将mapToXmlStr作为参数调用微信统一下单Api
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_XML);
HttpEntity<String> formEntity = new HttpEntity<>(mapToXmlStr, headers);
ResponseEntity<String> postForEntity = restTemplate.postForEntity(payUrl, formEntity, String.class);
//获取微信返回的信息
String returnXmlString = postForEntity.getBody();
Map<String, Object> xmlToMap = XmlUtil.xmlToMap(returnXmlString);
String returnCode = (String)xmlToMap.get("return_code");
if("SUCCESS".equals(returnCode)){
String codeUrl = (String)xmlToMap.get("code_url");
return codeUrl;
}
前台访问的页面(gotoNativePage.html)代码如下:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body >
<h3>购买商品:苹果</h3>
<h3>价格:20</h3>
<h3>数量:10个</h3>
<div id="qrcodeCanvas"></div>
<button style="width: 8%; height: 30px; alignment: center; background: #b49e8f" onclick="commitOrder()">生成支付二维码</button>
<script type="text/javascript" src="/sbe2/jquery-1.8.3.min.js"></script>
<script type="text/javascript" src="/sbe2/jquery.qrcode.js"></script>
<script type="text/javascript" src="/sbe2/qrcode.js"></script>
<script>
function commitOrder() {
$.ajax({
type: "POST",
url: "http://localhost:8090/sbe2/wx/naitvePay",
data: null,
success: function(data) {
jQuery('#qrcodeCanvas').qrcode({
text: data,
typeNumber:-10
});
}
})
}
</script>
</body>
</html>
测试结果:
http://localhost:8090/sbe2/gotoNativePage.html
点击生成支付二维码会生成要支付的二维码在数量下面。
支付回调的代码:
/**
* 支付回调
* @throws Exception
*/
@RequestMapping("/wxNotify")
public void wxNotify(HttpServletRequest request) throws Exception{
Map<String, String> parseNotifyParameter = parseNotifyParameter(request);
String sign = WeiXinUtil.generateSign(parseNotifyParameter,weiXinPayProperties.getKey());//生成签名
if(sign.equals(parseNotifyParameter.get("sign"))){
//支付成功
}
}
/**
* 从request的inputStream中获取参数
* @param request
* @return
* @throws Exception
*/
public Map<String, String> parseNotifyParameter(HttpServletRequest request) throws Exception {
InputStream inputStream = request.getInputStream();
ByteArrayOutputStream outSteam = new ByteArrayOutputStream();
byte[] buffer = new byte[1024];
int length = 0;
while ((length = inputStream.read(buffer)) != -1) {
outSteam.write(buffer, 0, length);
}
outSteam.close();
inputStream.close();
// 获取微信调用我们notify_url的返回信息
String resultXml = new String(outSteam.toByteArray(), "utf-8");
Map<String, String> notifyMap = WeiXinUtil.xmlToMap(resultXml);
return notifyMap;
}
支付回调需要外网,如果你想进行测试可以进行本地域名映射到外网进行整体的测试。因为账号我无法在进行更改,所以就没有做。但是代码是没有啥大问题的。
使用官方 SDK 实现扫码支付
我们已经简单的完成微信支付的集成,但是个人建议使用官方的SDK ,因为其他的SDK可能有XXE漏洞
。关于XXE漏洞
官方处理方案 https://pay.weixin.qq.com/wiki/doc/api/jsapi.php?chapter=23_5
下载JAVA版的SDK,然后导入到将SDK 通过 mvn install
安装到本地仓库中。然后再SpringBoot中引入 SDK 的依赖。
我下载的 SDK 版本是:3.0.9
<dependency>
<groupId>com.github.wxpay</groupId>
<artifactId>wxpay-sdk</artifactId>
<version>3.0.9</version>
</dependency>
你也可以根据自己的需要对SDK进行修改。
依赖添加完毕后第一步需要集成 微信SDK的配置抽象类:
@Component
@ConfigurationProperties(prefix="wxpay")
public class WeiXinOfficPayProperties extends WXPayConfig {
private String appID;
private String mchID;
private String key;
private InputStream certStream;
private int httpConnectTimeoutMs;
private int httpReadTimeoutMs;
private IWXPayDomain WXPayDomain;
}
微信扫码支付代码:
/**
* 微信扫码支付
* @return
*/
@RequestMapping("/naitvePay")
public String naitvePay(){
try {
WXPay wxpay = new WXPay(weiXinPayProperties);
Map<String, String> data = new HashMap<String, String>();
data.put("body", "购买苹果10个");
data.put("out_trade_no", "2016090910595900000012");
data.put("device_info", "");
data.put("fee_type", "CNY");
data.put("total_fee", "1");
data.put("spbill_create_ip", "123.12.12.123");
data.put("notify_url", "http://www.example.com/wxpay/notify");
data.put("trade_type", "NATIVE"); // 此处指定为扫码支付
data.put("product_id", "12");
Map<String, String> resp = wxpay.unifiedOrder(data);
String returnCode = resp.get("return_code");
if("SUCCESS".equals(returnCode)){
String codeUrl = resp.get("code_url");
return codeUrl;
}
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
微信支付的回调:
/**
* 微信支付成功后回调地址
* @param request
*/
@RequestMapping("/wxNotify")
public void wxNotify(HttpServletRequest request) {
try {
Map<String, String> notifyMap = WeiXinPaySupportUtil.getNotifyParameter(request);
WXPay wxpay = new WXPay(weiXinPayProperties);
if (wxpay.isPayResultNotifySignatureValid(notifyMap)) {
// 签名正确
// 进行处理。
// 注意特殊情况:订单已经退款,但收到了支付结果成功的通知,不应把商户侧订单状态从退款改成支付成功
}
else {
// 签名错误,如果数据里没有sign字段,也认为是签名错误
}
} catch (Exception e) {
e.printStackTrace();
}
}
进行测试:
http://localhost:8090/sbe2/gotoNativePage2.html
需要说明的是官方 3.0.9 Java SDK 正式支付生成 sign 默认使用的是 HMAC-SHA256
小结
微信支付集成并不是很难,但是集成的时候有很多地方要注意:
- 首先是 sign 的生成需要排序传递参数,但是不包含 sign 本身这个字段。
- 回调地址 notify_url 切记需要外网访问,写完回调的方法后建议自己先调用一下看看能不能调用成功。
- 订单在重复支付的时候,要保证支付的参数和第一次支付一致,否则会生成code失败。
- 如果没有使用微信支付官方SDK,要注意XXE漏洞。也可以直接通过前台轮询进行查询的方式判断是否支付成功。
- 后台要添加手动判断订单是否支付成功,并且同步的操作。防止订单支付成功,用户没有够买到的情况。
代码示例
具体代码示例请查看 :
博客的读者可以通过查看下面仓库的中的模块工程名: spring-boot-2.x-wxpay
Github:spring-boot-2.x-wxpay
如果您对这些感兴趣,欢迎 star、或转发给予支持!转发请标明出处!
参考文献
Spring Boot入门教程(四十一):微信支付集成-扫码支付:
Spring Boot入门教程(三十九):微信支付集成-申请服务号和微信支付: