基本的支付系统有哪些组成-极客
支付系统结构图:
支付具体流程:
聚合支付流程图:
支付系统的组成:
微信支付的7种支付流程
JSAPI支付
APP支付
H5支付
Native支付
小程序支付
合单支付
付款码支付
微信支付签名验签流程
用户发起请求支付的过程中, 会有签名和验签的过程:
签名过程
构造签名串
HTTP请求方法\n URL\n 请求时间戳\n 请求随机串\n 请求报文主体\n
计算签名值
绝大多数编程语言提供的签名函数支持对签名数据进行签名。强烈建议商户调用该类函数,使用商户私钥对待签名串进行SHA256 with RSA签名,并对签名结果进行Base64编码得到签名值。 $ echo -n -e \ "GET\n/v3/certificates\n1554208460\n593BEC0C930BF1AFEB40B4A08C8FB242\n\n" \ | openssl dgst -sha256 -sign apiclient_key.pem \ | openssl base64 -A uOVRnA4qG/MNnYzdQxJanN+zU+lTgIcnU9BxGw5dKjK+VdEUz2FeIoC+D5sB/LN+nGzX3hfZg6r5wT1pl2ZobmIc6p0ldN7J6yDgUzbX8Uk3sD4a4eZVPTBvqNDoUqcYMlZ9uuDdCvNv4TM3c1WzsXUrExwVkI1XO5jCNbgDJ25nkT/c1gIFvqoogl7MdSFGc4W4xZsqCItnqbypR3RuGIlR9h9vlRsy7zJR9PBI83X8alLDIfR1ukt1P7tMnmogZ0cuDY8cZsd8ZlCgLadmvej58SLsIkVxFJ8XyUgx9FmutKSYTmYtWBZ0+tNvfGmbXU7cob8H/4nLBiCwIUFluw==
设置HTTP头
微信支付商户API v3要求请求通过HTTP Authorization头来传递签名。 Authorization由认证类型和签名信息两个部分组成。 下面我们使用命令行演示如何生成签名。 Authorization: 认证类型 签名信息 具体组成为: 1.认证类型,目前为WECHATPAY2-SHA256-RSA2048 2.签名信息 发起请求的商户(包括直连商户、服务商或渠道商)的商户号 mchid 商户API证书序列号serial_no,用于声明所使用的证书 请求随机串nonce_str 时间戳timestamp 签名值signature 注:以上五项签名信息,无顺序要求。 Authorization 头的示例如下:(注意,示例因为排版可能存在换行,实际数据应在一行) Authorization: WECHATPAY2-SHA256-RSA2048 mchid="1900009191",nonce_str="593BEC0C930BF1AFEB40B4A08C8FB242",signature="uOVRnA4qG/MNnYzdQxJanN+zU+lTgIcnU9BxGw5dKjK+VdEUz2FeIoC+D5sB/LN+nGzX3hfZg6r5wT1pl2ZobmIc6p0ldN7J6yDgUzbX8Uk3sD4a4eZVPTBvqNDoUqcYMlZ9uuDdCvNv4TM3c1WzsXUrExwVkI1XO5jCNbgDJ25nkT/c1gIFvqoogl7MdSFGc4W4xZsqCItnqbypR3RuGIlR9h9vlRsy7zJR9PBI83X8alLDIfR1ukt1P7tMnmogZ0cuDY8cZsd8ZlCgLadmvej58SLsIkVxFJ8XyUgx9FmutKSYTmYtWBZ0+tNvfGmbXU7cob8H/4nLBiCwIUFluw==",timestamp="1554208460",serial_no="1DDE55AD98ED71D6EDD4A4A16996DE7B47773A8C"
验签
验签有两个时候, 一种是我们发送请求支付生成预支付单, 对方同步应答的时候, 验证微信的同步应答, 一种是支付结果时候对方的异步请求, 同步应答是建议验签, 而异步应答是必须要验签的, 验签的时候使用微信支付平台的公钥验签, 公钥只能从平台证书中获取, 平台证书是通过API获取的
微信支付Java接入
前提
是为了获取:标识商户身份的信息、商户的证书和私钥、微信支付的证书、微信支付API的URL
1.获取商户号
微信商户平台:https://pay.weixin.qq.com/ 步骤:申请成为商户 => 提交资料 => 签署协议 => 获取商户号
2.获取AppID
微信公众平台:https://mp.weixin.qq.com/ 步骤:注册服务号 => 服务号认证 => 获取APPID => 绑定商户号
3.申请商户证书
步骤:登录商户平台 => 选择 账户中心 => 安全中心 => API安全 => 申请API证书 包括商户证书和商户私钥
4.获取微信的证书
可以预先下载,也可以通过编程的方式获取。
5.获取APIv3秘钥(在微信支付回调通知和商户获取平台证书使用APIv3密钥)
步骤:登录商户平台 => 选择 账户中心 => 安全中心 => API安全 => 设置APIv3密钥
1.引依赖
<!--wechatpay-sdk-->
<dependency>
<groupId>com.github.wechatpay-apiv3</groupId>
<artifactId>wechatpay-apache-httpclient</artifactId>
<version>0.4.5</version>
</dependency>
2.写配置类
@Configuration
public class WechatpayConfig{
@Value("${wechat-pay.merchant-id}")
private String merchantId;
@Value("${wechat-pay.merchant-serial-number}")
private String merchantSerialNumber;
@Value("${wechat-pay.private-key}")
private String privateKey;
@Value("${wechat-pay.api-v3-key}")
private String apiV3Key;
/**
* 给容器中加入WechatPay的HttpClient,虽然它是WechatPay的,
* 但可以用它给任何外部发请求,因为它只对发给WechatPay的请求做处理而不对发给别的的请求做处理.
*/
@Bean
public HttpClient httpClient(){
//私钥
PrivateKey merchantPrivateKey=PemUtil.loadPrivateKey(privateKey);
//微信证书校验器
Verifier verifier=null;
try{
//获取证书管理器实例
CertificatesManager certificatesManager=CertificatesManager.getInstance();
//向证书管理器增加需要自动更新平台证书的商户信息(默认时间间隔:24小时)
certificatesManager.putMerchant(merchantId,new WechatPay2Credentials(merchantId,new PrivateKeySigner(merchantSerialNumber,merchantPrivateKey)),apiV3Key.getBytes(StandardCharsets.UTF_8));
//从证书管理器中获取verifier
verifier=certificatesManager.getVerifier(merchantId);
}
catch(Exception e){
new RuntimeException("微信证书校验器配置失败");
}
WechatPayHttpClientBuilder builder=WechatPayHttpClientBuilder.create()
.withMerchant(merchantId,merchantSerialNumber,merchantPrivateKey)
.withValidator(new WechatPay2Validator(verifier));
CloseableHttpClient httpClient=builder.build();
return httpClient;
}
/**
* 和上面相比只是不需要验证签名了
* @return
*/
@Bean
public HttpClient httpClientWithNoSign(){
//私钥
PrivateKey merchantPrivateKey=PemUtil.loadPrivateKey(privateKey);
//微信证书校验器
Verifier verifier=null;
try{
//获取证书管理器实例
CertificatesManager certificatesManager=CertificatesManager.getInstance();
//向证书管理器增加需要自动更新平台证书的商户信息(默认时间间隔:24小时)
certificatesManager.putMerchant(merchantId,new WechatPay2Credentials(merchantId,new PrivateKeySigner(merchantSerialNumber,merchantPrivateKey)),apiV3Key.getBytes(StandardCharsets.UTF_8));
//从证书管理器中获取verifier
verifier=certificatesManager.getVerifier(merchantId);
}
catch(Exception e){
new RuntimeException("微信证书校验器配置失败");
}
WechatPayHttpClientBuilder builder=WechatPayHttpClientBuilder.create()
.withMerchant(merchantId,merchantSerialNumber,merchantPrivateKey)
.withValidator(response->true);
CloseableHttpClient httpClient=builder.build();
return httpClient;
}
}
3.写配置
wechat-pay:
#接下来两个用来标识用户
#商户id
merchant-id: xxxxxxxxxxx
#公众号appid(和商户id绑定过)
appid: xxxxxxxxxxx
#接下来两个用来确保SSL(内容未作任何加密,只做了签名.)
#商户证书序列号
merchant-serial-number: xxxxxxxxxxx
#商户私钥
private-key: xxxxxxxxxxx
#APIv3密钥(在微信支付回调通知和商户获取平台证书使用APIv3密钥)
api-v3-key: xxxxxxxxxxx
#接下来两个是相关地址
#微信服务器地址
domain: https://api.mch.weixin.qq.com
#接收结果通知地址
notify-domain: xxxxxxxxxxx
4.使用
流程图
以下涉及的共有的内容
public class WechatPayConstant{
public static final String CANCEL_PAY_URL="/v3/pay/transactions/out-trade-no/%s/close";
public static final String CREATE_PAY_URL="/v3/pay/transactions/native";
public static final String QUERY_PAY_URL="/v3/pay/transactions/out-trade-no/%s?mchid=%s";
public static final String CREATE_REFUND_URL="/v3/refund/domestic/refunds";
public static final String QUERY_REFUND_URL="/v3/refund/domestic/refunds/%s";
public static final String TRADE_BILL_URL="/v3/bill/tradebill?bill_date=%s&bill_type=%s";
public static final String FLOW_BILL_URL="/v3/bill/fundflowbill?bill_date=%s";
public static final String TRADE_STATE_SUCCESS="SUCCESS";
public static final String REFUND_STATE_SUCCESS="SUCCESS";
}
@Value("${wechat-pay.merchant-id}")
private String merchantId;
@Value("${wechat-pay.merchant-serial-number}")
private String merchantSerialNumber;
@Value("${wechat-pay.api-v3-key}")
private String apiV3Key;
@Value("${wechat-pay.domain}")
private String domain;
@Value("${wechat-pay.appid}")
private String appId;
@Value("${wechat-pay.notify-url}")
private String notifyUrl;
@Autowired
private HttpClient httpClient;
@Autowired
private HttpClient httpClientWithNoSign;
支付
Native支付流程图
创建支付
https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_4_1.shtml
private String createPay(OrderInfo orderInfo) throws Exception{
//请求构造
HttpPost httpPost=new HttpPost(domain+WechatPayConstant.CREATE_PAY_URL);
//请求体
//构造数据
HashMap<String,Object> reqData=new HashMap<>();
reqData.put("appid",appId);
reqData.put("mchid",merchantId);
reqData.put("description",orderInfo.getTitle());
reqData.put("out_trade_no",orderInfo.getOrderNo());
reqData.put("notify_url",notifyUrl+"/pay/order/order-signal");
HashMap<String,Integer> amount=new HashMap<>();
//单位是分
amount.put("total",orderInfo.getTotalFee());
reqData.put("amount",amount);
String jsonReqData=new Gson().toJson(reqData);
StringEntity entity=new StringEntity(jsonReqData,"utf-8");
entity.setContentType("application/json");
httpPost.setEntity(entity);
//请求头
httpPost.setHeader("Accept","application/json");
//完成签名并执行请求
CloseableHttpResponse response=(CloseableHttpResponse)httpClient.execute(httpPost);
Map<String,String> dataMap=null;
try{
int statusCode=response.getStatusLine()
.getStatusCode();
//成功
if(statusCode==200){
String body=EntityUtils.toString(response.getEntity());
dataMap=new Gson().fromJson(body,HashMap.class);
}
//失败
else{
if(statusCode!=204){
String body=EntityUtils.toString(response.getEntity());
log.error(body);
return null;
}
}
}
finally{
response.close();
}
//返回二维码的地址
return dataMap.get("code_url");
}
支付通知
https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_4_5.shtml
@PostMapping("/pay-signal")
public HashMap<String,String> paySignal(@RequestBody Map<String,Object> signalRes,HttpServletResponse response){
log.debug("收到微信回调");
try{
//TODO:验签
//用密文解密出明文
Map<String,String> resource=(Map<String,String>)signalRes.get("resource");
String ciphertext=resource.get("ciphertext");
String associatedData=resource.get("associated_data");
String nonce=resource.get("nonce");
String plainText=new AesUtil(apiV3Key.getBytes(StandardCharsets.UTF_8)).decryptToString(associatedData.getBytes(StandardCharsets.UTF_8),nonce.getBytes(StandardCharsets.UTF_8),ciphertext);
//转换
HashMap<String,Object> data=new Gson().fromJson(plainText,HashMap.class);
//从数据库中查出对应的订单
QueryWrapper<OrderInfo> queryWrapper=new QueryWrapper<>();
queryWrapper.eq("order_no",data.get("out_trade_no"));
OrderInfo orderInfo=orderInfoService.getOne(queryWrapper);
synchronized(this){
if(orderInfo.getOrderStatus()
.equals(OrderInfo.NOT_PAIED)){
//将订单设置为已支付状态
orderInfo.setOrderStatus(OrderInfo.PAIED);
//更新订单状态
orderInfoService.updateById(orderInfo);
//添加支付记录
PaymentInfo paymentInfo=new PaymentInfo();
paymentInfo.setOrderNo(orderInfo.getOrderNo());
paymentInfo.setCreateTime(new Date());
paymentInfo.setUpdateTime(new Date());
paymentInfo.setPayerTotal(orderInfo.getTotalFee());
paymentInfo.setPaymentType(PaymentInfo.WECHAT_PAY);
//微信支付中的支付编号
paymentInfo.setTransactionId((String)data.get("transaction_id"));
//交易类型(扫码 刷脸等等)
paymentInfo.setTradeType((String)data.get("trade_type"));
paymentInfo.setTradeState((String)data.get("trade_state"));
//存放全部数据(json)以备不时之需
paymentInfo.setContent(plainText);
paymentInfoService.save(paymentInfo);
log.info("订单{}的支付记录添加成功,支付记录id为{}.",orderInfo.getOrderNo(),paymentInfo.getId());
}
else{
log.debug("订单{}状态为{},回调处理退出.",orderInfo.getOrderNo(),OrderInfo.PAIED);
}
}
return null;
}
catch(Exception e){
log.error(e.getMessage());
response.setStatus(HttpStatus.SC_INTERNAL_SERVER_ERROR);
HashMap<String,String> map=new HashMap<>();
map.put("code","FAIL");
map.put("message","支付失败");
return map;
}
}
查询支付
https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_4_2.shtml
private Map<String,Object> queryPay(String orderNo) throws Exception{
//请求构造
HttpGet httpGet=new HttpGet(String.format(domain+WechatPayConstant.QUERY_PAY_URL,orderNo,merchantId));
//请求头
httpGet.setHeader("Accept","application/json");
//完成签名并执行请求
CloseableHttpResponse response=(CloseableHttpResponse)httpClient.execute(httpGet);
Map<String,Object> dataMap=null;
try{
int statusCode=response.getStatusLine()
.getStatusCode();
String body=EntityUtils.toString(response.getEntity());
//成功
if(statusCode<400){
dataMap=new Gson().fromJson(body,HashMap.class);
log.info("查询订单支付{}成功",orderNo);
}
//失败
else{
log.error("查询订单支付{}失败,返回数据为{}.",orderNo,body);
throw new IOException("订单查询失败");
}
}
finally{
response.close();
}
return dataMap;
}
取消支付
https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_4_3.shtml
private boolean cancelPay(String orderNo){
//请求构造
HttpPost httpPost=new HttpPost(String.format(domain+WechatPayConstant.CANCEL_PAY_URL,orderNo));
//请求体
//构造数据
HashMap<String,Object> reqData=new HashMap<>();
reqData.put("mchid",merchantId);
String jsonReqData=new Gson().toJson(reqData);
StringEntity entity=new StringEntity(jsonReqData,"utf-8");
entity.setContentType("application/json");
httpPost.setEntity(entity);
//请求头
httpPost.setHeader("Accept","application/json");
CloseableHttpResponse response=null;
try{
//完成签名并执行请求
response=(CloseableHttpResponse)httpClient.execute(httpPost);
Map<String,String> dataMap=null;
int statusCode=response.getStatusLine()
.getStatusCode();
//成功
if(statusCode==204){
log.info("取消订单{}成功",orderNo);
return true;
}
//失败
else{
log.error("取消订单{}失败",orderNo);
return false;
}
}
catch(Exception e){
log.error("取消订单{}失败",orderNo);
return false;
}
finally{
try{
response.close();
}
catch(IOException e){
log.error("关闭response失败");
}
}
}
退款
创建退款
https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_4_10.shtml
private String createRefund(RefundInfo refundInfo) throws Exception{
//请求构造
HttpPost httpPost=new HttpPost(domain+WechatPayConstant.CREATE_REFUND_URL);
//请求体
//构造数据
HashMap<String,Object> reqData=new HashMap<>();
reqData.put("out_trade_no",refundInfo.getOrderNo());//订单编号
reqData.put("out_refund_no",refundInfo.getRefundNo());//退款单编号
reqData.put("reason",refundInfo.getReason());//退款原因
reqData.put("notify_url",notifyUrl+"/pay/order/refund-signal");//退款通知地址
HashMap<String,Object> amount=new HashMap<>();
amount.put("refund",refundInfo.getRefund());//退款金额
amount.put("total",refundInfo.getTotalFee());//原订单金额
amount.put("currency","CNY");//币种
reqData.put("amount",amount);
//将参数转换成json字符串
String jsonData=new Gson().toJson(reqData);
log.info("请求参数 ===> {}"+jsonData);
StringEntity entity=new StringEntity(jsonData,"utf-8");
httpPost.setEntity(entity);//将请求报文放入请求对象
//请求头
httpPost.setHeader("content-type","application/json");
httpPost.setHeader("Accept","application/json");//设置响应报文格式
//完成签名并执行请求
CloseableHttpResponse response=(CloseableHttpResponse)httpClient.execute(httpPost);
try{
//解析响应结果
String bodyAsString=EntityUtils.toString(response.getEntity());
int statusCode=response.getStatusLine()
.getStatusCode();
if(statusCode==200){
log.info("成功, 退款返回结果 = "+bodyAsString);
return bodyAsString;
}
else{
if(statusCode!=204){
log.warn("退款异常:"+bodyAsString);
}
return null;
}
}
finally{
response.close();
}
}
退款通知
https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_4_11.shtml
@PostMapping("/refund-signal")
public HashMap<String,String> refundSignal(@RequestBody Map<String,Object> signalRes,HttpServletResponse response){
log.debug("收到微信回调");
try{
Map<String,String> resource=(Map<String,String>)signalRes.get("resource");
String ciphertext=resource.get("ciphertext");
String associatedData=resource.get("associated_data");
String nonce=resource.get("nonce");
//解密出明文
String plainText=new AesUtil(apiV3Key.getBytes(StandardCharsets.UTF_8)).decryptToString(associatedData.getBytes(StandardCharsets.UTF_8),nonce.getBytes(StandardCharsets.UTF_8),ciphertext);
//转换
HashMap<String,Object> data=new Gson().fromJson(plainText,HashMap.class);
//从数据库中查出对应的退款信息
QueryWrapper<RefundInfo> queryWrapper=new QueryWrapper<>();
queryWrapper.eq("refund_no",data.get("out_refund_no"));
RefundInfo refundInfo=refundInfoService.getOne(queryWrapper);
synchronized(this){
if(RefundInfo.REFUND_PROCESSING.equals(refundInfo.getRefundStatus())){
//将退款状态设置为退款成功
refundInfo.setRefundStatus(RefundInfo.REFUND_SUCCESS);
//存放全部数据(json)以备不时之需
refundInfo.setContentNotify(plainText);
//更新退款
refundInfoService.updateById(refundInfo);
//更新订单的状态
String orderNo=refundInfo.getOrderNo();
QueryWrapper<OrderInfo> orderInfoQueryWrapper=new QueryWrapper<>();
orderInfoQueryWrapper.eq("order_no",orderNo);
OrderInfo orderInfoToUpdate=new OrderInfo();
orderInfoToUpdate.setOrderStatus(OrderInfo.REFUNDED);
orderInfoService.update(orderInfoToUpdate,orderInfoQueryWrapper);
log.debug("退款成功,退款单为{},对应的订单为{}.",refundInfo.getRefundNo(),refundInfo.getOrderNo());
}
else{
log.debug("退款单{}状态为{},回调处理退出.",refundInfo.getRefundNo(),refundInfo.getRefundStatus());
}
}
HashMap<String,String> map=new HashMap<>();
map.put("code","SUCCESS");
map.put("message","成功");
return map;
}
catch(Exception e){
log.error(e.getMessage());
response.setStatus(HttpStatus.SC_INTERNAL_SERVER_ERROR);
HashMap<String,String> map=new HashMap<>();
map.put("code","FAIL");
map.put("message","失败");
return map;
}
}
查询退款
https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_4_10.shtml
private Map<String,Object> queryRefund(String refundNo) throws Exception{
//请求构造
HttpGet httpGet=new HttpGet(String.format(domain+WechatPayConstant.QUERY_REFUND_URL,refundNo));
//请求头
httpGet.setHeader("Accept","application/json");
//完成签名并执行请求
CloseableHttpResponse response=(CloseableHttpResponse)httpClient.execute(httpGet);
Map<String,Object> dataMap=null;
try{
int statusCode=response.getStatusLine()
.getStatusCode();
String body=EntityUtils.toString(response.getEntity());
//成功
if(statusCode<400){
dataMap=new Gson().fromJson(body,HashMap.class);
log.info("查询退款{}成功",refundNo);
}
//失败
else{
log.warn("查询退款{}失败,返回数据为{}.",refundNo,body);
}
}
finally{
response.close();
}
return dataMap;
}
账单
查询交易账单(注重交易双方)下载URL
https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_4_6.shtml
/**
* @param billDate 格式:yyyy-MM-dd 5.6日的账单记录的时间为05-06 9:00到05-07 9:00,并且在05-07 9:00后才能查到.
* @param billType ALL(支付成功且未退款+支付成功且退款) SUCCESS(支付成功且未退款) REFUND(支付成功且退款)
* @return 账单下载url(30s后则失效)
*/
private String queryTradeBillDownloadUrl(String billDate,String billType)throws Exception{
//请求构造
HttpGet httpGet=new HttpGet(String.format(domain+WechatPayConstant.TRADE_BILL_URL,billDate,billType));
//请求头
httpGet.setHeader("Accept","application/json");
//完成签名并执行请求
CloseableHttpResponse response=(CloseableHttpResponse)httpClient.execute(httpGet);
String downloadUrl=null;
try{
int statusCode=response.getStatusLine()
.getStatusCode();
String body=EntityUtils.toString(response.getEntity());
//成功
if(statusCode<400){
downloadUrl=(String)new Gson().fromJson(body,HashMap.class)
.get("download_url");
return downloadUrl;
}
//失败
else{
log.warn("查询downloadUrl失败,返回数据为{}.",body);
return null;
}
}
catch(Exception e){
log.warn("查询downloadUrl失败");
return null;
}
finally{
response.close();
}
}
查询流水账单(只注重商户)下载URL
https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_4_7.shtml
/**
* @param billDate 格式:yyyy-MM-dd 5.6日的账单记录的时间为05-06 9:00到05-07 9:00,并且在05-07 9:00后才能查到.
* @return 账单下载url(30s后则失效)
*/
private String queryFlowBillDownloadUrl(String billDate) throws Exception{
//请求构造
HttpGet httpGet=new HttpGet(String.format(domain+WechatPayConstant.FLOW_BILL_URL,billDate));
//请求头
httpGet.setHeader("Accept","application/json");
//完成签名并执行请求
CloseableHttpResponse response=(CloseableHttpResponse)httpClient.execute(httpGet);
String downloadUrl=null;
try{
int statusCode=response.getStatusLine()
.getStatusCode();
String body=EntityUtils.toString(response.getEntity());
//成功
if(statusCode<400){
downloadUrl=(String)new Gson().fromJson(body,HashMap.class)
.get("download_url");
return downloadUrl;
}
//失败
else{
log.warn("查询downloadUrl失败,返回数据为{}.",body);
return null;
}
}
catch(Exception e){
log.warn("查询downloadUrl失败");
return null;
}
finally{
response.close();
}
}
获取账单(包括交易/流水)数据
https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_4_8.shtml
/**
* 调用微信支付的接口只返回数据,然后我们把数据返回给前端,前端将其转化为excel.
* @param downloadUrl 交易/流水账单的下载URL(即上面两个方法的返回值)
*/
private String downloadBill(String downloadUrl)throws Exception{
//请求构造
HttpGet httpGet=new HttpGet(downloadUrl);
//请求头
httpGet.addHeader("Accept","application/json");
//完成签名并执行请求(注意:这里必须用httpClientWithNoSign,因为这个请求返回的数据是没有签名的.)
CloseableHttpResponse response=(CloseableHttpResponse)httpClientWithNoSign.execute(httpGet);
try{
String bodyAsString=EntityUtils.toString(response.getEntity());
int statusCode=response.getStatusLine()
.getStatusCode();
if(statusCode<400){
log.debug("下载账单成功");
//存放数据
return bodyAsString;
}
else{
log.warn("下载账单失败,返回结果为:{}",bodyAsString);
return null;
}
}
catch(Exception e){
log.warn("下载账单失败");
return null;
}
finally{
response.close();
}
}
完整下载账单的操作
@GetMapping("/download-trade-bill")
public AjaxResult downloadTradeBill(String billDate,@RequestParam(defaultValue="ALL") String billType)throws Exception{
//获取downloadUrl
String downloadUrl=queryTradeBillDownloadUrl(billDate,billType);
log.debug("downloadUrl:{}",downloadUrl);
//访问downloadUrl获取数据
String returnData=downloadBill(downloadUrl);
HashMap<String,String> resultMap=new HashMap<>();
resultMap.put("result",returnData);
return AjaxResult.success(AjaxResult.QUERY_SUCCESS,resultMap);
}
@GetMapping("/download-flow-bill")
public AjaxResult downloadFlowBill(String billDate) throws Exception{
//获取downloadUrl
String downloadUrl=queryFlowBillDownloadUrl(billDate);
log.debug("downloadUrl:{}",downloadUrl);
//访问downloadUrl获取数据
String returnData=downloadBill(downloadUrl);
HashMap<String,String> resultMap=new HashMap<>();
resultMap.put("result",returnData);
return AjaxResult.success(AjaxResult.QUERY_SUCCESS,resultMap);
}
微信支付安全-尚硅谷
对称加密和非对称加密
对称加密AES算法, 运算速度快, 只有一个秘钥
非对称RSA算法, 运算速度慢, 公钥和私钥
先用非对称加密传输秘钥, 然后用对称加密来进行信息传输
身份认证
公钥加密, 私钥解密的作用是加密信息
私钥加密, 公钥解密的作用是身份认证
数字签名
MD5和SHA1的抗碰撞性不太好, 比较常用的SHA2
在发送信件的过程总附着摘要, 收信人就可以观察完整性, 但是不具有机密性, 有中间人攻击的可能性.
所以发件人:
- 写信
- 得到摘要
- 私钥将摘要加密形成数字签名附在原文下面
收件人:
- 收到信
- 将信件原文用摘要算法得到信件摘要
- 用公钥解密签名得到摘要
- 对比两个摘要, 一致则说明是发件人发的, 并且没有篡改
==数字签名可以保证传递的信息没有被篡改, 并且得到信息传递者的真实身份==
数字证书
==公钥信任问题, 收件人的公钥是否是真实发件人的私钥对应的公钥, 解决办法就是数字证书==
Bob获得证书的过程:
字证书颁发过程一般为:用户首先产生自己的秘钥对,并将公共密钥及部分个人身份信息传送给认证中心CA。认证中心在核实身份后,将执行一些必要的步骤,以确信请求确实由用户发送而来。然后,认证中心将发给用户一个数字证书,该证书内包含用户的个人信息和他的公钥信息,同时还附有认证中心的签名信息。用户就可以使用自己的数字证书进行相关的各种活动。
Bob有了数字证书之后, 和Pat的通信过程(也就是此时获得的public key是真正的):
- 用户先用CA的public key(==大招是在系统里面的==)来解密签名获得企业未被篡改的public key
- 用认证过的public key和企业来进行通信
微信支付安全
微信支付下载下来的有平台证书和私钥是非对称加密所使用的
API秘钥是对称的那个秘钥,
微信支付注意事项-尚硅谷
处理支付结果重复通知
先检查对应业务数据的状态, 判断该通知是否已经处理, 对业务进行查询和处理之前, 需要使用数据锁来进行并发控制, 单体业务可以用 ReentrantLock
或者synchronized
来获取锁后进行查询和处理, 分布式的话使用redisson
.
前端轮询查询支付结果直至成功
用户主动取消订单
后端定时查询订单来获取异步通知结果
引入spring定时任务, @EnableScheduling
从第0秒开始每隔30秒执行1次,向支付平台查询创建超过5分钟,并且未支付的订单, 如果支付平台已支付, 则修改订单状态为已支付, 如果支付平台未支付, 则关闭订单
支付漏洞
整型溢出
小数点四舍五入
比如1.4个物品和1.5个物品
或者2.0019=2.00, 那么每一次可以多一点点钱
重复购买限购商品
签约漏洞: 给一个二维码然后用多个手机扫描, 然后每个手机扫描二维码后停留, 最终一起付款
无限首充: 新用户首充
支付漏洞-越权替别人支付
平台账号和游戏账号时分开的,
两个账号都有首充优惠, 然后其他账号在充值的时候替换A账号为游戏账号, 然后很多首充
并发漏洞
账户只有一个优惠券, 然后手动点击多次, 然后一起放行同一个请求, 这时候每一个订单都有一个优惠券, 大额优惠券危害很大.
返回值
因为支付结果的签名, 无法修改, 但是在提交的时候, 如果传入的是price的话, 可以修改这个参数, 但一般情况下是只传入商品id, 后端查询价格然后向支付平台提交支付请求.
支付逻辑漏洞45分钟开始
资损常见情况和应对-极客时间
网络异常
- 调支付平台的支付接口出现网络异常, 此时支付平台并不一定失败, 如果此时再支付就会重复支付, 应该做的方法是此时支付订单记录记录为处理中, 等待定时查询订单的处理。
查询和通知问题
支付接口一般分为交易接口, 主动查询接口, 异步通知接口.
查询和通知类接口出现的问题:
- 查询失败或者查询异常
查询订单出现状态码为失败,并不代表交易失败, 因为这是查询本身操作失败, 而不是交易失败。 - 查询频率过快
为了更快的得到支付结果, 所以重试的频率过快, 但是调起支付的过程中有网络异常, 此时等待15s后继续重试重新调起支付, 此时支付平台由于支付链路长, 无此订单, 如果重新调起支付的话, 就会多次支付订单. - 被查询接口幂等性问题
- 通知问题
上游或者下游重复通知问题, 或者上游或者下游重复通知, 并且前后两次通知不一致, 此时如果第一次通知和第二次通知不一样, 则以第一次为准并且人为预警
接口幂等性问题
只要做到幂等, 方可以进行重复提示, 通常用上游订单流水号做唯一索引做幂等
另外还可以在请求入口处用redis来做
状态同步
- 查询订单不存在, 需要单独设置响应码, 做特殊预警, 付款类的交易不可以设置为失败状态
- 对于不确定的状态, 就认为它是失败的
- redis, mq宕机后的重启的状态同步
直接引起重复提交问题
- 并发导致的重复支付
- 表单重复提交导致的重复支付(点击过快, 网速)
- 定时器重复执行, 定时器浪打浪-控制定时器的执行状态以及状态机有限状态转移
- 各种重试机制导致的重复机制, 比如mq, http等各种中间件的重复支付
前端防重:
后端防重: 数据库乐观锁+有限状态机+白名单状态
“我的支付总结-博客园”
基础概念
系统设计
常见问题⭐
订单重复提交问题及解决
支付中常见幂等设计
- 订单号添加唯一索引, 操作前先查询是否已经支付过
- 用redis, 将订单号没有时放在redis中, 操作成功后删除key, 有时直接拒绝2
支付系统的几种常见技术事故
支付场景下的微服务改造与性能优化
LAST
欢迎在评论区中进行批评指正,转载请注明来源,如涉及侵权,请联系作者删除。