需求:
如图,实现流程:选择一个课程,点击支付,然后弹出一个支付二维码,用户扫描二维码进行支付。
签名和验签的理解:
A发送一个请求给B,
A需要将请求的一些参数进行签名加密,
B接收到请求后,需要对签名进行验签,作用就是判断发过来的请求是否被篡改过。如果没篡改过,则可以正常使用该方法的参数。
签名生成流程
签名验证流程
这些支付参数是视频制作者提供的
这是个配置文件
如图:这个 wxpay.private-key-path=apiclient_key.pem 是商户私钥文件路径,可以通过这个路径获取私钥文件
在商户平台申请的API证书,有私钥和证书,证书里面封装了公钥
商户的私钥文件加载到应用程序当中的目的主要是为了做签名,用私钥将请求进行签名,然后发请求的信息发送给微信的服务器端,微信的服务器端就会根据发来的请求中的 商户API证书的序列号 这个参数找到对应的证书,然后再从这个证书中解密出我们的公钥,然后用这个公钥对这个发来的加密的请求进行验签。
(公钥就存放在加密了的证书中)
这就是一个请求发送和接收的过程,对应的是一个签名和验签的过程。
签名:就是将请求加密
验签:就是用来判断这个请求有没有被篡改过,验签通过就是没被篡改过
现在这个是商户私钥的路径,通过这个路径获取商户私钥文件
这个密钥是一个对称加密的密钥
在申请商户号的时候,同时申请的微信公众号,这个就是微信公众号的id
远程向这个地址发起调用
向微信发起请求
微信向商户端发起请求
每个人的地址都是不一样的,记得修改
创建一个配置文件来读取支付参数配置文件里面的数据
这个方法可以读取到配置文件的值。
也可以通过这种方式来获取配置文件中的属性值,这个是其他的代码,仅作记录
能成功拿到配置文件的里面的属性值
可以帮助我们生成自定义配置的元数据信息,让配置文件和Java代码之间的对应参数可以自动定位,方
便开发。
如图,在idea 中,没有把这个wxpay 看成是一个spring的配置文件,虽然不影响程序运行,但是却少了很多功能,比如自动定位,就是点击配置文件的属性名,能自动定位到 WxPayConfig 配置文件的对应字段去。
实现配置文件能够自动定位的操作
把商户私钥复制到项目根目录下:
https://pay.weixin.qq.com/wiki/doc/apiv3/wechatpay/wechatpay6_0.shtml
https://github.com/wechatpay-apiv3/wechatpay-apache-httpclient
我们可以使用官方提供的 SDK,帮助我们完成开发。实现了请求签名的生成和应答签名的验证
微信支付的SDK 依赖
com.github.wechatpay-apiv3
wechatpay-apache-httpclient
0.3.0
因为我们的私钥是存在一个文件的(apiclient_key.pem),所以用第一个示例方法
https://github.com/wechatpay-apiv3/wechatpay-apache-httpclient
在 PaymentDemoApplicationTests 测试类中添加如下方法,测试私钥对象是否能够获取出来。
(将前面的方法改成public的再进行测试)
在测试类里面测试
https://pay.weixin.qq.com/wiki/doc/apiv3_partner/wechatpay/wechatpay3_0.shtml
https://github.com/wechatpay-apiv3/wechatpay-apache-httpclient (定时更新平台证书功能)
平台证书:平台证书封装了微信的公钥,商户可以使用平台证书中的公钥进行验签。
签名验证器:帮助我们进行验签工作,我们单独将它定义出来,方便后面的开发。
下图是学习视频的访问该网址的代码,但是现在打开该网址的代码已经改变了。
https://github.com/wechatpay-apiv3/wechatpay-apache-httpclient (定时更新平台证书功能)
HttpClient 对象:是建立远程连接的基础,我们通过SDK创建这个对象
**HttpClient对象作用: **通过 WechatPayHttpClientBuilder 构造的 HttpClient,会自动的处理签名和验签,并进行证书自动更新。
package cn.ljh.paymentdemo.config;
import com.wechat.pay.contrib.apache.httpclient.WechatPayHttpClientBuilder;
import com.wechat.pay.contrib.apache.httpclient.auth.PrivateKeySigner;
import com.wechat.pay.contrib.apache.httpclient.auth.ScheduledUpdateCertificatesVerifier;
import com.wechat.pay.contrib.apache.httpclient.auth.WechatPay2Credentials;
import com.wechat.pay.contrib.apache.httpclient.auth.WechatPay2Validator;
import com.wechat.pay.contrib.apache.httpclient.util.PemUtil;
import lombok.Data;
import org.apache.http.impl.client.CloseableHttpClient;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.PropertySource;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.nio.charset.StandardCharsets;
import java.security.PrivateKey;
@Configuration
@PropertySource("classpath:wxpay.properties") //读取配置文件
@ConfigurationProperties(prefix = "wxpay") //读取wxpay节点
@Data //使用set方法将wxpay节点中的值填充到当前类的属性中
public class WxPayConfig
{
// 商户号
private String mchId;
// 商户API证书序列号
private String mchSerialNo;
// 商户私钥文件的路径
private String privateKeyPath;
// APIv3密钥
private String apiV3Key;
// APPID
private String appid;
// 微信服务器地址
private String domain;
// 接收结果通知地址
private String notifyDomain;
/**
* 获取商户的私钥文件
*
* @param fileName 私钥文件的路径
* @return PrivateKey
*/
public PrivateKey getPrivateKey(String fileName)
{
try
{
return PemUtil.loadPrivateKey(
new FileInputStream(fileName));
} catch (FileNotFoundException e)
{
throw new RuntimeException("私钥文件不存在", e);
}
}
/**
* 获取签名验证器
*
* @return 签名验证器
*/
@Bean
public ScheduledUpdateCertificatesVerifier getVerifer()
{
//获取商户私钥
PrivateKey privateKey = getPrivateKey(privateKeyPath);
//私钥签名对象
PrivateKeySigner privateKeySigner = new PrivateKeySigner(mchSerialNo, privateKey);
//身份认证对象
WechatPay2Credentials wechatPay2Credentials = new WechatPay2Credentials(mchId, privateKeySigner);
// 使用定时更新的签名验证器,不需要传入证书
ScheduledUpdateCertificatesVerifier verifier = new ScheduledUpdateCertificatesVerifier(
wechatPay2Credentials,
apiV3Key.getBytes(StandardCharsets.UTF_8));
return verifier;
}
/**
* 获取HttpClient 请求对象:是建立远程连接的基础,我们通过SDK创建这个对象
* 通过 WechatPayHttpClientBuilder 构造的 HttpClient,会自动的处理签名和验签,并进行证书自动更新
*
* @param verifier 签名验证器
* @return HttpClient 请求对象,会自动的处理签名和验签,并进行证书自动更新
*/
@Bean
public CloseableHttpClient getWxPayClient(ScheduledUpdateCertificatesVerifier verifier)
{
//获取商户私钥
PrivateKey privateKey = getPrivateKey(privateKeyPath);
//用于构造HttpClient
WechatPayHttpClientBuilder builder = WechatPayHttpClientBuilder.create()
.withMerchant(mchId, mchSerialNo, privateKey)
.withValidator(new WechatPay2Validator(verifier));
// ... 接下来,你仍然可以通过builder设置各种参数,来配置你的HttpClient
// 通过 WechatPayHttpClientBuilder 构造的 HttpClient,会自动的处理签名和验签,并进行证书自动更新
CloseableHttpClient httpClient = builder.build();
return httpClient;
}
}
https://pay.weixin.qq.com/docs/merchant/development/interface-rules/introduction.html
我们的项目中要实现以下所有API的功能。
https://pay.weixin.qq.com/wiki/doc/apiv3/open/pay/chapter2_7_3.shtml
Native支付API列表
https://pay.weixin.qq.com/wiki/doc/apiv3/wechatpay/wechatpay2_0.shtml
微信支付 APIv3 使用 JSON 作为消息体的数据交换格式。
如上图,因为使用JSON作为数据交互的格式,不再使用XML , 所以在项目里面添加这个json处理的依赖
为了开发方便,我们预先在项目中定义一些枚举。枚举中定义的内容包括接口地址,支付状态等信息。
就是我们去调用支付的一些 商户订单号查询订单、关闭订单、退款申请 的接口,这些都是微信支付平台提供的接口,因为会经常调用,所以把这些接口地址、状态等直接封装成枚举,方便调用。
将资料文件夹中的 util 目录复制到源码目录中,我们将会使用这些辅助工具简化项目的开发
如图,选择一个课程,点击支付,然后弹出一个支付二维码,用户扫描二维码进行支付。
完整流程
https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_4_4.shtml
生成订单–>调用统一下单的API,生成支付二维码–>生成预支付交易–>返回预支付交易链接
下单的接口说明
https://pay.weixin.qq.com/docs/merchant/apis/native-payment/direct-jsons/native-prepay.html
由官方指定的一些要求和示例
【服务端】Native下单 的官方示例代码
https://pay.weixin.qq.com/wiki/doc/apiv3/open/pay/chapter2_7_2.shtml
native下单的接口说明
https://pay.weixin.qq.com/docs/merchant/apis/native-payment/direct-jsons/native-prepay.html
创建订单,调用native支付接口
一些配置文件、枚举类就没列出来了,太多了
package cn.ljh.paymentdemo.controller;
import cn.ljh.paymentdemo.service.WxPayService;
import cn.ljh.paymentdemo.vo.R;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource;
import java.util.Map;
@CrossOrigin //跨域
@RestController
@RequestMapping("/api/wx-pay")
@Api(tags = "网站微信支付API") //swagger 注解
@Slf4j
public class WxPayController
{
@Resource
private WxPayService wxPayService;
//调用统一下单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);
}
}
package cn.ljh.paymentdemo.service;
import java.util.Map;
public interface WxPayService
{
//调用统一下单API,生成支付二维码的链接和订单号
Map<String, Object> nativePay(Long productId) throws Exception;
}
package cn.ljh.paymentdemo.service.impl;
import cn.ljh.paymentdemo.config.WxPayConfig;
import cn.ljh.paymentdemo.entity.OrderInfo;
import cn.ljh.paymentdemo.enums.OrderStatus;
import cn.ljh.paymentdemo.enums.wxpay.WxApiType;
import cn.ljh.paymentdemo.enums.wxpay.WxNotifyType;
import cn.ljh.paymentdemo.service.WxPayService;
import cn.ljh.paymentdemo.util.OrderNoUtils;
import com.google.gson.Gson;
import lombok.extern.slf4j.Slf4j;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.util.EntityUtils;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
//创建订单,调用 Native 支付接口
@Service
@Slf4j
public class WxPayServiceImpl implements WxPayService
{
@Resource
private WxPayConfig wxPayConfig;
/* 原本应该注 入WxPayConfig 这个类,然后调用 getWxPayClient() 方法获取 HttpClient请求对象
* 但是因为 getWxPayClient() 方法加了@Bean注解,交给了spring容器管理,所以项目启动的时候就会执行这个方法,
* 就会存在返回值为 CloseableHttpClient 类型的 HttpClient请求对象
* 所以这里可以直接注入这个 CloseableHttpClient 对象
*/
@Resource
private CloseableHttpClient wxPayClient;
/**
* 创建订单,调用 Native 支付接口
*
* @param productId 商品id
* @return code_url 和 订单号
* @throws Exception
*/
//调用统一下单API,生成支付二维码的链接和订单号
@Override
public Map<String, Object> nativePay(Long productId) throws Exception
{
log.info("生成订单.....");
//生成订单
OrderInfo orderInfo = new OrderInfo();
orderInfo.setTitle("test"); //订单标题
orderInfo.setOrderNo(OrderNoUtils.getOrderNo()); //生成订单号
orderInfo.setProductId(productId); //商品id
orderInfo.setTotalFee(1); //单位是:分
orderInfo.setOrderStatus(OrderStatus.NOTPAY.getType()); //支付状态
//TODO: 需要将这个订单存到数据库
/*
* 官方提供的 Native下单 接口
* 支持商户:【普通商户】
* 请求方式:【POST】/v3/pay/transactions/native
* 请求域名:【主域名】https://api.mch.weixin.qq.com
* "https://api.mch.weixin.qq.com/v3/pay/transactions/native" 改成
* wxPayConfig.getDomain().concat(WxApiType.NATIVE_PAY.getType())
*/
log.info("调用统一下单API.....");
//调用统一下单API---拷贝官网的实例代码进行修改---统一下单的接口地址
HttpPost httpPost = new HttpPost(wxPayConfig.getDomain().concat(WxApiType.NATIVE_PAY.getType()));
// 请求body参数---------调用接口需要的参数
Gson gson = new Gson();
//数据类型不固定,所以就不写泛型了
Map paramsMap = new HashMap();
//设置参数 --- 根据官网要求设置对应的参数
paramsMap.put("appid", wxPayConfig.getAppid()); //公众号ID
paramsMap.put("mchid", wxPayConfig.getMchId()); //直连商户号
paramsMap.put("description", orderInfo.getTitle()); // 商品描述
paramsMap.put("out_trade_no", orderInfo.getOrderNo()); //商户订单号
paramsMap.put("notify_url", wxPayConfig.getNotifyDomain().concat(WxNotifyType.NATIVE_NOTIFY.getType())); //通知地址
//订单金额有两个参数--嵌套数据
Map amountMap = new HashMap();
amountMap.put("total", orderInfo.getTotalFee()); //总金额
amountMap.put("currency", "CNY"); //货币类型
paramsMap.put("amount", amountMap); // 订单金额
//将参数转成字符串
String jsonParams = gson.toJson(paramsMap);
log.info("支付的请求参数:" + jsonParams);
//把参数设置到请求体当中
StringEntity entity = new StringEntity(jsonParams, "utf-8");
entity.setContentType("application/json");
httpPost.setEntity(entity);
//希望得到的响应类型
httpPost.setHeader("Accept", "application/json");
//完成签名并执行请求
CloseableHttpResponse response = wxPayClient.execute(httpPost);
//这些就是对调用下单方法的响应结果的处理了
try
{
//字符串形式的响应体
String bodyAsString = EntityUtils.toString(response.getEntity());
//响应状态码
int statusCode = response.getStatusLine().getStatusCode();
if (statusCode == 200)
{ //处理成功
System.out.println("成功, 返回结果 = " + bodyAsString);
} else if (statusCode == 204)
{ //处理成功,无返回Body
System.out.println("成功");
} else
{
System.out.println("下单失败, 响应码 = " + statusCode + ", 返回结果 = " + bodyAsString);
throw new IOException("请求失败 request failed");
}
//如果下单成功,获取响应结果, gson.fromJson()用于将 JSON 字符串转换为 Java 对象
Map<String, String> resultMap = gson.fromJson(bodyAsString, HashMap.class);
//从返回的结果中获取二维码的url, 从官网看出 code_url 是 二维码的key
String codeUrl = resultMap.get("code_url");
//创建一个url和订单号的返回值
Map<String, Object> map = new HashMap<>();
map.put("codeUrl", codeUrl);
map.put("orderNo", orderInfo.getOrderNo());
return map;
} finally
{
response.close();
}
}
}
1、前端调用后端接口流程
后端返回的数据,主要的商品列表数据存在属性名为 productList 中,
前端的 created(){} 是页面加载的时候自动执行,
productApi 这个类里面有一个 list() 方法,
list()方法有url,一看就是在执行远程调用,就是在调用后端的 url: ‘/api/product/list’ 接口
then 就是回调函数,在then 方法中取出后端的响应数据,response 就是后端响应到前端的数据
response.data.productList 就是获取后端返回来的属性名为 productList 的值
data 就是response响应中的结果数据,因为response 还有其他一些数据。
2、一些值赋予过程
this.productList 就是把获取到的值设置到 data 里面的 productList: []
遍历的值就是从data里面获取的
3、模块调用分析
基于上面继续分析:
import 引入模块分析,如图,也可以理解为在 index.vue 中调用 productApi 这个模块
然后product.js 调用 request.js ,
request.js 引入 axios
如图:在 product.js 中,url 是调用后端的接口的具体路径,但是明显还少了主机地址,
就是http://localhost:端口号这些主机地址。
所以需要引入 request 模块,在这个request 模块中就初始化了所有远程ajax调用需要的请求的主机地址
然后为什么能实现ajax远程调用呢?
是因为在request模块中,又引入了 axios 模块,这个模块就是在vue中专门做发送ajax 请求的模块。
axios 模块是从哪里来的呢?
axios 是在 package.json 里面引入的,相当于 java 在 pom.xml 文件中引入所需依赖一样。
因为在 package.json 引入了 “axios”: “^0.24.0” 这个专门发送ajax的工具,所以前端才能实现ajax 调用,
所以可以在 request.js 中创建 axios 实例。
都是点击触发点击事件
远程调用都是一样的,跟获取商品列表数据一样