目錄
????????相關(guān)官網(wǎng)文檔
????????1.需要的參數(shù)
????????2.引入庫(kù)
????????3.用到的工具類
????????4.支付下單實(shí)現(xiàn)
????????5.支付回調(diào)
相關(guān)官網(wǎng)文檔
接入前準(zhǔn)備-小程序支付 | 微信支付商戶平臺(tái)文檔中心
微信支付-JSAPI下單
獲取平臺(tái)證書列表-文檔中心-微信支付商戶平臺(tái)
微信支付-支付通知API
1.需要的參數(shù)
# appId
wechat.appid=${WECHAT_APPID}
# 商戶號(hào)
wechat.mchid=${WECHAT_MCHID}
# 證書序列號(hào)
wechat.mch.certno=${WECHAT_CERTNO}
# APIv3密鑰
wechat.pay.api-v3-key=${WECHAT_V3KEY}
證書下載,apiclient_cert.p12,放到resource目錄下
需要的參數(shù)和證書,根據(jù)接入前準(zhǔn)備官方文檔獲取。
2.引入庫(kù)
<dependency>
<groupId>com.github.wechatpay-apiv3</groupId>
<artifactId>wechatpay-apache-httpclient</artifactId>
<version>0.2.2</version>
</dependency>
3.用到的工具類
import com.wechat.pay.contrib.apache.httpclient.auth.AutoUpdateCertificatesVerifier;
import com.wechat.pay.contrib.apache.httpclient.auth.PrivateKeySigner;
import com.wechat.pay.contrib.apache.httpclient.auth.WechatPay2Credentials;
import lombok.SneakyThrows;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.io.ClassPathResource;
import org.springframework.stereotype.Component;
import org.springframework.util.Base64Utils;
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.*;
import java.security.cert.X509Certificate;
import java.util.Date;
import java.util.Map;
import java.util.Random;
import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Collectors;
import java.util.stream.Stream;
/**
* @author Grace
*/
@Component
public class KeyPairFactory {
@Value("${wechat.mchid}")
private String MCHID;
@Value("${wechat.mch.certno}")
private String CERTNO;
@Value("${wechat.pay.api-v3-key}")
private String V3KEY;
private KeyStore store;
private static final String SYMBOLS = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
private static final Random RANDOM = new SecureRandom();
private final Object lock = new Object();
private static final Map<String, Object> VERIFIER_MAP = new ConcurrentHashMap<>();
/**
* 獲取公私鑰.
*
* @param keyPath API證書apiclient_cert.p12的classpath路徑
* @param keyAlias the key alias
* @param keyPass password
* @return the key pair
*/
public KeyPair createPKCS12(String keyPath, String keyAlias, String keyPass) {
ClassPathResource resource = new ClassPathResource(keyPath);
char[] pem = keyPass.toCharArray();
try {
synchronized (lock) {
if (store == null) {
synchronized (lock) {
store = KeyStore.getInstance("PKCS12");
store.load(resource.getInputStream(), pem);
}
}
}
X509Certificate certificate = (X509Certificate) store.getCertificate(keyAlias);
certificate.checkValidity();
// 證書的序列號(hào)
// String serialNumber = certificate.getSerialNumber().toString(16).toUpperCase();
// 證書的公鑰
PublicKey publicKey = certificate.getPublicKey();
// 證書的私鑰
PrivateKey storeKey = (PrivateKey) store.getKey(keyAlias, pem);
return new KeyPair(publicKey, storeKey);
} catch (Exception e) {
throw new IllegalStateException("Cannot load keys from store: " + resource, e);
}
}
/**
* V3 SHA256withRSA 簽名.
*
* @param method 請(qǐng)求方法 GET POST PUT DELETE 等
* @param canonicalUrl 例如 https://api.mch.weixin.qq.com/v3/pay/transactions/app?version=1 ——> /v3/pay/transactions/app?version=1
* @param timestamp 當(dāng)前時(shí)間戳 因?yàn)橐渲玫絋OKEN 中所以 簽名中的要跟TOKEN 保持一致
* @param nonceStr 隨機(jī)字符串 要和TOKEN中的保持一致
* @param body 請(qǐng)求體 GET 為 "" POST 為JSON
* @param keyPair 商戶API 證書解析的密鑰對(duì) 實(shí)際使用的是其中的私鑰
* @return the string
*/
@SneakyThrows
public String requestSign(String method, String canonicalUrl, long timestamp, String nonceStr, String body, KeyPair keyPair) {
String signatureStr = Stream.of(method, canonicalUrl, String.valueOf(timestamp), nonceStr, body)
.collect(Collectors.joining("\n", "", "\n"));
Signature sign = Signature.getInstance("SHA256withRSA");
sign.initSign(keyPair.getPrivate());
sign.update(signatureStr.getBytes(StandardCharsets.UTF_8));
return Base64Utils.encodeToString(sign.sign());
}
@SneakyThrows
public String awakenPaySign(String appid, long timestamp, String nonceStr, String body, KeyPair keyPair) {
String signatureStr = Stream.of(appid,String.valueOf(timestamp), nonceStr, body)
.collect(Collectors.joining("\n", "", "\n"));
Signature sign = Signature.getInstance("SHA256withRSA");
sign.initSign(keyPair.getPrivate());
sign.update(signatureStr.getBytes(StandardCharsets.UTF_8));
return Base64Utils.encodeToString(sign.sign());
}
/**
* 生成Token.
*
* @param mchId 商戶號(hào)
* @param nonceStr 隨機(jī)字符串
* @param timestamp 時(shí)間戳
* @param serialNo 證書序列號(hào)
* @param signature 簽名
* @return the string
*/
public String token(String mchId, String nonceStr, long timestamp, String serialNo, String signature) {
final String TOKEN_PATTERN = "WECHATPAY2-SHA256-RSA2048 mchid=\"%s\",nonce_str=\"%s\","
+ "timestamp=\"%d\",serial_no=\"%s\",signature=\"%s\"";
// 生成token
return String.format(TOKEN_PATTERN,
mchId,
nonceStr, timestamp, serialNo, signature);
}
public String generateNonceStr() {
char[] nonceChars = new char[32];
for (int index = 0; index < nonceChars.length; ++index) {
nonceChars[index] = SYMBOLS.charAt(RANDOM.nextInt(SYMBOLS.length()));
}
return new String(nonceChars);
}
/**
* 解密響應(yīng)體.
*
* @param apiV3Key API V3 KEY API v3密鑰 商戶平臺(tái)設(shè)置的32位字符串
* @param associatedData response.body.data[i].encrypt_certificate.associated_data
* @param nonce response.body.data[i].encrypt_certificate.nonce
* @param ciphertext response.body.data[i].encrypt_certificate.ciphertext
* @return the string
* @throws GeneralSecurityException the general security exception
*/
public String decryptResponseBody(String apiV3Key, String associatedData, String nonce, String ciphertext) {
try {
Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
SecretKeySpec key = new SecretKeySpec(apiV3Key.getBytes(StandardCharsets.UTF_8), "AES");
GCMParameterSpec spec = new GCMParameterSpec(128, nonce.getBytes(StandardCharsets.UTF_8));
cipher.init(Cipher.DECRYPT_MODE, key, spec);
cipher.updateAAD(associatedData.getBytes(StandardCharsets.UTF_8));
byte[] bytes;
try {
bytes = cipher.doFinal(Base64Utils.decodeFromString(ciphertext));
} catch (GeneralSecurityException e) {
throw new IllegalArgumentException(e);
}
return new String(bytes, StandardCharsets.UTF_8);
} catch (NoSuchAlgorithmException | NoSuchPaddingException e) {
throw new IllegalStateException(e);
} catch (InvalidKeyException | InvalidAlgorithmParameterException e) {
throw new IllegalArgumentException(e);
}
}
/**
* 構(gòu)造驗(yàn)簽名串.
*
* @param wechatpayTimestamp HTTP頭 Wechatpay-Timestamp 中的應(yīng)答時(shí)間戳。
* @param wechatpayNonce HTTP頭 Wechatpay-Nonce 中的應(yīng)答隨機(jī)串
* @param body 響應(yīng)體
* @return the string
*/
public String responseSign(String wechatpayTimestamp, String wechatpayNonce, String body) {
return Stream.of(wechatpayTimestamp, wechatpayNonce, body)
.collect(Collectors.joining("\n", "", "\n"));
}
public AutoUpdateCertificatesVerifier getVerifier() {
AutoUpdateCertificatesVerifier verifier = (AutoUpdateCertificatesVerifier) VERIFIER_MAP.get("verifier");
Date notAfter = (Date) VERIFIER_MAP.get("notAfter");
if(null == verifier || null == notAfter || notAfter.before(new Date())){
// 加載證書
KeyPair keyPair = createPKCS12("/apiclient_cert.p12",
"Tenpay Certificate", MCHID);
// 加載官方自動(dòng)更新證書
verifier = new AutoUpdateCertificatesVerifier(new WechatPay2Credentials(MCHID,
new PrivateKeySigner(CERTNO, keyPair.getPrivate())),
V3KEY.getBytes(StandardCharsets.UTF_8));
VERIFIER_MAP.put("verifier", verifier);
VERIFIER_MAP.put("notAfter", verifier.getValidCertificate().getNotAfter());
}
return verifier;
}
}
官方證書驗(yàn)證工具放到了全局變量中,文章來源:http://www.zghlxwxcb.cn/news/detail-521399.html
4.支付下單實(shí)現(xiàn)
@ApiOperation(value = "支付", httpMethod = "POST")
@RequestMapping(value = "/api/pay/order", method = RequestMethod.POST)
@Transactional
public PrePayResponse wxToPay(@Valid @RequestBody PayOrderRequest dto, HttpServletRequest request, HttpServletResponse response) throws Exception {
PrePayResponse res = new PrePayResponse();
// 用戶登錄校驗(yàn)
......
// 獲取用戶(數(shù)據(jù)庫(kù)中的實(shí)體類)
WechatUser user = xxx.getUser();
int fee = 0;
// 得到小程序傳過來的價(jià)格,注意這里的價(jià)格必須為整數(shù),1代表1分,所以傳過來的值必須*100;
if (null != dto.getPrice()) {
double price = dto.getPrice() * 100;
fee = (int) price;
}
// 生成訂單號(hào),商戶系統(tǒng)內(nèi)部訂單號(hào),只能是數(shù)字、大小寫字母_-*且在同一個(gè)商戶號(hào)下唯一
String OutTradeNo = DateUtil.getOrderNum() + String.valueOf(System.currentTimeMillis()).substring(4) + new Random().nextInt(999999999);
// 保存訂單信息
PayOrder order = new PayOrder();
order.setUser(user);
order.setCreatedAt(new Date());
order.setPrice(dto.getPrice());
order.setTradeNo(OutTradeNo);
......
payOrderRepository.save(order);
// 異步接收微信支付結(jié)果通知的回調(diào)地址,通知url必須為外網(wǎng)可訪問的url,不能攜帶參數(shù)。 公網(wǎng)域名必須為https,如果是走專線接入,使用專線NAT IP或者私有回調(diào)域名可使用http
String basePath = request.getScheme() + "://" + request.getServerName() + ":" +
request.getServerPort() + request.getContextPath();
String notify_url = basePath+"/api/anon/pay/notify";
//請(qǐng)求參數(shù)
cn.hutool.json.JSONObject json = new cn.hutool.json.JSONObject();
json.set("appid", APPID);
json.set("mchid", MCHID); // 商戶號(hào)
json.set("description", "test"); // 商品描述,必填
json.set("out_trade_no", OutTradeNo);
json.set("attach", "自定義參數(shù)"); // 附加數(shù)據(jù),在查詢API和支付通知中原樣返回,可作為自定義參數(shù)使用
json.set("notify_url", notify_url); // 回調(diào)地址
cn.hutool.json.JSONObject amount = new cn.hutool.json.JSONObject();
amount.set("total", fee);
amount.set("currency", "CNY");
json.set("amount",amount); // 訂單金額
cn.hutool.json.JSONObject payer = new cn.hutool.json.JSONObject();
payer.set("openid", user.getOpenId()); // 用戶openId
json.set("payer",payer); // 支付者
//獲取token
String nonceStr = keyPairFactory.generateNonceStr();
long timestamp = System.currentTimeMillis()/1000;
res.setNonceStr(nonceStr);
res.setTimeStamp(timestamp);
String wechatToken = this.getToken(JSON.toJSONString(json), "POST", "/v3/pay/transactions/jsapi", nonceStr, timestamp);
Map<String,String> headers = new HashMap<String,String>();
headers.put("Accept","application/json");
headers.put("Content-Type","application/json; charset=utf-8");
headers.put("Authorization",wechatToken);
HttpResponse httpResponse = HttpRequest.post("https://api.mch.weixin.qq.com/v3/pay/transactions/jsapi")
.headerMap(headers, false)
.body(String.valueOf(json))
.timeout(5 * 60 * 1000)
.execute();
String resultBody = httpResponse.body();
com.alibaba.fastjson.JSONObject jsonObject = com.alibaba.fastjson.JSONObject.parseObject(resultBody);
String prepayId = jsonObject.getString("prepay_id");
if(StringUtils.isEmpty(prepayId)){
order.setStatus(-1);
res.setCode(Constants.SC_MSG);
res.setMessage("支付失敗");
return res;
}
res.setPrepayId(prepayId);
res.setSignType("RSA");
// 加載證書
KeyPair keyPair = keyPairFactory.createPKCS12("/apiclient_cert.p12",
"Tenpay Certificate", MCHID);
String paySign = keyPairFactory.awakenPaySign(APPID, timestamp, nonceStr, "prepay_id="+prepayId, keyPair);
res.setPaySign(paySign);
res.setCode(Constants.SC_OK);
return res;
}
private String getToken(String body,String method,String url, String nonceStr, Long timestamp) {
//1.加載證書
KeyPair keyPair = keyPairFactory.createPKCS12("/apiclient_cert.p12", "Tenpay Certificate", MCHID);
//2.獲取簽名
String sign = keyPairFactory.requestSign(method, url, timestamp, nonceStr, body,keyPair);
//3.封裝token
String token = keyPairFactory.token(MCHID, nonceStr, timestamp, CERTNO, sign);
return token;
}
5.支付回調(diào)
@ApiOperation(value = "支付回調(diào)地址")
@RequestMapping(value = "/api/anon/pay/notify", produces = MediaType.APPLICATION_JSON_VALUE)
@ResponseBody
@Transactional
public ResultResponse notify(HttpServletRequest request, HttpServletResponse response) throws Exception {
ResultResponse res = new ResultResponse();
// 從請(qǐng)求頭獲取驗(yàn)簽字段
String Timestamp = request.getHeader("Wechatpay-Timestamp");
String Nonce = request.getHeader("Wechatpay-Nonce");
String Signature = request.getHeader("Wechatpay-Signature");
String Serial = request.getHeader("Wechatpay-Serial");
// 獲取官方驗(yàn)簽工具
AutoUpdateCertificatesVerifier verifier = keyPairFactory.getVerifier();
// 讀取請(qǐng)求體的信息
ServletInputStream inputStream = request.getInputStream();
StringBuffer stringBuffer = new StringBuffer();
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream));
String s;
// 讀取回調(diào)請(qǐng)求體
while ((s = bufferedReader.readLine()) != null) {
stringBuffer.append(s);
}
String s1 = stringBuffer.toString();
Map requestMap = (Map) JSON.parse(s1);
String resource = String.valueOf(requestMap.get("resource"));
Map requestMap2 = (Map) JSON.parse(resource);
String associated_data = requestMap2.get("associated_data").toString();
String nonce = requestMap2.get("nonce").toString();
String ciphertext = requestMap2.get("ciphertext").toString();
//按照文檔要求拼接驗(yàn)簽串
String VerifySignature = Timestamp + "\n" + Nonce + "\n" + s1 + "\n";
// System.out.println("拼接后的驗(yàn)簽串=" + VerifySignature);
//使用官方驗(yàn)簽工具進(jìn)行驗(yàn)簽
boolean verify = verifier.verify(Serial, VerifySignature.getBytes(), Signature);
//判斷驗(yàn)簽的結(jié)果
// System.out.println("驗(yàn)簽結(jié)果:"+verify);
// 驗(yàn)簽成功
// System.out.println("驗(yàn)簽成功后,開始進(jìn)行解密");
com.wechat.pay.contrib.apache.httpclient.util.AesUtil aesUtil = new AesUtil(V3KEY.getBytes());
String aes = aesUtil.decryptToString(associated_data.getBytes(), nonce.getBytes(), ciphertext);
com.alibaba.fastjson.JSONObject jsonObject = com.alibaba.fastjson.JSONObject.parseObject(aes);
if ("SUCCESS".equals(jsonObject.getString("trade_state"))) {
String out_trade_no = jsonObject.getString("out_trade_no");
// 訂單不為空
if (!StringUtils.isEmpty(out_trade_no)) {
//支付成功后的業(yè)務(wù)處理
PayOrder payOrder = payOrderRepository.findByTradeNo(out_trade_no);
payOrder.setStatus(1);
......
res.setCode(Constants.SC_OK);
return res;
}
} else {
// 支付失敗后的操作
}
res.setCode(Constants.SC_OK);
return res;
}
注意:回調(diào)接口不要被攔截文章來源地址http://www.zghlxwxcb.cn/news/detail-521399.html
到了這里,關(guān)于【Java】微信小程序V3支付(后臺(tái))的文章就介紹完了。如果您還想了解更多內(nèi)容,請(qǐng)?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!