微信支付
# 基础介绍
1、需要开通微信商户号、微信服务号实名才可以微信支付。
2、新版本的api以v3为主,老版本api为V2版本,申请商户API证书;
3、设置api秘钥(V2版本)、api V3秘钥;
4、获取微信平台证书,可以预先下载,也可以编程方式获取
5、使用的工具: 微信官方、wx-java、
<dependency>
<groupId>com.github.wechatpay-apiv3</groupId>
<artifactId>wechatpay-apache-httpclient</artifactId>
<version>0.2.2</version>
</dependency>
2
3
4
5
# 支付安全
# 信息安全的基础
区别:
1、对称加密中加密和解密使用的秘钥是同一个;非对称加密中采用两个密钥,一般使用公钥进行加密,私钥进行解密。
2、对称加密解密的速度比较快,非对称加密和解密花费的时间长、速度相对较慢。
3、对称加密的安全性相对较低,非对称加密的安全性较高。
参考文章:
浅谈对称加密与非对称加密 - 知乎 (zhihu.com) (opens new window)
# 身份认证
B有公钥和私钥,当A给B写信时,可以用B的公钥对信进行加密,B拿到信时,用私钥进行解密阅读。 其它人C给B写信时,也是一样的流程。如果反过来,B用自已的私钥给A写信加密, A可以用B的公钥解密, 其它人也可以用B的公钥解密,也就是说知道这信是B写的,就用B的公钥,即为身份认证。
公钥加密,私钥解密的作用是信息加密
私钥加密,公钥解密的作用是身份认证
# 数据摘要
B给A写信,想保证信的完整性,没有被篡改,实现完整性的就段主要就是摘要算法,也叫哈希算法
# 数字签名
B给A写信,先将信的内容生成一个摘要(理解成一个MD5码),附在信件里面,A收到信时,将接收到的信内容生成摘要,进行对比,如果没有修改,则说明信没有修改。但是如果信在传输中被修改了,同时也修改了附带的摘要信息,则A就无法辨别到底有没有修改。
数字签名的作法是: B先将信的内容生成一个摘要,然后用B的私钥给摘要进行加密生成签名,附在信件里面,A收到信后,将签名用B的公钥进行解密,得到摘要,然后将信的摘要值与解密后的值进行对比,如果一致,则说明数据没有被修改。 微信支付就是采用这一种方式
# 数字证书
C将自已的公钥给A,说这是B的公钥,C写信时用自已的私钥生成签名,发送信给A,A用C的公钥(A以为是B的公钥,实质上是C自已给的公钥),也可以验签,但A以为是跟B在通信,实际上是在跟C通信, 这就带来公钥的信任问题
# 微信支付中的证书和签名
# 商户号证书
商户API证书是指由商户申请的,包含商户的商户号、公司名称、公钥信息的证书。 商户证书在商户后台申请:https://pay.weixin.qq.com/index.php/core/cert/api_cert#/
apiclient_cert.pem
为公钥 apiclient_key.pem
为私钥
# 微信公众号平台证书
微信支付平台证书是指由微信支付 负责申请的,包含微信支付平台标识、公钥信息的证书。商户可以使 用平台证书中的公钥进行验签。 平台证书的获取:https://pay.weixin.qq.com/wiki/doc/apiv3/wechatpay/wechatpay3_0.shtml
# API密钥和APIv3密钥
都是对称加密需要使用的加密和解密密钥,一定要保管好,不能泄露。
API密钥
对应V2版本的API ; APIv3
密钥对应V3版本的API
# 搭建支付测试环境
参考官方文档:
api的调用地址加上
# 工具
如果开发者使用过Postman
API的调试,官方推荐在正式开发之前,使用Postman签名脚本 (opens new window) 进行接口体验
另外,官方提供的微信支付平台证书下载工具 (opens new window),可以协助开发者完成证书下载,它也是个很好的Java示例程序。
git clone https://github.com.cnpmjs.org/wechatpay-apiv3/wechatpay-postman-script.git
# 接入JSAPI微信支付
微信支付-开发者文档 (qq.com) (opens new window)
# APIv3证书与密钥使用说明
- 私钥可以从商户后台下载的文件里面读取到
//获取商户私钥,传入apiclient_key.pem路径
PrivateKey privateKey = PemUtil.loadPrivateKey(new FileInputStream(privateKeyPath));
//私钥签名对象, 传入证书序列号和私钥
PrivateKeySigner privateKeySigner = new PrivateKeySigner(mchSerialNo, privateKey);
//身份认证对象
WechatPay2Credentials wechatPay2Credentials = new WechatPay2Credentials(mchId, privateKeySigner);
// 使用定时更新的签名验证器,不需要传入证书
ScheduledUpdateCertificatesVerifier verifier = new ScheduledUpdateCertificatesVerifier(
wechatPay2Credentials,
apiV3Key.getBytes(StandardCharsets.UTF_8));
// 获取httpClient
WechatPayHttpClientBuilder builder = WechatPayHttpClientBuilder.create()
.withMerchant(merchantId, merchantSerialNumber, merchantPrivateKey)
.withValidator(new WechatPay2Validator(verifier))
// ... 接下来,你仍然可以通过builder设置各种参数,来配置你的HttpClient
// 通过WechatPayHttpClientBuilder构造的HttpClient,会自动的处理签名和验签,并进行证书自动更新
CloseableHttpClient httpClient = builder.build();
// 后面跟使用Apache HttpClient一样
CloseableHttpResponse response = httpClient.execute(...);
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
# 支付时序图:
重点步骤说明:
步骤3 用户下单发起支付,商户可通过JSAPI下单 (opens new window)创建支付订单。
步骤8 商户可在微信浏览器内通过JSAPI调起支付API (opens new window)调起微信支付,发起支付请求。
步骤15 用户支付成功后,商户可接收到微信支付支付结果通知支付结果通知API (opens new window)。
步骤20 商户在没有接收到微信支付结果通知的情况下需要主动调用查询订单API (opens new window)查询支付结果。
参考: 微信支付-开发者文档 (qq.com) (opens new window)
# 签名生成 (opens new window)
准备:私钥和商户证书
源码见: com.wechat.pay.contrib.apache.httpclient.auth.WechatPay2Credentials
# 签名验证 (opens new window)
# 第一步:从微信支付平台下载证书公钥
微信支付API v3使用微信支付的平台私钥(不是商户私钥 )进行应答签名。相应的,商户的技术人员应使用微信支付平台证书中的公钥验签。目前平台证书只提供API进行下载
源码见:com.wechat.pay.contrib.apache.httpclient.auth.ScheduledUpdateCertificatesVerifier
CertManagerSingleton
// 从微信支付平台下载证书公钥
public synchronized void init(Credentials credentials, byte[] apiV3Key, long minutesInterval) {
if (credentials == null || apiV3Key.length == 0 || minutesInterval == 0) {
throw new IllegalArgumentException("credentials或apiV3Key或minutesInterval为空");
}
if (this.credentials == null || this.apiV3Key.length == 0 || this.executor == null
|| this.certificates == null) {
this.credentials = credentials;
this.apiV3Key = apiV3Key;
this.executor = new SafeSingleScheduleExecutor();
this.certificates = new ConcurrentHashMap<>();
// 初始化证书
initCertificates();
// 启动定时更新证书任务
Runnable runnable = () -> {
try {
Thread.currentThread().setName(SCHEDULE_UPDATE_CERT_THREAD_NAME);
log.info("Begin update Certificate.Date:{}", Instant.now());
updateCertificates();
log.info("Finish update Certificate.Date:{}", Instant.now());
} catch (Throwable t) {
log.error("Update Certificate failed", t);
}
};
executor.scheduleAtFixedRate(runnable, 0, minutesInterval, TimeUnit.MINUTES);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
# 第二步:用微信平台公钥对微信发送过来的签名进行验证
见源码:com.wechat.pay.contrib.apache.httpclient.auth.WechatPay2Validator
ScheduledUpdateCertificatesVerifier
# 1、封装支付的请求对象和返回对象
A、请求对象: WxPayUnifiedOrderV3Request
B、返回的对象:com.github.binarywang.wxpay.bean.result.WxPayUnifiedOrderV3Result
C、创建httpClient对象
<!-- 微信支付SDK -->
<dependency>
<groupId>com.github.wechatpay-apiv3</groupId>
<artifactId>wechatpay-apache-httpclient</artifactId>
<version>0.3.0</version>
</dependency>
<dependency>
<groupId>com.github.wxpay</groupId>
<artifactId>wxpay-sdk</artifactId>
<version>0.0.3</version>
</dependency>
2
3
4
5
6
7
8
9
10
11
@Before
public void setup() throws IOException {
// 加载商户私钥(privateKey:私钥字符串)
PrivateKey merchantPrivateKey = PemUtil
.loadPrivateKey(new ByteArrayInputStream(privateKey.getBytes("utf-8")));
// 方式二: 加载apiclient_key.pem证书文件路径方式也可以
PrivateKey merchantPrivateKey = PemUtil.loadPrivateKey(new FileInputStream(privateKeyPath));
// 加载平台证书(mchId:商户号,mchSerialNo:商户证书序列号,apiV3Key:V3密钥)
AutoUpdateCertificatesVerifier verifier = new AutoUpdateCertificatesVerifier(
new WechatPay2Credentials(mchId, new PrivateKeySigner(mchSerialNo, merchantPrivateKey)),apiV3Key.getBytes("utf-8"));
// 初始化httpClient
httpClient = WechatPayHttpClientBuilder.create()
.withMerchant(mchId, mchSerialNo, merchantPrivateKey)
.withValidator(new WechatPay2Validator(verifier)).build();
}
@After
public void after() throws IOException {
httpClient.close();
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# 2、必须发送的数据
{
"mchid": "1900006XXX",
"appid": "wxdace645e0bc2cXXX",
"out_trade_no": "1217752501201407033233368318", // 商户订单号
"description": "Image形象店-深圳腾大-QQ公仔", //
"notify_url": "https://weixin.qq.com/", //通知地址
"amount": {
"total": 1, // 单位分
"currency": "CNY"
},
"payer": {
"openid": "o4GgauInH_RCEdvrrNGrntXDuXXX"
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
# 3、建立httpclient
public class WxMchContent {
// 远程连接
public static Map<String, CloseableHttpClient> httpClients = new ConcurrentHashMap<>();
public static Map<String, CloseableHttpClient> wxPayNoSignClients = new ConcurrentHashMap<>();
public static Map<String, ScheduledUpdateCertificatesVerifier> verifiers = new ConcurrentHashMap<>();
}
2
3
4
5
6
7
8
@Component
@Order(2)
@Slf4j
@AllArgsConstructor
public class WxMchInit implements CommandLineRunner {
private final WxMchService wxMchService;
/**
* 获取商户的私钥文件
* @param filename
* @return
*/
private PrivateKey getPrivateKey(String filename){
// getClassLoader加载不需要/,class.getResource()是以resources为根目录的绝对路径,文件名(参数)前面需要加/
InputStream is = Thread.currentThread().getContextClassLoader().getResourceAsStream(filename);
// new FileInputStream( filename )
try {
if (is == null) { //不是放在resource目录下,就从文件路径里面拿
is = new FileInputStream(filename);
}
return PemUtil.loadPrivateKey(is);
} catch (Exception e) {
throw new RuntimeException("私钥文件不存在", e);
}
}
@Override
public void run(String... args) throws Exception {
log.debug("启动:"+args);
List<WxMch> list = wxMchService.list();
log.debug("获取所有的商户号:"+list);
if (!ObjectUtils.isEmpty(list)){
for (WxMch wxMch: list) {
String mchId = wxMch.getMchId();
String mchSerialNo = wxMch.getMchSerialNo();
//获取商户私钥
PrivateKey privateKey = getPrivateKey( wxMch.getPrivateKeyPath());
//用于构造HttpClient
WechatPayHttpClientBuilder builder = WechatPayHttpClientBuilder.create()
//设置商户信息
.withMerchant(mchId, mchSerialNo, privateKey)
//无需进行签名验证、通过withValidator((response) -> true)实现
.withValidator((response) -> true);
// 通过WechatPayHttpClientBuilder构造的HttpClient,会自动的处理签名和验签,并进行证书自动更新
log.info("== getWxPayNoSignClient END ==");
WxMchContent.httpClients.put(mchId, builder.build());
log.info("获取签名验证器");
//私钥签名对象
PrivateKeySigner privateKeySigner = new PrivateKeySigner(mchSerialNo, privateKey);
//身份认证对象
WechatPay2Credentials wechatPay2Credentials = new WechatPay2Credentials(mchId, privateKeySigner);
// 使用定时更新的签名验证器,不需要传入证书
ScheduledUpdateCertificatesVerifier verifier = new ScheduledUpdateCertificatesVerifier(
wechatPay2Credentials,
wxMch.getApiV3Key().getBytes(StandardCharsets.UTF_8));
WxMchContent.verifiers.put(wxMch.getMchId(), verifier);
//用于构造HttpClient
WechatPayHttpClientBuilder builder2 = WechatPayHttpClientBuilder.create()
//设置商户信息
.withMerchant(mchId, mchSerialNo, privateKey)
//无需进行签名验证、通过withValidator((response) -> true)实现
.withValidator((response) -> true);
// 通过WechatPayHttpClientBuilder构造的HttpClient,会自动的处理签名和验签,并进行证书自动更新
log.info("== getWxPayNoSignClient END ==");
WxMchContent.wxPayNoSignClients.put(wxMch.getMchId(), builder2.build());
}
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
# 4、如何加载商户私钥
商户申请商户API证书时,会生成商户私钥,并保存在本地证书文件夹的文件apiclient_key.pem
中。商户开发者可以使用方法PemUtil.loadPrivateKey()
加载证书。
# 示例:私钥存储在文件
PrivateKey merchantPrivateKey = PemUtil.loadPrivateKey(
new FileInputStream("F:\\svn\\document\\cert\\1613426068\\apiclient_key.pem"));
/**
* 获取商户的私钥文件
* @param filename
* @return
*/
private PrivateKey getPrivateKey(String filename){
// getClassLoader加载不需要/,class.getResource()是以resources为根目录的绝对路径,文件名(参数)前面需要加/
InputStream is = Thread.currentThread().getContextClassLoader().getResourceAsStream(filename);
// new FileInputStream( filename )
try {
if (is == null) { //不是放在resource目录下,
is = new FileInputStream(filename);
}
return PemUtil.loadPrivateKey(is);
} catch (Exception e) {
throw new RuntimeException("私钥文件不存在", e);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# 5、防重复通知、通知幂等性
private final ReentrantLock lock = new ReentrantLock();
//尝试获取锁: 成功获取则立即返回true,获取失败则立即返回false。不必一直等待锁的释放
if(lock.tryLock()){
try {
//处理重复的通知,接口调用的幂等性:无论接口被调用多少次,产生的结果是一致的。
...
} finally {
//要主动释放锁
lock.unlock();
}
}
2
3
4
5
6
7
8
9
10
11
12
# 内网穿透
ngrok - secure introspectable tunnels to localhost (opens new window) 注册账号,下载ngrok.exe,进行安装
ngrok authtoken 3BnWXVdyuvgLVZiywkhqc_7Re6E3Sciu4LAfa9jTjqW //C:\Users\Administrator/.ngrok2/ngrok.yml
ngrok http 19080 // 启动
2
缺点:
每次关闭ngrok后,生成的地址会更换,可以使用花生壳软件的内网穿透 http://4210ri2640.zicp.vip/
http://421l02640d.hsk.top
# 现金红包
单日发放红包总金额不超过 5000.00 元
单用户单日领取个数: 每日同一用户领取本商户红包个数不超过 10 个
每日同一用户领取本商户红包金额不超过 1000.00 元
【微信支付】现金红包开发者文档 (qq.com) (opens new window)
# 企业付款到零钱、批量付款到零钱
默认同一用户单日收款额度200元/天,单笔付款区间200元/笔,要提高金额,需要申请
免费:不收取付款手续费,节省企业成本。
灵活:可通过页面或接口发起付款,灵活满足企业不同场景的付款需求。
友好:通过openid即可实现付款,用户授权即可,体验更好。
快速:在发起后,及时到账用户零钱。通过微信消息触达,用户及时获知入账详情。
安全:提供多种安全工具,满足不同场景安全需求。如:按需调整付款额度;支持收款账户限制;支持安全防刷,拦截恶意用户、小号、机器号码。
支持自定义大额通知等。
# 企业付款到银行卡
目前支持17家银行,更多银行逐步开放;
目前支持付款至对私银行储蓄卡账户;
发起方式灵活,可通过页面或接口发起
T+1工作日入账(受部分银行影响可能会更久)
通过指定收款户名、卡号银行信息实现付款
每笔会收取一定金额的手续费,按单笔金额收取,T+1到账,每笔收取0.1%,最低1元,最高25元
商户号已入驻90日且截止今日回推30天商户号保持连续不间断的交易。
单笔付款最低金额 0.01 元
单笔付款最高金额 20,000.00 元
用户维度日付款总额 20,000.00 元
商户维度日付款总额 100,000.00 元
每日向同一用户付款 不允许超过 3 次
# 常见支付方式
# 1、JSAPI支付
JSAPI支付授权目录、 Native支付回调链接、H5支付域名(需要备案)
2、
# 分析ijpay-springboot demo
技术点: enjoy模板、alibaba fastjson序列化
# 整合步骤
1、修改pom的profile默认激活为dev,修改成production
2、修改production/wxpay.properties里面的参数
wxpay.appId=应
wxpay.appSecret=appSecret 是 appId 对应的接口密码,微信公众号授权获取用户 openId 时使用
wxpay.mchId=微信支付商户号
wxpay.partnerKey=API 密钥
wxpay.certPath=apiclient_cert.p1 证书路径,在微信商户后台下载
wxpay.domain=外网访问项目的域名,支付通知中会使用
2
3
4
5
6