评论

微信支付V3版java整合

java整合微信支付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);
    }

文件秘钥截图:

最后一次编辑于  01-12  
点赞 7
收藏
评论

11 个评论

  • 阿布
    阿布
    02-01

    您好,我现在回调对返回参数解密 一直报错 key的问题,参考你的代码做的,能解答下吗 谢谢

    02-01
    赞同 2
    回复
  • 陈世间
    陈世间
    11-20

    想请教一下 证书序列号在哪里拿

    11-20
    赞同
    回复 1
    • 蓝桉
      蓝桉
      11-25
      商户平台支付证书那里
      11-25
      回复
  • ℳঞdear小胖᭄
    ℳঞdear小胖᭄
    10-22

    按照您的写法,支付没问题,但是退款回调 如果还是用您的 4-3.处理微信异步回调 notify()

    这个方法,微信会一直请求回调接口,但是验签是成功的啊,不知道咋回事

    10-22
    赞同
    回复 1
    • .
      .
      11-16
      你肯定没有给他成功的标志
      11-16
      回复
  • 后来啊
    后来啊
    07-30

    有demo吗 按你写的操作下来很多报错

    07-30
    赞同
    回复
  • ......
    ......
    06-24

    楼主能将一下 商户私钥是从哪里得到的吗


    06-24
    赞同
    回复
  • Smile阳😀
    Smile阳😀
    06-14

    请问一下。如果是服务商模式的话里面涉及到的商户 mercId ,是服务商,还是特约商户?

    06-14
    赞同
    回复
  • 歪瑞古德·标特否
    歪瑞古德·标特否
    06-02

    请问楼主证书序列号是什么

    06-02
    赞同
    回复 1
    • ......
      ......
      06-24
      序列号能在商户 api证书那里查看
      06-24
      回复
  • 起个好听的名字
    起个好听的名字
    04-15

    你回调异步没有验证签名啊 ?那别人模仿微信调你的回调接口怎么办?

    04-15
    赞同
    回复 2
    • 彡
      04-15
      他这样做,只能通过apiV3key来保证安全了。
      04-15
      回复
    • 彡
      04-15
      使用微信提供的 autoUpdateCertVerifier.verify来做验签,我感觉是比较好的,也考虑了微信平台证书的更新。
      04-15
      回复
  • 彡
    04-14

    从从request的body读取数据,为何要追加\n 。一般从request的io流读数据都没有加\n 。好奇怪的搞法,这里我都不敢使用@RequestBody 注解的方式来获取body数据了

    04-14
    赞同
    回复 1
    • Swan
      Swan
      04-24
      04-24
      回复
  • 飘过……
    飘过……
    04-06

    privateKeyFilePath这个参数是什么意思老大们

    04-06
    赞同
    回复 3
    • 彡
      04-14
      商户私钥证书的路径
      04-14
      回复
    • ......
      ......
      06-24
      是apiclient_key.pem的路径吗
      06-24
      回复
    • 大星鑫
      大星鑫
      10-14
      回答正确
      10-14
      回复

正在加载...

登录 后发表内容