微信支付SDK JAVA使用方法
使用wechatpay-apache-httpclient
,获取httpclient。返回的httpClient将会自动处理请求头中的Authorization
鉴权。此处使用的是自动更新证书功能(可选)版本,将自动获取微信平台证书。
sdk github:https://github.com/wechatpay-apiv3/wechatpay-apache-httpclient
/**
* 获取访问微信服务器的httpclient
MERCHANTID : 商户号
MERCHANT_SERIAL_NUMBER:商户CA证书序列号,商户后台查看CA证书获取
privateKey:利用工具生成的商户私钥apiclient_key.pem,经过处理后生成PrivateKey对象,再传入
* @return
*/
public static HttpClient getWXHttpClient(){
String apiV3Key = WXPayParam.API_V3;
PrivateKey privateKey = getPrivateKey();
AutoUpdateCertificatesVerifier verifier = getVerifier();
WechatPayHttpClientBuilder builder = WechatPayHttpClientBuilder.create()
.withMerchant(WXPayParam.MERCHANTID, WXPayParam.MERCHANT_SERIAL_NUMBER, privateKey)
.withValidator(new WechatPay2Validator(verifier));
// 通过WechatPayHttpClientBuilder构造的HttpClient,会自动的处理签名和验签,并进行证书自动更新
HttpClient httpClient = builder.build();
return httpClient;
}
/**
* 获取verifier
* @return
*/
public static AutoUpdateCertificatesVerifier getVerifier(){
AutoUpdateCertificatesVerifier verifier = null;
PrivateKey privateKey = getPrivateKey();
try {
verifier = new AutoUpdateCertificatesVerifier(
new WechatPay2Credentials(WXPayParam.MERCHANTID, new PrivateKeySigner(WXPayParam.MERCHANT_SERIAL_NUMBER, privateKey)),
WXPayParam.API_V3.getBytes("utf-8"));
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
return verifier;
}
请求签名
商户需要使用自身的私钥对API URL、消息体等关键数据的组合进行SHA-256 with RSA签名。请求的签名信息通过HTTP头
Authorization
传递,具体说明请见签名生成指南。没有携带签名或者签名验证不通过的请求,都不会被执行,并返回401 Unauthorized
。
其中privateKey
由getPrivateKey
函数读取CA颁发证书中的apiclient_key.pem
后生成,详见下方getPrivateKey
函数。
注意:读取pem文件后需要去除开头的-----BEGIN PRIVATE KEY-----
和结尾的-----END PRIVATE KEY-----
。
示例:MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDCKPgXTnYkgcNK yNHGK9wjKbTCmnG8ikThJG0B9uLt9vbAyJRP4K05fBk/5Lz+33aVAH1pIk5ftD/L 1HCR0XxWijMH0FkCGk/De0Yxrh1cdcL/l0z9NnvsHACkEbvs1yMsG8Fhm/fvDq3I Pvtu3DWXiIZ7hC/wd/AGYJY6hZbGy6X5finZh+mK7s9aBI4gp7xmk7+AoAjJ9zk2 iWTGd9+SgcwiYvK11QhMi+SMMfDiSYkQofm9iumFVexLEldw02jo0nIoziBRP4Yn qePQ3uBWvtK/yjAQ+TryoAtCr/6q7gflZjTtwQxCMGBqEd6k78aAc/WhZko3HLWJ 18hl8YsnAgMBAAECggEALCGDwkhjMgkMioL6q0Bs2NEx9MmF8IS9Ay90V232RoBL taXhkAZRWS+LzaoACy5flZ524t6ZUcd2eK3gqEQlLsZasvv4PzIbzyLF4aThp5Jc sBuDtEoeAJycyK3/OOXtaKkmWzlIMV30wf8OxzPmOnsdlhWFj/Ky68AoZUTX8HcT 7DWpvyA5XBd0BDex89zYK79i50DFFFNLZ7REd5xVG/hD9GDrcaEqkInSm11jxNIX 4Vd07jO53XSxWzJYcA9Xc3+mJqBLbJDLTAN3cv8cLpv4H94WsRxj5Y7gbF71vAcK KdYZ7QuDKsb9swgKxY+1p/6ZOi72Kc7Gz6q4qgmbAQKBgQDmHhtfZMJFKWHrrD8d Gianzzq7EQpCcJQuG5CIxhZVX1bwSJ+B/Gxg1UlDMyr+QHxmOpw73kd2WydDmbNd LNJEDlkm3Efi5QExWT+6TnSKEBgx7oxvAcHATb7/yXtOOwttGkBAC8ljFRKBNW8R
kQFMeelY+kRJZpnzthGWR7KnXwKBgQDX/4SCmxjQKkRLawbHcX1Mib6Bbz84poKr
gCr4ZQHn0rJr/oasn+UB5xlUioW5rLMSAO38d//xW21fjUkxOAwDiGjnpprT4xjM
v9zENU9nXQ8AlF2Jzbha/RkJWvgor7wAhMAm36yW63+d3V7rpBzBHHdXwjblKp34
BhFlLm4ZOQKBgFqCgv+tUOAFG9enYxeePpAIaTBEzoU9ZHsSKnIxf31Kx5Yw6lQl
JbecjHla+dERKhzHdsXxcqgxyCrFnI/MXlOYVSZ8w+WRbzuqv+8Whq37EJkrG59Z
0IxDyBkxdUda3+6kwZqvSCGpmyKpEquVHi6nUMnHfe5k5a6+8QHr52//AoGAN16o
+VII6lPrbenhsv7Ev/oPe96otjz5Aj24xjQeaO76DfURUO8sJXC4bZOU9CPxQ4w5
dZ7NXXGyd+wf9x4G9mDhg4CR7/8nPFVyolmIIVcZoWxnDgxOVgTLhjprowJpjzh4
iX6NH6L+89jrnDxVoqtJbJW8vMJP/GSR0P41+wECgYEAjp85VuTKCiQQE+7epLwZ
MmParo9h2iPZeTYzRl2Ur9FDFFWYJIcTk1ct+opMFju/NLdGtiXMPW+KD/KEiHBK
MTE84Gdd6Ims8lMIFa1MiwLLPmemKfkARNX6yKja83fkPSse8v7DQWvlrnt2JGz7
aBP3r35QWv+BWSZ8wiCky4A=
不知道为什么,利用Scanner读取文件后,自动去除私钥(pem文件内容)所有空格了,因此去除-----BEGINPRIVATEKEY-----
和----ENDPRIVATEKEY-----
即可。
/**
* 获取私钥的PrivateKey对象
* @param key
* @return
*/
public static PrivateKey getPrivateKey() {
String s = readFile("D:\\_Document\\fanmini\\1353229602_20210310_cert\\apiclient_key.pem");
// System.out.println("===========file:"+s);
return getPrivateKey(s);
}
public static PrivateKey getPrivateKey(String key) {
try {
//这个也行
// byte[] byteKey = new BASE64Decoder().decodeBuffer(key);
byte[] byteKey = DatatypeConverter.parseBase64Binary(key);
PKCS8EncodedKeySpec x509EncodedKeySpec = new PKCS8EncodedKeySpec(byteKey);
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
return keyFactory.generatePrivate(x509EncodedKeySpec);
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
/**
* 读取文件
* @param filePath
* @return
*/
private static String readFile(String filePath){
File file = new File(filePath);
try(Scanner scanner=new Scanner(file)){
StringBuilder stringBuilder = new StringBuilder();
while(scanner.hasNext()){
stringBuilder.append(scanner.next());
}
String s = stringBuilder.toString();
return s.replace("-----BEGINPRIVATEKEY-----","").replace("-----ENDPRIVATEKEY-----","");
} catch (FileNotFoundException e) {
e.printStackTrace();
return null;
}
}
微信apiV3接口访问示例代码
文档: https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_5_1.shtml
统一下单
像微信支付平台提交订单,获取prepay_id.
/**
* 统一下单
* orderId:商户订单编号
* content:商品详情
* amount:订单金额
*/
public static String putOrder(String orderId, String content, BigDecimal amount) throws UnsupportedEncodingException {
HttpClient httpClient = WXUtil.getWXHttpClient();
int total = amount.multiply(new BigDecimal(100)).intValue();
//配置所需参数
JSONObject jsonObject = new JSONObject();
jsonObject.put("appid",WXPayParam.APPID);
jsonObject.put("mchid",WXPayParam.MERCHANTID);
jsonObject.put("description",content);
jsonObject.put("out_trade_no",orderId);
jsonObject.put("notify_url",WXPayUrl.NOTIFY_URL);
jsonObject.put("amount",new MapObject().put("total",total).put("currency","CNY"));
jsonObject.put("payer",new MapObject().put("openid","ozQGu5ZKdSlDtAvR0yjcthNijmGA"));
//StringEntity作为POST请求中的body
StringEntity stringEntity = new StringEntity(jsonObject.toString());
stringEntity.setContentType(ContentType.APPLICATION_JSON.toString());
// 创建http请求
HttpPost httpPost = new HttpPost(WXPayUrl.PAY);
httpPost.setEntity(stringEntity);
//设置请求头,必须填,可以默认为这两个。
//详见:https://wechatpay-api.gitbook.io/wechatpay-api-v3/jie-kou-wen-dang/ping-tai-zheng-shu
httpPost.setHeader("Accept","application/json");
httpPost.setHeader("User-Agent","用户代理(https://zh.wikipedia.org/wiki/User_agent)");
// 后面跟使用Apache HttpClient一样
try {
HttpResponse response = httpClient.execute(httpPost);
HttpEntity entity = response.getEntity();
JSONObject resJson = JSONUtil.parseObj(EntityUtils.toString(entity));
return resJson.get("prepay_id").toString();
} catch (IOException e) {
e.printStackTrace();
}
return "";
}
调起微信小程序支付接口
用途:返回微信支付调用时需要的参数,包括计算签名、prepay_id
.
prepayId从上述putOrder
函数获取。签名从getMiniProgramSign
函数获取,如下所示。
接口参数详见:https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_5_4.shtml
public static JSONObject callMiniPay(String prepayId){
String timeStamp = String.valueOf( DateUtil.currentSeconds());
String nonceStr = RandomUtil.randomString(32);
String packageStr = "prepay_id=" + prepayId;
String paySign = WXUtil.getMiniProgramSign(WXPayParam.APPID, timeStamp, nonceStr, packageStr);
JSONObject jsonObject = new JSONObject();
jsonObject.put("appId",WXPayParam.APPID);
jsonObject.put("timeStamp",timeStamp);
jsonObject.put("nonceStr",nonceStr);
jsonObject.put("package",packageStr);
jsonObject.put("signType","RSA");
jsonObject.put("paySign",paySign);
return jsonObject;
}
获取小程序调起支付的签名getMiniProgramSign
函数:
注意:
签名串一共有四行,每一行为一个参数。行尾以\n(换行符,ASCII编码值为0x0A)结束,包括最后一行。
最后一行一定还要加换行符
/**
* 获取小程序调起支付的签名
* @param appId
* @param timeStamp
* @param nonceStr
* @param packageStr
* @return
*/
public static String getMiniProgramSign(String appId,String timeStamp,String nonceStr,String packageStr){
// {"prepay_id":"wx111528217145060d54d6345ff317950000"}
StringBuilder builder = new StringBuilder();
builder.append(appId);
builder.append("\n");
builder.append(timeStamp);
builder.append("\n");
builder.append(nonceStr);
builder.append("\n");
builder.append(packageStr);
builder.append("\n");
String sign = sign(getPrivateKey(), builder.toString());
System.out.println("sign: "+sign);
return sign;
}
接收微信应答
用途:接受微信发回的支付结果。resource部分需要解密才能知道,包含了商户订单。
主要是对应答的解密。下述代码使用decryptToString
函数解密,具体见下述代码。
文档:https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_5_5.shtml
/**
* 接受微信支付订单通知
*
* @param id
* @param mmap
* @return
*/
@ApiOperation(value = "接受微信支付订单通知", notes = "接受微信支付订单通知")
@PostMapping("/get/notification")
@ResponseBody
public Object getNotification(@RequestBody String notification) throws GeneralSecurityException, IOException {
System.out.println("notification: "+notification);
JSONObject jsonObject = JSONUtil.parseObj(notification);
byte[] nonce = jsonObject.getByPath("resource.nonce").toString().getBytes(StandardCharsets.UTF_8);
byte[] associatedData = jsonObject.getByPath("resource.associated_data").toString().getBytes(StandardCharsets.UTF_8);
String ciphertext = jsonObject.getByPath("resource.ciphertext").toString();
String res = WXUtil.decryptToString(associatedData, nonce, ciphertext);
//应答的resource对象
JSONObject jb = JSONUtil.parseObj(res);
System.out.println("res:\n"+jb.toStringPretty());
String trade_state = jb.get("trade_state").toString();
String orderId = jb.get("out_trade_no").toString();
if(trade_state.equals("SUCCESS")){
//更新订单状态为已支付
Order order = orderService.selectByPrimaryKey(orderId);
order.setStatus(OrderStatus.HAS_PAYED.getCode());
order.setPayTime(DateUtils.getNowDate());
orderService.updateByPrimaryKeySelective(order);
//处理订单支付后的操作
orderService.doAfterPay(order);
}
return new MapObject().put("code","SUCCESS").put("message","");
}
解密函数decryptToString
:
(AES_256对称解密算法,使用apiv3秘钥解密)
/**
* apiv3秘钥(后台自己设置的那个)
*/
private static final byte[] aesKey=WXPayParam.API_V3.getBytes(StandardCharsets.UTF_8);
/**
* 解密微信应答报文
* @param associatedData
* @param nonce
* @param ciphertext
* @return
* @throws GeneralSecurityException
* @throws IOException
*/
public static String decryptToString(byte[] associatedData, byte[] nonce, String ciphertext)
throws GeneralSecurityException, IOException {
try {
Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
SecretKeySpec key = new SecretKeySpec(aesKey, "AES");
GCMParameterSpec spec = new GCMParameterSpec(TAG_LENGTH_BIT, nonce);
cipher.init(Cipher.DECRYPT_MODE, key, spec);
cipher.updateAAD(associatedData);
return new String(cipher.doFinal(Base64.getDecoder().decode(ciphertext)), "utf-8");
} catch (NoSuchAlgorithmException | NoSuchPaddingException e) {
throw new IllegalStateException(e);
} catch (InvalidKeyException | InvalidAlgorithmParameterException e) {
throw new IllegalArgumentException(e);
}
}
下面详细描述对通知数据进行解密的流程:
1、用商户平台上设置的APIv3密钥【微信商户平台—>账户设置—>API安全—>设置APIv3密钥】,记为key; 2、针对resource.algorithm中描述的算法(目前为AEAD_AES_256_GCM),取得对应的参数nonce和associated_data; 3、使用key、nonce和associated_data,对数据密文resource.ciphertext进行解密,得到JSON形式的资源对象;