支付通知(接收支付通知【签名验证、参数解密、处理订单(更新订单状态、记录支付日志、重复通知的接口幂等性处理、可重入锁)】和 返回应答【应答成功、应答失败】)
就是当用户扫描二维码并支付之后,微信支付那边就会返回一个【支付通知】,来告诉商户他的支付结果。商户收到这个支付通知后,会进行一些签名的验签和订单的处理操作之类的,然后再响应回给微信系统,向微信支付系统应答我们对这个支付通知的处理情况。
(应答包括:接收支付通知成功(响应码:200或204)和接收失败(响应码:4xx或5xx))
正常就是告诉微信它发来的支付通知,我们这边已经收到了,并且通过支付通知的数据,处理完自己的核心业务了,并给微信支付平台一个成功的应答。
现在就是做图片中的这步:
微信平台异步通知商户支付结果,商户端告知支付通知的接收情况。
这里是接收微信支付系统发来的商户支付后的支付通知,这边先不做验签和订单处理,先简单的写几个应答情况给微信支付平台看应答效果
应答情况分为:
接收支付通知成功则返回响应码200或204
接受支付通知失败则返回响应码5xx或4xx
支付通知:
这里是接收微信支付系统发来的商户支付后的支付通知,这边先不做验签和订单处理,先简单的测试应答情况给微信支付平台
WxPayController 的这个 nativeNotify() 方法是让微信支付平台来调用的,不是我们去调用的
路径的来源如图:之前调用微信平台的下单接口时,发送过去的我们定义的一个回调支付通知地址。
当我们支付成功后哦,微信平台就会通过这个地址,把支付情况发送给我们。
如图,发来的数据是加密的,后续需要对微信发来的结果进行验签,验签成功再用对称加密的密钥对数据进行解密。
支付通知:
当响应给微信支付系统的响应码是错误的201时,属于应答不符合规范。
微信认为通知失败,因为我们要么没收到通知,要么处理通知失败,所以微信会通过一定的策略定期重新发起通知
让线程睡眠5秒,然后微信支付那边会因为超时没有收到商户的应答,而认为商户这边没有收到通知或通知处理失败,就会继续按规则重复发送通知
APIv3证书与密钥使用说明:
自定义一个针对微信平台发来的request类型的支付通知的签名,进行验签操作的工具类。
因为微信支付端并没有给我们提供默认的集成在SDK内部的不用我们自己编写的签名验证,所以我们要自己编写一个【针对微信发来的请求的一个签名验证】。
先看一个jar包里面的针对响应的一个签名验证工具类,参考SDK源码中的 WechatPay2Validator 创建通知验签工具类 WechatPay2ValidatorForRequest
我们要弄一个针对微信支付系统发来的请求来进行验证的工具类,就可以模仿这个工具类
构造验签名串:
自定义验签工具类
验签工具类–验的是微信支付平台发给商户端的支付通知,
属于request请求
@CrossOrigin //跨域
@RestController
@RequestMapping("/api/wx-pay")
@Api(tags = "网站微信支付API") //swagger 注解
@Slf4j
public class WxPayController
{
@Resource
private WxPayService wxPayService;
//获取签名验证器需要的类
@Resource
private Verifier verifier;
//调用统一下单API,生成支付二维码的链接和订单号
//swagger注解
@ApiOperation("调用统一下单API,生成支付二维码")
@PostMapping("/native/{productId}")
public R nativePay(@PathVariable Long productId) throws Exception
{
log.info("发起支付请求");
//返回支付二维码的链接和订单号
Map<String, Object> map = wxPayService.nativePay(productId);
return R.ok().setData(map);
}
/*
* 接收并处理完微信发来的通知后,需要给微信支付系统应答我们的处理情况
* 告诉微信它发来的支付成功的通知,我们这边已经收到了,并且处理完自己的核心业务了,现在可以给微信一个成功的应答了。
*/
//当我们支付后,微信支付系统会通过我们之前给的notify_url路径,把支付的结果响应回来
//通知接口(回调通知)---接收微信服务器给我们发来的请求
//request: 接收微信支付端发来的支付通知的请求的参数数据 , response: 响应回给微信支付端的应答
@PostMapping("/native/notify")
public String nativeNotify(HttpServletRequest request, HttpServletResponse response)
{
Gson gson = new Gson();
//创建一个应答对象
HashMap<String, String> map = new HashMap<>();
try
{
//处理微信支付系统响应回来的通知参数
String body = HttpUtils.readData(request);
//gson.fromJson()是Gson库提供的一个方法,用于将 JSON 字符串转换为 Java 对象
Map<String, Object> bodyMap = gson.fromJson(body, HashMap.class);
//微信支付端发来的支付通知的请求的id
String requestId = (String) bodyMap.get("id");
log.info("支付通知的id ====> {}", bodyMap.get("id"));
//log.info("支付通知的完整数据 ====> {}", body);
//todo:签名的验证
WechatPay2ValidatorForRequest wechatPay2ValidatorForRequest =
new WechatPay2ValidatorForRequest(verifier, requestId,body);
//判断验签是否成功
if (!wechatPay2ValidatorForRequest.validate(request))
{
log.error("通知验签失败");
//如果验签不通过,就给个失败的应答
response.setStatus(500);
map.put("code", "ERROR");
map.put("message", "微信支付系统发给商户的通知,验签失败");
return gson.toJson(map);
}
log.info("签名验证成功");
//todo :处理订单
/*
* 通知应答
* 接收成功: HTTP应答状态码需返回200或204,无需返回应答报文。
* 接收失败: HTTP应答状态码需返回5XX或4XX,同时需返回应答报文,格式如下
*/
//Thread.sleep(10000); //单位:毫秒, 1000毫秒=1秒,这里是睡眠10秒钟
//TimeUnit.SECONDS.sleep(6); //让线程睡眠超过5秒
response.setStatus(200);
map.put("code", "SUCCESS");
map.put("message", "成功");
return gson.toJson(map);
} catch (Exception e)
{
e.printStackTrace();
response.setStatus(500);
map.put("code", "ERROR");
map.put("message", "失败");
return gson.toJson(map);
}
}
}
验签工具类–验的是微信支付平台发给商户端的支付通知,
属于request请求
package cn.ljh.paymentdemo.util;
import com.wechat.pay.contrib.apache.httpclient.auth.Verifier;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.time.DateTimeException;
import java.time.Duration;
import java.time.Instant;
import static com.wechat.pay.contrib.apache.httpclient.constant.WechatPayHttpHeaders.*;
//这个类用来验证微信支付系统发给商户的支付通知的签名,就是验签
public class WechatPay2ValidatorForRequest
{
protected static final Logger log = LoggerFactory.getLogger(WechatPay2ValidatorForRequest.class);
/**
* 应答超时时间,单位为分钟
*/
protected static final long RESPONSE_EXPIRED_MINUTES = 5;
protected final Verifier verifier;
protected final String requestId;
protected final String body;
//参数1:获取签名验证器 参数2:微信支付系统发来的那个请求通知的id 参数3:微信支付系统响应回来的通知参数
public WechatPay2ValidatorForRequest(Verifier verifier, String requestId, String body)
{
this.verifier = verifier;
this.requestId = requestId;
this.body = body;
}
protected static IllegalArgumentException parameterError(String message, Object... args)
{
message = String.format(message, args);
return new IllegalArgumentException("parameter error: " + message);
}
protected static IllegalArgumentException verifyFail(String message, Object... args)
{
message = String.format(message, args);
return new IllegalArgumentException("signature verify fail: " + message);
}
//验签方法
public final boolean validate(HttpServletRequest request) throws IOException
{
try
{
//处理请求参数
validateParameters(request);
//调用构造验签名串方法
String message = buildMessage(request);
//从请求头当中拿到平台证书序列号
String serial = request.getHeader(WECHAT_PAY_SERIAL);
//从请求头当中拿到请求当中携带的签名
String signature = request.getHeader(WECHAT_PAY_SIGNATURE);
//利用verifier这个对象,进行验签的具体操作
if (!verifier.verify(serial, message.getBytes(StandardCharsets.UTF_8), signature))
{
//如果验签失败,则抛异常
throw verifyFail("serial=[%s] message=[%s] sign=[%s], request-id=[%s]",
serial, message, signature, requestId);
}
} catch (IllegalArgumentException e)
{
log.warn(e.getMessage());
return false;
}
//验签成功
return true;
}
//处理请求参数的方法-- 对参数进行判断的过程
protected final void validateParameters(HttpServletRequest request)
{
// NOTE: ensure HEADER_WECHAT_PAY_TIMESTAMP at last
String[] headers = {WECHAT_PAY_SERIAL, WECHAT_PAY_SIGNATURE, WECHAT_PAY_NONCE, WECHAT_PAY_TIMESTAMP};
String header = null;
//判空
for (String headerName : headers)
{
header = request.getHeader(headerName);
if (header == null)
{
throw parameterError("empty [%s], request-id=[%s]", headerName, requestId);
}
}
//header 就是这个---> WECHAT_PAY_TIMESTAMP 时间戳,用于判断请求是否过期
String timestampStr = header;
try
{
//通过时间戳创建一个基于时间戳那个时间的时间对象
Instant responseTime = Instant.ofEpochSecond(Long.parseLong(timestampStr));
// 如果时间戳过期了,就拒绝过期请求, 创建一个基于此时此刻的时间对象--> Instant.now()
//比较这两个时间对象 --> responseTimeh 和 Instant.now() ,如果比较的时间大于5分钟,就会认为这是一个过期的请求,那么就拒绝
if (Duration.between(responseTime, Instant.now()).abs().toMinutes() >= RESPONSE_EXPIRED_MINUTES)
{
throw parameterError("timestamp=[%s] expires, request-id=[%s]", timestampStr, requestId);
}
} catch (DateTimeException | NumberFormatException e)
{
throw parameterError("invalid timestamp=[%s], request-id=[%s]", timestampStr, requestId);
}
}
//构造验签名串方法
protected final String buildMessage(HttpServletRequest request) throws IOException
{
//获取时间戳
String timestamp = request.getHeader(WECHAT_PAY_TIMESTAMP);
//随机数的字符串形式
String nonce = request.getHeader(WECHAT_PAY_NONCE);
return timestamp + "\n"
+ nonce + "\n"
+ body + "\n";
}
}
流程:
1、商户端向微信平台发起支付请求。
2、微信平台则对该请求进行响应,响应的内容是一个订单编号和支付二维码的 URL 。
3、商户端这边接收到支付二维码的URL,通过这个URL获取支付二维码图片,并进行支付。
4、商户端进行支付之后,微信平台会自动发起一个请求,请求的内容是商户端支付的结果,就是支付通知。
5、商户端这边对该支付通知进行验签,得到该请求的数据并进行一些业务操作,然后给微信支付平台应答,告诉其自己已经成功收到该通知了。
应答的内容也包括:接收通知成功和接收通知失败两种。
6、如果给微信支付平台的应答是接受通知失败的状态码。那么微信支付平台就会根据规则,在一段事件内重复发送支付通知给客户端。当然,这个重复发送通知的作用,只是为了提高商户端那边能成功接收到通知的概率(比如商户端接收通知失败的原因可以有网络原因),但是不保证一定成功。
签名和验签的理解:
商户端发送一个支付请求给微信支付平台:
商户端需要将请求中的一些敏感参数,通过微信支付平台提供的平台证书的公钥,进行加密,变成密文,然后再对整个请求进行签名加密,
微信支付平台接收到请求后:
1、需要先对签名进行验签,作用就是判断发过来的请求是否被篡改过。
2、如果没篡改过,就从该请求中获取数据密文ciphertext(就是加密了的那些参数存放的地方),再用微信平台自己的私钥(对称加密秘钥),对请求中的数据密文进行解密,解密成明文,然后使用该参数数据进行业务操作。
反过来,微信支付平台发送请求到商户端,也是一样的操作
如何加密解密敏感信息
商户端这边,对微信平台发来的支付通知中的密文(通知参数)进行解密。
APIv3证书与密钥使用说明
加密的数据就存在这个ciphertext里面,我们要把它解密出来
密文就在这里:支付通知中的resource中的ciphertext
每次测试都要记得看ngrok是否已经过期了,要重新生成隧道–就是配置文件中的接收结果通知地址wxpay.notify-domain,不然的话,微信支付系统在回调方法的时候,调用不到这个nativeNotify()方法
cmd打开ngrok,输入这个:ngrok http 8090 命令创建一条隧道。
更新订单的支付状态 和 记录支付日志
**更新订单的支付状态:**就是更新未支付、已支付这些状态。
当我们支付成功并且收到微信支付平台发来的支付通知时,获取支付通知里面的支付状态,并更新设置到该订单表里面。
记录支付日志就是有一个专门的表用来记录支付的记录,从支付通知中获取的参数,得到想要的数据封装成一个PaymentInfo 支付记录对象,存到数据库表 t_payment_info 里面
支付通知
还是属于在支付通知的方法上面继续完善功能。
成功更新订单状态和记录一条支付日志到指定表 t_payment_info 中。
记录支付的日志记录
处理重复通知:
商户端进行支付之后,微信支付平台就会返回一个支付通知,商户端这边接收到通知后,就会修改订单的支付状态,以及添加一条支付记录的日志,然后就会应答回给微信支付平台,说自己已经成功接收到支付通知了。
但是,商户端收到支付结果通知,需要在5秒内返回应答报文,否则微信支付会认为通知失败,后续会重复发送通知。
**问题来了:**商户端这边每次接收到通知,都会修改一下订单的状态和记录一条支付日志。在微信支付那边重复发送通知的情况下,修改订单状态因为每次都是修改支付成功,所以修改多次没影响,但是记录支付日志,如果重复接收到支付通知,那么就会重复的添加日志记录,原本只有一条的记录,会变成记录了很多条。
**影响场景:**如果这个添加日志记录有顺便添加积分之类的功能,就会导致原本只加10积分,变成加了几十上百的积分。
处理重复通知:---------- 处理因商户端没及时应答而导致微信支付端重复发送支付通知而导致出现多条重复的支付日志记录的情况
要实现接口调用的幂等性:就是无论接口被调用多少次,产生的结果都是一致的。
支付通知:
很简单。
每次的接口调用,都先根据商品的订单号查询该商品的订单状态,如果订单状态不是【未支付】,说明该订单已经处理过支付通知了(就是处理过日志记录这些操作了)。
那么就直接return结束这个接口方法的执行就可以了。
判断订单的支付状态,如果不是未支付,表示这个订单已经处理过了,那么就直接结束这个方法的执行。
成功,无论因为应答不及时,而导致微信支付多次调用这个处理支付通知的接口,最终的结果都是支付日志记录只有一条,保证了接口调用的幂等性。
如图,可以看出该接口被多次调用
成功,虽然支付通知的接口被多次调用,但是同一个订单的日志记录始终只有一条,也只能有一条。
模拟第一次处理通知时,因为网络问题,在商户端没有及时应答的情况下,微信支付平台定时重复发起支付通知,然后刚好多个调用这个接口的线程一起要执行这一步,就会导致出现多条支付日志记录。
(但是我演示的时候,并没有出现多条支付日志记录,但还是会出现这个问题)
就是微信支付系统多次调用这个支付通知的方法,有可能会同时执行这个记录支付日志的方法导致出现多条同样的日志记录
使用锁对数据进行并发控制。
使用可重入锁把这个判断和执行记录支付日志的方法包起来。
如果使用普通的synchronized锁,因为一个资源只能被一个线程占有,没释放之前其他线程是不能获取这个资源的。
可重入锁就是,我这个线程获取到这个资源的锁后,可以根据需要,再重复获取这个资源的锁,而且不会引发死锁。
处理订单的方法
因为写这锁之前,那个延时模拟并发通知的效果没出现,所以测试并没有测试出区别,跟需求5的处理重复通知一样的效果。
A线程要访问B共享资源:
正常情况的锁:A获取了B资源的锁,进行访问,然后根据需要,A线程再次获取这个B资源的锁再进行访问,正常情况是不行的,引发死锁,就是我占用了这个锁,又要再去获取这个锁,而我本身又因为线程没执行完,还占有这个锁,就造成了死锁。
可重入锁:就是在上面这种情况下,A已经持有B资源的锁了,又想再次获取B资源的锁,如果这个B资源方法是用可重入锁包起来的,那么就没问题了,因为可重入锁允许线程多次获取同一把锁,从而避免死锁的问题。