最近有个项目里面要使用微信支付,网上找了一大堆资料都没有相关的,微信支付的文档写的让人堪忧,没有对接经验的看微信文档多少有点懵,所以打算自己写一篇对接的文档,也是怕自我忘记,闲话不多说,开整!!
整体流程:
1、生成签名。
2、将签名转换为密文。
3、设置HTTP头部
4、网络请求微信支付。
不说废话,上代码,能跑通一切好说~!
只需填写MCH_ID、SERIAL_NO、APP_ID 即可启动测试
下述有每一步详细的解读,授人以鱼不如授人以渔。
import com.alibaba.fastjson.JSONObject;
import okhttp3.HttpUrl;
import java.io.*;
import java.net.HttpURLConnection;
import java.net.URL;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.security.*;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.PKCS8EncodedKeySpec;
import java.util.Base64;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
/**
* @program: management
* @description:
* @author: YH
* @create: 2021-10-08 17:12
**/
public class TestNative {
static String schema = "WECHATPAY2-SHA256-RSA2048";
/**
* 填写你的商户号
*/
static String MCH_ID = "";
/**
* 填写证书序列号
*/
static String SERIAL_NO = "";
/**
* 填写应用APP ID
*/
static String APP_ID = "";
/**
* apiclient_key.pem文件地址 如C:\Users\Administrator\Desktop\wechat\apiclient_key.pem
*/
static String PATH_PEM = "C:\\Users\\Administrator\\Desktop\\wechat\\apiclient_key.pem";
/**
* 请求地址
*/
static String NATIVE_URL = "https://api.mch.weixin.qq.com/v3/pay/transactions/native";
/**
* 填写你的返回地址 需要HTTPS返回地址
*/
static String RETURN_ADDRESS = "https://www.baidu.com";
public static void main(String[] args) {
try {
int money = 100;
String description = "测试支付";
HttpUrl httpurl = HttpUrl.parse(NATIVE_URL);
//签名表头信息 Authorization
String orderId = UUID.randomUUID().toString().replaceAll("-", "").toUpperCase();
String body = OrderData(orderId, money, description);
String authorization = schema + " " + getToken("POST", httpurl, body, orderId);
//下单调用的接口,JSON格式
String codeUrl = nativePostBody(NATIVE_URL, body, authorization);
System.out.println(codeUrl);
} catch (Exception e) {
e.printStackTrace();
}
}
static String OrderData(String orderId, int money, String description) {
// 应用ID appid
// 直连商户号 mchid
// 商品描述 description
//商户订单号 out_trade_no
//通知地址 notify_url
//订单金额 amount对象: 总金额 total 货币类型 currency
JSONObject jsonObject = new JSONObject();
jsonObject.put("mchid", MCH_ID);
jsonObject.put("out_trade_no", orderId);
jsonObject.put("appid", APP_ID);
jsonObject.put("description", description);
jsonObject.put("notify_url", RETURN_ADDRESS);
Map<String, Object> map = new HashMap<>();
map.put("total", money);
map.put("currency", "CNY");
jsonObject.put("amount", map);
return String.valueOf(jsonObject);
}
/**
* 获取签名信息所有值
*
* @param method 请求方法
* @param url URL地址
* @param body BOdy参数
* @return
*/
static String getToken(String method, HttpUrl url, String body, String orderId) throws Exception {
String nonceStr = orderId;
//获得系统时间,把毫秒换算成秒 /1000
long timestamp = System.currentTimeMillis() / 1000;
String message = buildMessage(method, url, timestamp, nonceStr, body);
String signature = sign(message.getBytes("utf-8"));
return "mchid=\"" + MCH_ID + "\","
+ "nonce_str=\"" + nonceStr + "\","
+ "timestamp=\"" + timestamp + "\","
+ "serial_no=\"" + SERIAL_NO + "\","
+ "signature=\"" + signature + "\"";
}
/**
* 拼接明文数值
*
* @param method 请求方法 GET or POST
* @param url 网络请求方法地址 取除域名项
* @param timestamp 时间戳
* @param nonceStr 随机数
* @param body GET请求不需要Body参数,POST需要Body
* @return
*/
static String buildMessage(String method, HttpUrl url, long timestamp, String nonceStr, String body) {
String canonicalUrl = url.encodedPath();
//get请求自动做了校验,会把空字符串进行识别,注意是空字符串!!!!!
if (url.encodedQuery() != null) {
canonicalUrl += "?" + url.encodedQuery();
}
//官方的方法自动做了换行的所有动作,注意唤起支付的参数不一样需要更换(这里是统一下单所以直接照搬即可)
return method + "\n"
+ canonicalUrl + "\n"
+ timestamp + "\n"
+ nonceStr + "\n"
+ body + "\n";
}
/**
* 签名加密
*
* @param message
* @return
* @throws NoSuchAlgorithmException
* @throws SignatureException
* @throws IOException
* @throws InvalidKeyException
*/
static String sign(byte[] message) throws NoSuchAlgorithmException, SignatureException, IOException, InvalidKeyException {
//加密方式
Signature sign = Signature.getInstance("SHA256withRSA");
//私钥,通过getPrivateKey来获取,这是个方法可以接调用 ,需要的是_key.pem文件的绝对路径配上文件名
sign.initSign(getPrivateKey(PATH_PEM));
sign.update(message);
return Base64.getEncoder().encodeToString(sign.sign());
}
/**
* 获取私钥。
*
* @param filename 私钥文件路径 (required)
* @return 私钥对象
* <p>
* 完全不需要修改,注意此方法也是去掉了头部和尾部,注意文件路径名
*/
public static PrivateKey getPrivateKey(String filename) throws IOException {
String content = new String(Files.readAllBytes(Paths.get(filename)), "utf-8");
try {
String privateKey = content.replace("-----BEGIN PRIVATE KEY-----", "")
.replace("-----END PRIVATE KEY-----", "")
.replaceAll("\\s+", "");
KeyFactory kf = KeyFactory.getInstance("RSA");
return kf.generatePrivate(
new PKCS8EncodedKeySpec(Base64.getDecoder().decode(privateKey)));
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException("当前Java环境不支持RSA", e);
} catch (InvalidKeySpecException e) {
throw new RuntimeException("无效的密钥格式");
}
}
/**
* Post请求带Body参数
*
* @param actionUrl
* @param params
* @param requestString
* @return
* @throws IOException
*/
public static String nativePostBody(String actionUrl, String params, String requestString)
throws IOException {
String serverURL = actionUrl;
StringBuffer sbf = new StringBuffer();
String strRead = null;
URL url = new URL(serverURL);
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
//请求post方式
connection.setRequestMethod("POST");
connection.setDoInput(true);
connection.setDoOutput(true);
//header内的的参数在这里set
connection.setRequestProperty("Content-Type", "application/json");
//Native支付需要的参数表头参数
connection.setRequestProperty("Authorization", requestString);
connection.connect();
OutputStreamWriter writer = new OutputStreamWriter(connection.getOutputStream(), "UTF-8");
//body参数放这里
writer.write(params);
writer.flush();
InputStream is = connection.getInputStream();
BufferedReader reader = new BufferedReader(new InputStreamReader(is, "UTF-8"));
while ((strRead = reader.readLine()) != null) {
sbf.append(strRead);
sbf.append("\r\n");
}
reader.close();
connection.disconnect();
String results = sbf.toString();
return results;
}
}
下方为解读上述代码如何实现
签名组成:微信签名组成分为 “明文”,“密文” ,明文组成方式如下:
构造签名串 明文
一共有五行,分别对应着 HTTP请求方法,URL,请求时间戳,请求随机串,请求报文主体 注意!每行后面都需换行 加入\n 即使为空也需要换行
第一行:HTTP请求方式 为GET、POST、PUT等(注意:请求方式是指的是微信支付接口需要何种你就写哪种)举个栗子 :下图为Native下单接口 。
在这个接口上可以看到是POST提交方式,因此如果要对接此接口构造签名串的话在HTTP一栏写上POST。
第二行:获取请求的绝对URL地址 也就是除去域名部分得到的即使绝对地址,举个栗子: https://api.mch.weixin.qq.com/v3/pay/transactions/nativeNative 下单这个为例,https://api.mch.weixin.qq.com这一节为域名部分,/v3/pay/transactions/native 而这一节就是请求的绝对路径了,在构造签名串第二步填上即可。 如果你请求的地址内包含了请求参数那同样也是需要加入的到请求绝对路径中的,同样 再来举个栗子: 下图为Native支付微信订单号查询接口
在这个例子中就能看出这是个GET请求 在路径后追加了参数,在构造签名串的时候也是需要带上的 以上图举例 URL: https://api.mch.weixin.qq.com/v3/pay/transactions/id/{微信订单号} 它的绝对路径为 /v3/pay/transactions/id/{微信订单号}?mchid=商户号 这样才是一个完整的。
第三步 系统时间戳 因为微信支付会拒绝处理很久之前发起的请求,获取当前时间毫秒数转换成秒即可:
long timestamp = System.currentTimeMillis() / 1000;
System.currentTimeMillis()此方法获得的是毫秒数,微信支付要的是秒,所以将毫秒除以1000得到秒。
第四步 生成请求随机串 生成一个请求随机串 ,这一步其实为商户系统生成的订单号
UUID.randomUUID().toString().replaceAll("-", "").toUpperCase();
这里为了方便我使用UUID代替,也可以使用一些有意义的数字代替,如根据日期+当前时间+商品ID+当前下单人数,类似的当然这是有可能会重复的在并发环境下的话。个人认为不如UUID来的省事。
第五步 请求中的报文主题 Body 请求方法为GET时,报文主体为空,当请求方法为POST或PUT时应发送一组JSON报文格式,报文格式为什么样呢,举个栗子 下图为Native下单请求报文
这为Native请求报文,这就是为请求报文主题Body中的内容,如果是GET请求Body是为空的,注意为空不是写Null而是“”代表为空。
明文示例: 将上述5步完成则如下
GET请求签名串明文: 用Native查询订单作为GET请求例子
GET\n
/v3/pay/transactions/id/{微信订单号}?mchid=商户号\n
时间戳\n
随机字符串\n
\n
POST请求签名串明文: 用Native下单作为POST请求例子
POST\n
/v3/pay/transactions/native\n
时间戳\n
随机字符串\n
{"mchid": "1900006XXX","out_trade_no": "native12177525012014070332333","appid": "wxdace645e0bc2cXXX","description": "Image形象店-深圳腾大-QQ公仔","notify_url": "https://weixin.qq.com/","amount": {"total": 1,"currency": "CNY"}}\n
明文构造解析详解说到这,其实微信支付文档里有提供生成签名的代码,缺少了部分注释,后面我把自己的代码给帖出来供参考。
构造签名串 密文
我对于这个密文的理解,个人理解,APIV3规则里需要生成一组SHA256格式的密文此密文需使用Base64编码才能得到微信签名值,为什么要这么做呢,我猜测是微信这边为了防止虚假请求、或虚拟请求、模拟请求,可能造成的一些财产损失所制定的规约,使用的两种加密方式都是几乎不可逆。
SHA256是安全散列算法,是摘要算法的一种,简单点来说,SHA可以看作是一种单向函数,几乎不可逆,给一段信息,它会给你生成固定长度的摘要,当然不同的信息是的确有可能生成相同的摘要的,这被称作碰撞,简单来说,摘要的长度越长,碰撞的几率越低,从SHA的特点来看,用来作文件验证和数字签名是非常合适的,比特币使用的就是SHA-2中的SHA256算法,对于加密数字签名是具有可靠性的。
Base64算法了解的朋友应该清楚,采用Base64编码具有不可读性,需要解码后才能阅读,两者配合使用,前者将明文加密,后者将加密后的进行Base64编码,标准的Base64编码不适合直接放在URL里传输,应为URL因为URL编码器会把标准Base64中的“/”和“+”字符变为形如“%XX”的形式,而这些“%”号在存入数据库时还需要再进行转换,因为ANSI SQL中已将“%”号用通配符,为解决此问题,采用一种用于URL的改进Base64编码,此编码很好辨认,它在尾部填充‘=’号,微信支付使用的便是此。因密文是需要通过URL传递的。
构造密文,也就是上述两种方式进行构造,先使用SHA256,再使用Base64进行编码,后续会将代码在下方贴出,我是觉得理解了该怎么写比直接复制代码更有用。
构造微信支付HTTP头
微信支付商户API V3要求通过HTTP Authorization头来传递签名。 Authorization由认证类型和签名信息两个部分组成,上述做的密文就是加入在Authorization内的信息,因为每请求微信支付,微信支付都会检查你的头部,有没有携带Authorization ,Authorization是否正确。
Authorization是如何组成的呢:一个是认证类型 一个是签名信息
认证信息:固定写死目前为: WECHATPAY2-SHA256-RSA2048 (2021-10-08文档编写日期)
签名信息:分为五个部分,商户号、商户API证书序列号、请求随机串、时间戳、签名值
商户号:发起请求的商户 在微信支付商家内能找到商户号。 mchid
商户API证书:需要去微信支付商户去申请证书,在证书列表中可找到此项。 serial_no
请求随机串:注意此项不是让你重新生成一个,而是拿你生成签名的随机串来使用(一个不愿透露姓名的踩坑人)。nonce_str
时间戳:也就是生成签名时生成的那一个。timestamp
签名值:明文转换为SHA256密文再通过Base64编码的值。signature
此上五个无顺序要求。
举个例子:
WECHATPAY2-SHA256-RSA2048 mchid="商户号",nonce_str="订单号",timestamp="时间戳",serial_no="证书序列号",signature="密文"
上述说所说都需加在Authorization HTTP头部信息内。
如何加入呢,如下代码供参考:
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
//请求post方式
connection.setRequestMethod("POST");
//指定的格式
connection.setRequestProperty("Content-Type", "application/json");
//HTTP头部信息
connection.setRequestProperty("Authorization", authorization);
//连接
connection.connect();
到此密文构造、头部拼接 都描述的差不多了,上组成他们的代码!
Body构成(使用的是alibaba.fastjson)
String OrderData(String orderId, int money, String description) {
// 应用ID appid
// 直连商户号 mchid
// 商品描述 description
//商户订单号 out_trade_no
//通知地址 notify_url
//订单金额 amount对象: 总金额 total 货币类型 currency
JSONObject jsonObject = new JSONObject();
jsonObject.put("mchid", "商户ID");
jsonObject.put("out_trade_no", orderId);
jsonObject.put("appid", "应用APPID");
jsonObject.put("description", description);
jsonObject.put("notify_url", "支付成功用户回调地址 必须为HTTPS");
Map<String, Object> map = new HashMap<>();
map.put("total", money);
map.put("currency", "CNY");
jsonObject.put("amount", map);
return String.valueOf(jsonObject);
}
获取签名信息
/**
* 获取签名信息
*
* @param method 请求方法
* @param url URL地址
* @param body BOdy参数
* @return
*/
String getToken(String method, HttpUrl url, String body, String orderId) throws Exception {
String nonceStr = orderId;
//获得系统时间,把毫秒换算成秒 /1000
long timestamp = System.currentTimeMillis() / 1000;
String message = buildMessage(method, url, timestamp, nonceStr, body);
String signature = sign(message.getBytes("utf-8"));
return "mchid=\"" + Constant.MCH_ID + "\","
+ "nonce_str=\"" + nonceStr + "\","
+ "timestamp=\"" + timestamp + "\","
+ "serial_no=\"" + Constant.SERIAL_NO + "\","
+ "signature=\"" + signature + "\"";
}
拼接明文值
/**
* 拼接明文数值
*
* @param method 请求方法 GET or POST
* @param url 网络请求方法地址 取除域名项
* @param timestamp 时间戳
* @param nonceStr 随机数
* @param body GET请求不需要Body参数,POST需要Body
* @return
*/
String buildMessage(String method, HttpUrl url, long timestamp, String nonceStr, String body) {
String canonicalUrl = url.encodedPath();
//get请求自动做了校验,会把空字符串进行识别,注意是空字符串!!!!!
if (url.encodedQuery() != null) {
canonicalUrl += "?" + url.encodedQuery();
}
//官方的方法自动做了换行的所有动作,注意唤起支付的参数不一样需要更换(这里是统一下单所以直接照搬即可)
return method + "\n"
+ canonicalUrl + "\n"
+ timestamp + "\n"
+ nonceStr + "\n"
+ body + "\n";
}
明文加密
/**
* 签名加密
*
* @param message
* @return
* @throws NoSuchAlgorithmException
* @throws SignatureException
* @throws IOException
* @throws InvalidKeyException
*/
String sign(byte[] message) throws NoSuchAlgorithmException, SignatureException, IOException, InvalidKeyException {
//加密方式
Signature sign = Signature.getInstance("SHA256withRSA");
//私钥,通过getPrivateKey来获取,这是个方法可以接调用 ,需要的是_key.pem文件的绝对路径配上文件名
sign.initSign(getPrivateKey("C:\\Users\\Administrator\\Desktop\\wechat\\apiclient_key.pem"));
sign.update(message);
return Base64.getEncoder().encodeToString(sign.sign());
}
获取私钥
/**
* 获取私钥。
*
* @param filename 私钥文件路径 (required)
* @return 私钥对象
* <p>
* 完全不需要修改,注意此方法也是去掉了头部和尾部,注意文件路径名
*/
public static PrivateKey getPrivateKey(String filename) throws IOException {
String content = new String(Files.readAllBytes(Paths.get(filename)), "utf-8");
try {
String privateKey = content.replace("-----BEGIN PRIVATE KEY-----", "")
.replace("-----END PRIVATE KEY-----", "")
.replaceAll("\\s+", "");
KeyFactory kf = KeyFactory.getInstance("RSA");
return kf.generatePrivate(
new PKCS8EncodedKeySpec(Base64.getDecoder().decode(privateKey)));
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException("当前Java环境不支持RSA", e);
} catch (InvalidKeySpecException e) {
throw new RuntimeException("无效的密钥格式");
}
}
请求方法 Post请求带参网络请求方法
/**
* Post请求带Body参数
*
* @param actionUrl
* @param params
* @param requestString
* @return
* @throws IOException
*/
public static String nativePostBody(String actionUrl, String params, String requestString)
throws IOException {
String serverURL = actionUrl;
StringBuffer sbf = new StringBuffer();
String strRead = null;
URL url = new URL(serverURL);
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
//请求post方式
connection.setRequestMethod("POST");
connection.setDoInput(true);
connection.setDoOutput(true);
//header内的的参数在这里set
connection.setRequestProperty("Content-Type", "application/json");
//Native支付需要的参数表头参数
connection.setRequestProperty("Authorization", requestString);
connection.connect();
OutputStreamWriter writer = new OutputStreamWriter(connection.getOutputStream(), "UTF-8");
//body参数放这里
writer.write(params);
writer.flush();
InputStream is = connection.getInputStream();
BufferedReader reader = new BufferedReader(new InputStreamReader(is, "UTF-8"));
while ((strRead = reader.readLine()) != null) {
sbf.append(strRead);
sbf.append("\r\n");
}
reader.close();
connection.disconnect();
String results = sbf.toString();
return results;
}
以上方法都是单独的,需要一个集中操作类来调用,代码如下:
//认证类型 建议变成静态常量
String schema = "WECHATPAY2-SHA256-RSA2048";
//发起请求的URL地址后续在buildMessage操作方法里拿到绝对路径
HttpUrl httpurl = HttpUrl.parse("请求的URL地址,如Native https://api.mch.weixin.qq.com/v3/pay/transactions/native");
//订单UUID
String orderId = UUID.randomUUID().toString().replaceAll("-", "").toUpperCase();
//body参数值 Native下单
String body = OrderData(orderId, money, description);
//拼接头部 authorization
String authorization = schema + " " + getToken("POST", httpurl, body, orderId);
//网络请求
String codeUrl = nativePostBody("请求地址 如Native https://api.mch.weixin.qq.com/v3/pay/transactions/native", body, authorization);
上述为关键代码,每个人的业务不一样因此就不贴出ServiceImpl层的代码给大伙了,把上面的代码写在一个方法里面,其他的写在一个类里面就可以了。
说一下我的机器开发环境,JDK1.8 、win10 64位,Idea2020.3 其余的好像也没什么了,按照上述代码操作基本没问题,细心一点准没错。
如果请求返回了401 不用慌,只要按照上面来了慢慢排错一定会找到问题的,可以使用微信提供的验签工具,我在这里贴出来
https://pay.weixin.qq.com/wiki/doc/apiv3/wechatpay/download/Product_5.zip 复制连接在浏览器就可以下载。
下载解压出来是这样
点击打开
选择签名/验签
注意我使用的是 apiclient_key.pem文件生成的签名
验证签名的时候千万别忘记后面的空行,这是必须要的。
到这里基本已经结束了,微信支付API V3的规则基本已经完成了。
以上为个人对接微信支付所积累的经验,将此写出防止以后对接忘记。
个人感觉这篇文章写的算是详细的,供各位参考,有问题望各位指出,大家共同进步。
感谢看完!!!!!!