微信支付V3版
阿新 • • 發佈:2021-01-01
微信支付V3版
1.引入依賴
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.5.RELEASE</version>
<relativePath/>
< /parent>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>com.squareup.okhttp3</groupId>
<artifactId>okhttp</artifactId>
<version>4.6.0</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>net.sf.json-lib</groupId>
<artifactId>json-lib</artifactId>
<version>2.4</version>
<classifier>jdk15</classifier>
</dependency>
<!-- huTool 工具包 -->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-crypto</artifactId>
<version>5.5.0</version>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-http</artifactId>
<version>5.5.0</version>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-json</artifactId>
<version>5.5.0</version>
</dependency>
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
<version>4.5.12</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>servlet-api</artifactId>
<version>2.5</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>com.github.xkzhangsan</groupId>
<artifactId>xk-time</artifactId>
<version>2.1.2</version>
</dependency>
</dependencies>
2.建立時間工具類 DateTimeZoneUtil.class
import cn.hutool.core.util.StrUtil;
import com.xkzhangsan.time.converter.DateTimeConverterUtil;
import com.xkzhangsan.time.formatter.DateTimeFormatterUtil;
import java.io.Serializable;
import java.time.ZonedDateTime;
import java.util.Date;
/**
* 〈一句話功能簡述〉<br>
* 〈時間轉換工具類〉
*
* @author Gym
* @create 2020/12/18
* @since 1.0.0
*/
public class DateTimeZoneUtil implements Serializable {
/**
* 時間轉 TimeZone
*
* @param time
* @return
* @throws Exception
*/
public static String dateToTimeZone(long time) throws Exception {
return dateToTimeZone(new Date(time));
}
/**
* 時間轉 TimeZone
*
* @param date
* @return
* @throws Exception
*/
public static String dateToTimeZone(Date date) throws Exception {
String time;
if (date == null) {
throw new Exception("date is not null");
}
ZonedDateTime zonedDateTime = DateTimeConverterUtil.toZonedDateTime(date);
time = DateTimeFormatterUtil.format(zonedDateTime, DateTimeFormatterUtil.YYYY_MM_DD_T_HH_MM_SS_XXX_FMT);
return time;
}
/**
* TimeZone 時間轉標準時間
*
* @param str
* @return
* @throws Exception
*/
public static String timeZoneDateToStr(String str) throws Exception {
String time;
if (StrUtil.isBlank(str)) {
throw new Exception("str is not null");
}
ZonedDateTime zonedDateTime = DateTimeFormatterUtil.parseToZonedDateTime(str, DateTimeFormatterUtil.YYYY_MM_DD_T_HH_MM_SS_XXX_FMT);
if (zonedDateTime == null) {
throw new Exception("str to zonedDateTime fail");
}
time = zonedDateTime.format(DateTimeFormatterUtil.YYYY_MM_DD_HH_MM_SS_FMT);
return time;
}
}
3.解密工具類 AesUtil.class
import cn.hutool.core.codec.Base64;
import javax.crypto.Cipher;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.spec.GCMParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.security.GeneralSecurityException;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
/**
* 〈一句話功能簡述〉<br>
* 〈〉
*
* @author Gym
* @create 2020/12/18
* @since 1.0.0
*/
public class AesUtil {
static final int KEY_LENGTH_BYTE = 32;
static final int TAG_LENGTH_BIT = 128;
private final byte[] aesKey;
/**
* @param key APIv3 金鑰
*/
public AesUtil(byte[] key) {
if (key.length != KEY_LENGTH_BYTE) {
throw new IllegalArgumentException("無效的ApiV3Key,長度必須為32個位元組");
}
this.aesKey = key;
}
/**
* 證書和回撥報文解密
*
* @param associatedData associated_data
* @param nonce nonce
* @param cipherText ciphertext
* @return {String} 平臺證書明文
* @throws GeneralSecurityException 異常
*/
public String decryptToString(byte[] associatedData, byte[] nonce, String cipherText) throws GeneralSecurityException {
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.decode(cipherText)), StandardCharsets.UTF_8);
} catch (NoSuchAlgorithmException | NoSuchPaddingException e) {
throw new IllegalStateException(e);
} catch (InvalidKeyException | InvalidAlgorithmParameterException e) {
throw new IllegalArgumentException(e);
}
}
}
4.對外暴露方法
公共引數
//請求閘道器
private static final String url_prex = "https://api.mch.weixin.qq.com/";
//編碼
private static final String charset = "UTF-8";
4-1.支付下單 V3PayGet()
/**
* 微信支付下單
*
* @param url 請求地址(只需傳入域名之後的路由地址)
* @param jsonStr 請求體 json字串 此引數與微信官方文件一致
* @param mercId 商戶ID
* @param serial_no 證書序列號
* @param privateKeyFilePath 私鑰的路徑
* @return 訂單支付的引數
* @throws Exception
*/
public static String V3PayGet(String url, String jsonStr, String mercId, String serial_no, String privateKeyFilePath) throws Exception {
String body = "";
//建立httpclient物件
CloseableHttpClient client = HttpClients.createDefault();
//建立post方式請求物件
HttpPost httpPost = new HttpPost(url_prex + url);
//裝填引數
StringEntity s = new StringEntity(jsonStr, charset);
s.setContentEncoding(new BasicHeader(HTTP.CONTENT_TYPE,
"application/json"));
//設定引數到請求物件中
httpPost.setEntity(s);
String post = getToken("POST", HttpUrl.parse(url_prex + url), mercId, serial_no, privateKeyFilePath, jsonStr);
//設定header資訊
//指定報文頭【Content-type】、【User-Agent】
httpPost.setHeader("Content-type", "application/json");
httpPost.setHeader("User-Agent", "Mozilla/4.0 (compatible; MSIE 5.0; Windows NT; DigExt)");
httpPost.setHeader("Accept", "application/json");
httpPost.setHeader("Authorization",
"WECHATPAY2-SHA256-RSA2048 " + post);
//執行請求操作,並拿到結果(同步阻塞)
CloseableHttpResponse response = client.execute(httpPost);
//獲取結果實體
HttpEntity entity = response.getEntity();
if (entity != null) {
//按指定編碼轉換結果實體為String型別
body = EntityUtils.toString(entity, charset);
}
EntityUtils.consume(entity);
//釋放連結
response.close();
switch (url) {
case "v3/pay/transactions/app"://返回APP支付所需的引數
return JSONObject.fromObject(body).getString("prepay_id");
case "v3/pay/transactions/jsapi"://返回JSAPI支付所需的引數
return JSONObject.fromObject(body).getString("prepay_id");
case "v3/pay/transactions/native"://返回native的請求地址
return JSONObject.fromObject(body).getString("code_url");
case "v3/pay/transactions/h5"://返回h5支付的連結
return JSONObject.fromObject(body).getString("h5_url");
}
return null;
}
4-2.微信調起支付引數 WxTuneUp()
/**
* 微信調起支付引數
* 返回引數如有不理解 請訪問微信官方文件
* https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter4_1_4.shtml
*
* @param prepayId 微信下單返回的prepay_id
* @param appId 應用ID(appid)
* @param privateKeyFilePath 私鑰的地址
* @return 當前調起支付所需的引數
* @throws Exception
*/
public static JSONObject WxTuneUp(String prepayId, String appId, String privateKeyFilePath) throws Exception {
String time = System.currentTimeMillis() / 1000 + "";
String nonceStr = UUID.randomUUID().toString().replace("-", "");
String packageStr = "prepay_id=" + prepayId;
ArrayList<String> list = new ArrayList<>();
list.add(appId);
list.add(time);
list.add(nonceStr);
list.add(packageStr);
//載入簽名
String packageSign = sign(buildSignMessage(list).getBytes(), privateKeyFilePath);
JSONObject jsonObject = new JSONObject();
jsonObject.put("appid", appId);
jsonObject.put("timeStamp", time);
jsonObject.put("nonceStr", nonceStr);
jsonObject.put("packages", packageStr);
jsonObject.put("signType", "RSA");
jsonObject.put("paySign", packageSign);
return jsonObject;
}
4-3.處理微信非同步回撥 notify()
/**
* 處理微信非同步回撥
*
* @param request
* @param response
* @param privateKey 32的祕鑰
*/
public static String notify(HttpServletRequest request, HttpServletResponse response, String privateKey) throws Exception {
Map<String, String> map = new HashMap<>(12);
String result = readData(request);
// 需要通過證書序列號查詢對應的證書,verifyNotify 中有驗證證書的序列號
String plainText = verifyNotify(result, privateKey);
if (StrUtil.isNotEmpty(plainText)) {
response.setStatus(200);
map.put("code", "SUCCESS");
map.put("message", "SUCCESS");
} else {
response.setStatus(500);
map.put("code", "ERROR");
map.put("message", "簽名錯誤");
}
response.setHeader("Content-type", ContentType.JSON.toString());
response.getOutputStream().write(JSONUtil.toJsonStr(map).getBytes(StandardCharsets.UTF_8));
response.flushBuffer();
String out_trade_no = JSONObject.fromObject(plainText).getString("out_trade_no");
return out_trade_no;
}
5. 同類類方法
5-1.生成組裝請求頭 getToken()
/**
* 生成組裝請求頭
*
* @param method 請求方式
* @param url 請求地址
* @param mercId 商戶ID
* @param serial_no 證書序列號
* @param privateKeyFilePath 私鑰路徑
* @param body 請求體
* @return 組裝請求的資料
* @throws Exception
*/
static String getToken(String method, HttpUrl url, String mercId, String serial_no, String privateKeyFilePath, String body) throws Exception {
String nonceStr = UUID.randomUUID().toString().replace("-", "");
long timestamp = System.currentTimeMillis() / 1000;
String message = buildMessage(method, url, timestamp, nonceStr, body);
String signature = sign(message.getBytes("UTF-8"), privateKeyFilePath);
return "mchid=\"" + mercId + "\","
+ "nonce_str=\"" + nonceStr + "\","
+ "timestamp=\"" + timestamp + "\","
+ "serial_no=\"" + serial_no + "\","
+ "signature=\"" + signature + "\"";
}
5-2.生成簽名 sign()
/**
* 生成簽名
*
* @param message 請求體
* @param privateKeyFilePath 私鑰的路徑
* @return 生成base64位簽名信息
* @throws Exception
*/
static String sign(byte[] message, String privateKeyFilePath) throws Exception {
Signature sign = Signature.getInstance("SHA256withRSA");
sign.initSign(getPrivateKey(privateKeyFilePath));
sign.update(message);
return Base64.getEncoder().encodeToString(sign.sign());
}
5-3.組裝簽名載入 buildMessage()
/**
* 組裝簽名載入
*
* @param method 請求方式
* @param url 請求地址
* @param timestamp 請求時間
* @param nonceStr 請求隨機字串
* @param body 請求體
* @return 組裝的字串
*/
static String buildMessage(String method, HttpUrl url, long timestamp, String nonceStr, String body) {
String canonicalUrl = url.encodedPath();
if (url.encodedQuery() != null) {
canonicalUrl += "?" + url.encodedQuery();
}
return method + "\n"
+ canonicalUrl + "\n"
+ timestamp + "\n"
+ nonceStr + "\n"
+ body + "\n";
}
5-4.獲取私鑰 getPrivateKey()
/**
* 獲取私鑰。
*
* @param filename 私鑰檔案路徑 (required)
* @return 私鑰物件
*/
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("無效的金鑰格式");
}
}
5-5.構造簽名串 buildSignMessage()
/**
* 構造簽名串
*
* @param signMessage 待簽名的引數
* @return 構造後帶待簽名串
*/
static String buildSignMessage(ArrayList<String> signMessage) {
if (signMessage == null || signMessage.size() <= 0) {
return null;
}
StringBuilder sbf = new StringBuilder();
for (String str : signMessage) {
sbf.append(str).append("\n");
}
return sbf.toString();
}
5-6.v3支付非同步通知驗證簽名 verifyNotify()
/**
* v3 支付非同步通知驗證簽名
*
* @param body 非同步通知密文
* @param key api 金鑰
* @return 非同步通知明文
* @throws Exception 異常資訊
*/
static String verifyNotify(String body, String key) throws Exception {
// 獲取平臺證書序列號
cn.hutool.json.JSONObject resultObject = JSONUtil.parseObj(body);
cn.hutool.json.JSONObject resource = resultObject.getJSONObject("resource");
String cipherText = resource.getStr("ciphertext");
String nonceStr = resource.getStr("nonce");
String associatedData = resource.getStr("associated_data");
AesUtil aesUtil = new AesUtil(key.getBytes(StandardCharsets.UTF_8));
// 密文解密
return aesUtil.decryptToString(
associatedData.getBytes(StandardCharsets.UTF_8),
nonceStr.getBytes(StandardCharsets.UTF_8),
cipherText
);
}
5-7.處理返回物件 readData()
/**
* 處理返回物件
*
* @param request
* @return
*/
static String readData(HttpServletRequest request) {
BufferedReader br = null;
try {
StringBuilder result = new StringBuilder();
br = request.getReader();
for (String line; (line = br.readLine()) != null; ) {
if (result.length() > 0) {
result.append("\n");
}
result.append(line);
}
return result.toString();
} catch (IOException e) {
throw new RuntimeException(e);
} finally {
if (br != null) {
try {
br.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
6.介面呼叫例項
6-1支付請求
@GetMapping("/wxPay")
public Object wxPay() throws Exception {
//支付的請求引數資訊(此引數與微信支付文件一致,文件地址:https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_1_1.shtml)
WxPayRequestBo wxPayRequestBo = new WxPayRequestBo()
.setAppid(appId)
.setMchid(mercId)
.setDescription("商品描述")
.setOut_trade_no(out_trade_no)
.setTime_expire(time_expire)
.setNotify_url(notify_url)
.setAmount(new Amount().setTotal(1))
.setPayer(new Payer().setOpenid(openId))
.setScene_info(new SceneInfo().setPayer_client_ip(client_ip));
String wxPayRequestJsonStr = JSONUtil.toJsonStr(wxPayRequestBo);
//第一步獲取prepay_id
String prepayId = WxPayV3Util.V3PayGet(url, wxPayRequestJsonStr, mercId, serial_no, privateKeyFilePath);
//第二步獲取調起支付的引數
JSONObject object = JSONObject.fromObject(WxPayV3Util.WxTuneUp(prepayId, appId, privateKeyFilePath));
return object;
}
6-2.非同步回撥
@RequestMapping(value = "/wxnoty", method = {org.springframework.web.bind.annotation.RequestMethod.POST, org.springframework.web.bind.annotation.RequestMethod.GET})
public void wxnoty(HttpServletRequest request, HttpServletResponse response) throws Exception {
System.err.println("小程式支付非同步訊息");
String out_trade_no = WxPayV3Util.notify(request, response, privateKey);
System.out.println(out_trade_no);
}