前言:
这周做了个微信支付的需求,然后看了微信支付的开发文档可以说是一言难尽啊...
也走了很多弯路踩了很多很多坑,在网上搜索感觉都很乱也没有一份具体的代码demo,今天为大家写一份从生成二维码到回调的实操代码,希望对大家有帮助
准备接入前准备:
我们进入微信支付的官网可以看到:我们需要申请的参数,设置的密钥等,我这里选择的是API_v3的Native支付,5个需要用到的参数就是:APPID,商户号, 商户API证书序列号,API_v3的密钥以及我们申请的密钥的地址。这里我不多说你跟着文档申请走就行了:
重点就是申请完证书后,我们会得到一个文件夹,那里面就是我们的密钥,这个东西非常重要,一定要保存好,生产环境最好放在服务器里面去读取
我们将得到的证书文件解压大概会得到这样三个文件
apiclient_key.pem这个文件就是我们的密钥了,在传参时候我们需要用它加载密钥。
拿到完后我们开始写获取二维码的代码:
我们先引入pom文件,这里我建议引入0.4.0以上的pom,因为他对后面的自动更新平台证书功能做了简洁,如果0.3版本好像要麻烦一点
<!--wxchat_pay-->
<dependency>
<groupId>com.github.wechatpay-apiv3</groupId>
<artifactId>wechatpay-apache-httpclient</artifactId>
<version>0.4.2</version>
</dependency>
我们先定义一个微信支付的wx_pay.properties放在resources目录下:
# 商户号
wxpay.mch-id=1651xxx7xx
# appid
wxpay.appid=ww445bxxxxxx5a46bf1
# 申请私钥的绝对位置
wxpay.private-key-path=D:\\MyAPP\\company_vxpay\\wx_pay\\apiclient_key.pem
# api_v3密码
wxpay.api-v3-key=xxxxxxxxxxxxxxx
# 商户API证书序列号
wxpay.mch-serial-no=2xxxxxxxxxxxxxDBF5245996A5E176A
需要定义一个配置类来读取刚才的properties文件:
import com.wechat.pay.contrib.apache.httpclient.WechatPayHttpClientBuilder;
import com.wechat.pay.contrib.apache.httpclient.auth.PrivateKeySigner;
import com.wechat.pay.contrib.apache.httpclient.auth.Verifier;
import com.wechat.pay.contrib.apache.httpclient.auth.WechatPay2Credentials;
import com.wechat.pay.contrib.apache.httpclient.auth.WechatPay2Validator;
import com.wechat.pay.contrib.apache.httpclient.cert.CertificatesManager;
import com.wechat.pay.contrib.apache.httpclient.exception.HttpCodeException;
import com.wechat.pay.contrib.apache.httpclient.exception.NotFoundException;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.utils.URIBuilder;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.util.EntityUtils;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.PropertySource;
import java.io.IOException;
import java.net.URISyntaxException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.security.GeneralSecurityException;
import java.security.KeyFactory;
import java.security.NoSuchAlgorithmException;
import java.security.PrivateKey;
import java.security.cert.X509Certificate;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.PKCS8EncodedKeySpec;
import java.util.Base64;
import java.util.Collections;
import java.util.List;
@Configuration
@PropertySource("classpath:wx_pay.properties") //读取配置文件
@ConfigurationProperties(prefix="wxpay") //读取wxpay节点
@Data
@Slf4j
public class WxPayTestConfig {
// 商户号
private String mchId;
// 商户API证书序列号
private String mchSerialNo;
// 商户私钥文件
private String privateKeyPath;
// APIv3密钥
private String apiV3Key;
// APPID
private String appid;
/**
* 获取http请求对象 这里我们需要通过这个对象向微信发送请求获取code_url
* @param
* @return
*/
public CloseableHttpClient getWxPayClient() throws IOException {
try {
//根据地址构建私钥
PrivateKey merchantPrivateKey=
WxPayTestConfig.getPrivateKey(privateKeyPath);
// 定时更新平台证书功能
CertificatesManager
certificatesManager=CertificatesManager.getInstance();
// 向证书管理器增加需要自动更新平台证书的商户信息
certificatesManager.putMerchant(mchId, new WechatPay2Credentials(mchId,
new PrivateKeySigner(mchSerialNo, merchantPrivateKey)),
apiV3Key.getBytes(StandardCharsets.UTF_8));
// ... 若有多个商户号,可继续调用putMerchant添加商户信息
// 从证书管理器中获取verifier
Verifier verifier = certificatesManager.getVerifier(mchId);
WechatPay2Validator wechatPay2Validator = new
WechatPay2Validator(verifier);
WechatPayHttpClientBuilder builder=WechatPayHttpClientBuilder.create()
.withMerchant(mchId,mchSerialNo,merchantPrivateKey)
.withValidator(new WechatPay2Validator(verifier));
CloseableHttpClient httpClient = builder.build();
return httpClient;
}catch (GeneralSecurityException e){
e.printStackTrace();
}catch (HttpCodeException e){
e.printStackTrace();
}catch (NotFoundException e){
e.printStackTrace();
}
return null;
}
/**
* 获取私钥。
*
* @param filename 私钥文件路径 (required)
* @return 私钥对象
*/
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("无效的密钥格式");
}
}
}
接下来我们开始写controller类,为了方便我这里就没有用service去做了,也没有使用订单服务,大家各自需求和服务不同写的代码也不同,我这只是微信支付的demo,订单服务需要大家自己去加了,我先写怎么获取native code_url的。
@RestController
@RequestMapping("weixinpay")
@Slf4j
public class WxPayController {
@Autowired
private WxPayTestConfig wxPayTestConfig;
private String REQUEST_METHOD="POST";
private final ReentrantLock lock = new ReentrantLock();
private static String NOTIFY_URL ="https://xxxxxxx/weixinpay/native/callback"; //回调地址 暂时内网穿透
private static String CURRENCY = "CNY"; //微信支付单位 人民币
private static String CODE_URL = "code_url"; //微信native返回参数
private static String REQUEST_URL = "https://api.mch.weixin.qq.com/v3/pay/transactions/native"; //native获取微信支付二维码连接
@PostMapping("/createNative")
public Object createNative( Float money) throws Exception {
log.info("发起微信支付请求APIv3版,支付金额:" + money);
if (ObjectUtil.isNull(money)) {
throw new IllegalAccessException();
}
//只支持rmb 不用兑换成美金
BigDecimal rmbAmount = new BigDecimal(money.toString());
//BigDecimal usdAmount=exchangeRate.multiply(new BigDecimal(money.toString())).setScale(5, BigDecimal.ROUND_DOWN);
String orderNo =UUID.randomUUID().toString().replaceAll("-", "").substring(0, 20);
log.info("本次订单号为 :"+orderNo);
String subject = "在线充值" + money + "元";
float totalFee = (money * 100);
//组参 wxPayTestConfig.getAppid() wxPayTestConfig.getMchId() description out_trade_no notify_url amount{ total currency}
Map<String, Object> paramMap = new HashMap<>();
paramMap.put("appid", wxPayTestConfig.getAppid());
paramMap.put("mchid", wxPayTestConfig.getMchId());
//商品描述
paramMap.put("description", subject);
//商户订单号
paramMap.put("out_trade_no", orderNo);
//回调地址 必须是https
paramMap.put("notify_url", NOTIFY_URL);
Map<String, Object> amountMap = new HashMap<>();
amountMap.put("total", Integer.valueOf((int) totalFee));
amountMap.put("currency", CURRENCY);
//String jsonString = JSON.toJSONString(amountMap);
paramMap.put("amount", amountMap);
String jsonParams = JSON.toJSONString(paramMap);
//获取httpclient客户端 发送post请求
HttpPost httpPost = new HttpPost(REQUEST_URL);
CloseableHttpClient httpClient = wxPayTestConfig.getWxPayClient();
StringEntity se = new StringEntity(jsonParams, "utf-8");
//http请求头设置的Authorization 当前时间戳
long timestmap = System.currentTimeMillis()/ 1000;;
String rendom =UUID.randomUUID().toString().replaceAll("-", "").substring(0, 20);
HttpUrl url = HttpUrl.parse(REQUEST_URL);
String token = getToken(url, timestmap, rendom, jsonParams);
String lastToken="WECHATPAY2-SHA256-RSA2048"+" "+token;
log.info("请求Token 为: "+token);
log.info("执行微信支付请求发送,参数为 : "+ se.toString());
// 请求体body 请求头的token accept和content_type
httpPost.setEntity(se);
httpPost.setHeader("User-Agent", "Mozilla/4.0 (compatible; MSIE 5.0; Windows NT; DigExt)");
//httpPost.setHeader(HttpHeaders.AUTHORIZATION, lastToken);
httpPost.setHeader(HttpHeaders.ACCEPT, "application/json");
httpPost.setHeader(HttpHeaders.CONTENT_TYPE, "application/json");
CloseableHttpResponse httpResponse = httpClient.execute(httpPost);
Integer statusCode = httpResponse.getStatusLine().getStatusCode();
HttpEntity entity = httpResponse.getEntity();
String res = EntityUtils.toString(entity);
if (!statusCode.equals(200) || res.isEmpty()) {
log.error("微信支付统一下单错误 ===> {} ", statusCode);
throw new RuntimeException("微信支付统一下单错误");
}
JSONObject jsonObject = new JSONObject(res);
String codeUrl = String.valueOf(jsonObject.get(CODE_URL));
Map<String, Object> responseMap = new HashMap<>();
responseMap.put("codeUrl", codeUrl);
responseMap.put("orderNo", orderNo);
return responseMap;
}
//需要签名的字符串
private String getSignature(HttpUrl url,long timestamp, String random, String body) {
String canonicalUrl = url.encodedPath();
return REQUEST_METHOD+"\n"
+ canonicalUrl+ "\n"
+ timestamp + "\n"
+ random + "\n"
+ body + "\n";
}
//根据密钥拿到签名串
private String sign(byte[] message){
try {
Signature sign = Signature.getInstance("SHA256withRSA");
sign.initSign(wxPayTestConfig.getPrivateKey(wxPayTestConfig.getPrivateKeyPath()));
sign.update(message);
return Base64.getEncoder().encodeToString(sign.sign());
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} catch (SignatureException e) {
e.printStackTrace();
} catch (InvalidKeyException e) {
e.printStackTrace();
}
return null;
}
//请求头里的Authorization
private String getToken(HttpUrl url,long timestamp,String random, String body) {
try {
String message = getSignature(url,timestamp, random,body);
String signature = sign(message.getBytes("utf-8"));
return "mchid=\"" + wxPayTestConfig.getMchId() + "\","
+ "nonce_str=\"" + random + "\","
+ "timestamp=\"" + timestamp + "\","
+ "serial_no=\"" + wxPayTestConfig.getMchSerialNo() + "\","
+ "signature=\"" + signature + "\"";
}catch (IOException e){
e.printStackTrace();
}
return null;
}
}
这里我踩了个大坑跟大家说一下:
微信文档写的很乱我在写请求这部分的时候文档居然要设置签名头Authorization,还需要几个必要参数,什么随机值,当前时间戳啊,我写完请求头发送请求竟然<400 BAD REQUEST >什么nginx的错了,搞了我快一下午才发现,在config的getWxPayClient方法中,我们拿到的http请求对象已经是设置了Authorization的,我们在给他设置导致的非法请求,不得不骂一句真的太坑了,所以其实这段代码的下面三个方法 getSignature,sign和getToken完全是可有可无的。在启动看效果前还有要注意的,微信支付的回调必须是https的这里就需要大家在测试前准备好内网穿透工具了,奉劝各位不要直接上生产测试,否则扣了工资别骂我,我这里用的是花生壳,下载好之后还要实名,然后在他官网配置你的https(这里需要6元钱购买)。
启动好项目后我们还需cmd输入ipconfig /all命令查看自己的地址 :
花生壳再配置你自己服务的端口,我这是8080到这我们就映射好了:
接下来我们就可以用postman做测试,我用的这个网站,能将微信返回的url转为二维码这样就可以做扫码支付测试回调的功能了,成功页面:
最后写我们的的回调方法:
回调这里我也写了很久,因为文档有很多步骤,但是又写的很乱导致很久家。但其实最后总结就三步,第一:拿定时更新的平台证书,拿出他的序列号跟回调请求头的序列号做对比,第二:拿这个证书的公钥去做验签,第三就是解密返回来的参数。这里的坑就是在验签传body的时候注意顺序,建议用 String responseBody = stringBuffer.toString();去拿参数。
@PostMapping("/native/callback")
public Object callback(HttpServletRequest request, HttpServletResponse response) throws Exception {
String serial = request.getHeader("Wechatpay-Serial");
log.info("回调得到证书序列号为 : "+serial);
//先验签是否来自微信 时间戳 应答随机串
String timestamp = request.getHeader("Wechatpay-Timestamp");
String nonce = request.getHeader("Wechatpay-Nonce");
String signature = request.getHeader("Wechatpay-Signature");
log.info("微信支付回调请求头读取,timestamp为: "+timestamp+" nonce为: "+nonce+" signature为: "+signature);
//读取请求体数据
ServletInputStream inputStream = request.getInputStream();
StringBuffer stringBuffer = new StringBuffer();
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream));
String s;
//读取回调请求体
while ((s = bufferedReader.readLine()) != null) {
stringBuffer.append(s);
}
String responseBody = stringBuffer.toString();
log.info("微信支付回调请求体读取,结果为: "+responseBody);
X509Certificate verifier = getVerifier();
//拿到平台证书
String realSerial = verifier.getSerialNumber().toString(16).toUpperCase();
log.info("实时请求拿到的公众平台序列号realSerial为: "+realSerial);
if (! realSerial.equals(serial)){
return CommonResponse.failed(500,"验证平台序列号失败");
}
//使用官方验签工具进行验签
boolean weChatPayReplySign = getWeChatPayReplySign(verifier,timestamp, nonce, responseBody, signature);
if (! weChatPayReplySign){
log.error("微信支付验签失败");
return CommonResponse.failed(500,"验签失败");
}
Map requestMap = (Map) JSON.parse(responseBody);
//开始按照验签进行拼接
String id = String.valueOf(requestMap.get("id"));
String resource = String.valueOf(requestMap.get("resource"));
Map requestMap2 = (Map) JSON.parse(resource);
log.info("微信支付解密出得到 id 为:"+id,"requestMap2 为:"+requestMap2.toString());
String bodyAssociated_data = String.valueOf(requestMap2.get("associated_data"));
String bodyNonce = String.valueOf( requestMap2.get("nonce"));
String bodyCiphertext = String.valueOf(requestMap2.get("ciphertext"));
log.info("微信支付解密出得到 bodyAssociated_data 为:"+bodyAssociated_data," bodyNonce 为:"+bodyNonce+" bodyCiphertext 为:"+bodyCiphertext);
//开始解密参数
AesUtil aesUtil = new AesUtil(wxPayTestConfig.getApiV3Key().getBytes(StandardCharsets.UTF_8));
String aes = aesUtil.decryptToString(bodyAssociated_data.getBytes(StandardCharsets.UTF_8), bodyNonce.getBytes(StandardCharsets.UTF_8), bodyCiphertext);
Map decryptMap = (Map) JSON.parse(aes);
//获取商户订单号
String orderNo =String.valueOf(decryptMap.get("out_trade_no"));
log.info("回调解密得到orderNo 为:"+orderNo);
log.info("解密resources 得到的回调参数 aes为: "+aes);
if(lock.tryLock()){
try {
//这里处理你的订单逻辑 一定要加锁!!!!
} finally {
//要主动释放锁
lock.unlock();
}
}
return CommonResponse.success();
}
private boolean getWeChatPayReplySign(X509Certificate validCertificate,String weChatPayTimestamp, String weChatPayNonce, String body, String weChatPaySignature){
try {
String str = Stream.of(weChatPayTimestamp, weChatPayNonce, body).collect(Collectors.joining("\n", "", "\n"));
log.info("验签流程 得到str为 "+ str);
Signature sign = Signature.getInstance("SHA256withRSA");
sign.initVerify(validCertificate.getPublicKey());
sign.update(str.getBytes(StandardCharsets.UTF_8));
// 解码后的字符串
byte[] decode = Base64.getDecoder().decode(weChatPaySignature.getBytes(StandardCharsets.UTF_8));
return sign.verify(decode);
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
} catch (SignatureException e) {
e.printStackTrace();
} catch (InvalidKeyException e) {
e.printStackTrace();
}
return false;
}
private X509Certificate getVerifier(){
X509Certificate validCertificate = null;
try {
PrivateKey merchantPrivateKey = wxPayTestConfig.getPrivateKey(wxPayTestConfig.getPrivateKeyPath());
// 定时更新平台证书功能
CertificatesManager certificatesManager=CertificatesManager.getInstance();
// 向证书管理器增加需要自动更新平台证书的商户信息
certificatesManager.putMerchant(wxPayTestConfig.getMchId(), new WechatPay2Credentials(wxPayTestConfig.getMchId(),
new PrivateKeySigner(wxPayTestConfig.getMchSerialNo(), merchantPrivateKey)), wxPayTestConfig.getApiV3Key().getBytes(StandardCharsets.UTF_8));
// 从证书管理器中获取verifier
Verifier verifier = certificatesManager.getVerifier(wxPayTestConfig.getMchId());
//拿到平台证书
validCertificate=verifier.getValidCertificate();
} catch (GeneralSecurityException e) {
e.printStackTrace();
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
} catch (NotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} catch (HttpCodeException e) {
e.printStackTrace();
}
return validCertificate;
}
总结:
这里微信文档上的东西我就没有给大家看了,很乱,但是步骤就是我这个步骤,希望能帮助到大家,如果有什么不懂的评论区问就行了,谢谢