微信支付API_v3+Native扫码支付

前言:

这周做了个微信支付的需求,然后看了微信支付的开发文档可以说是一言难尽啊...

                也走了很多弯路踩了很多很多坑,在网上搜索感觉都很乱也没有一份具体的代码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;
    }

总结:

这里微信文档上的东西我就没有给大家看了,很乱,但是步骤就是我这个步骤,希望能帮助到大家,如果有什么不懂的评论区问就行了,谢谢

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值