白嫖之微信支付V3 java restful

前言

疫情创业失败,回来打工了。。最近所在公司新做了一个小程序,涉及到支付功能,一看文档,居然出了新的v3 restful的API(原谅我很久没写代码了),记得很久之前写过v2的xml格式的,很是痛苦。最后决定用新的方式调用,搞了两天,终于通了,确实比老版的简单,但网上的整体代码文档很少,在这里跟大家分享下,供大家白嫖。

从未写过的同学,一步一步跟着走即可实现小程序微信支付V3统一下单、支付回调、验签功能。
微信支付调用流程很简单,大家看文档学习即可。
官方文档

新版支付核心概念-非对称加密

为啥要加密,这里不解释了。。大家应该都知道。

大家只要记住下面八个概念即可
1、四个动作,加密解密,签名验签(签名也是加过密的)
2、四个钥匙,我的公私钥,微信的公私钥

公钥是我给大家的,私钥是我自己保存好的,两个钥匙是一对,负责双方的加解密

我方加密为了加密传输信息,对方解密为了解密传输信息
我方签名是在我的请求上,签个名,为了证明来源是我本人
对方验签就是验证我的签名对不对,不对的话说明请求是别人假冒的

对应关系为
我的私钥用来签名,微信用我的公钥验证签名
我用微信的公钥加密传输信息,微信用自己的私钥解密信息
反之
微信用私钥签名,我用微信的公钥验证签名
微信用我得公钥加密信息,我用自己私钥解密信息

所以,我们和微信双方,都各自需要有3个钥匙,自己的私钥,自己的公钥,对方的公钥

大家可能发现了,公钥要发送给别人的公开的,被伪造了怎么办?
于是有了CA,主要作用就是给我们的证书做保证,保证是真。CA也是有更上一层机构去保证,大家有兴趣可以自行阅读。

以上信息请大家仔细阅读并理解,个人认为其为微信支付的关键,搞懂加密流程,其余就是并发与业务处理,暂不在本文章讨论范围。

概念说完了
我们要进入正式的微信支付了

第一步:获取自己的公私钥,把公钥给微信,设置APIv3密钥

我已经默认大家注册了商户号与小程序
登陆商户后台,上面账户中心,左侧API安全
下载微信证书工具,按照步骤即可下载自己的公私钥和证书信息,压缩包如下

然后将生成的公钥,复制粘贴到微信商户后台。至此
我们有了自己的公私钥,微信有自己公私钥和我们的公钥,我们还差一个微信的公钥,后面再讲。

第二步:准备微信支付所需参数

1、将下载的证书apiclient_cert.p12,随便放在项目里,我这里放在了src/main/resources
2、准备以下参数
商户id
自己证书id
小程序appid
支付后微信回调地址
自己设置的apiV3Key

以上参数都可以在小程序后台或者商户后台获取到

第三步:工具类WxPayUtil准备

开始白嫖代码,懒得看直接复制即可

整体工具类在附录

1、加载证书获取公私钥,即我们复制在src/main/resources下的apiclient_cert.p12

	private KeyStore store;
	private final Object lock = new Object();
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();
	            // 证书的序列号
//	            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);
	        }
	    }

2、随机串

	 private static final String SYMBOLS = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
	 private static final Random RANDOM = new SecureRandom();
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);
	    }

3、时间戳

long timestamp = System.currentTimeMillis()/1000;

4、请求参数body体,封装了三个DTO,大家可自行修改增加字段

public class WxPrePayMainDTO 
 
    private String appid;
    private String mchid;
    private String description;
    private String out_trade_no;
    private String attach;
    private String notify_url;
    private WxPrePayAmountDTO amount;
    private WxPrePayPayerDTO payer;
public class WxPrePayAmountDTO 
	//单位是分
	private Integer total;
	private String currency = "CNY";
public class WxPrePayPayerDTO 

	private String openid;

至此我们基本上是准备完成了

第四步:签名、token、测试下载证书接口

签名规则请仔细阅读文档,个人觉得这个文档还是很详细的。
签名生成

1、签名方法

@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());
	    }

我们最后需要的是token

2、用签名生成token(规则请参考微信文档)

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);
	    }

3、先用get方法试一下,ok不ok
试下这个接口,下载证书

public static void main(String[] args) {
	    	WxPayUtil KeyPairFactory = new WxPayUtil();
	    	KeyPair createPKCS12 = KeyPairFactory.createPKCS12("/apiclient_cert.p12", "Tenpay Certificate", "商户号");
	    	String nonceStr = KeyPairFactory.generateNonceStr();
	    	long timestamp = System.currentTimeMillis()/1000;
	    	String sign = KeyPairFactory.requestSign("GET", "/v3/certificates", 
	    			timestamp, nonceStr, 
	    			""
	    			, createPKCS12);
	    	String token = KeyPairFactory.token("商户号", nonceStr, timestamp,
	    			"证书号", sign);
	    	
	    	System.out.println(token);
		}

将生成的token,放在header里,如果返回401说明签名失败,如果配置没有问题,则200
的

第五步:下单,返给前端签名信息

官方文档

@Override
	public ResponseResult wxToPay(String userToken,Order order) {
		UserVO userVO = userService.findUserVOByToken(userToken);
		if(userVO==null){
			return ResponseResult.failure(ResponseResultEnum.no_user);
		}
		order.setUid(userVO.getId());
		order.setDescription("爱心捐赠");
		order.setCode(this.createOrderCode());//自己实现业务订单
		//建造者模式构建参数
		WxPrePayMainDTO wxPrePayMainDTO = new WxPrePayDTOBuilder()
				.appid(appid)
				.mchid(mchid)
				.description(order.getDescription())
				.out_trade_no(order.getCode())
				.attach("")
				.notify_url("https://www.baidu.com/")
				.amount(new WxPrePayAmountDTO(order.getAmount().multiply(new BigDecimal(100))))
				.payer(new WxPrePayPayerDTO(userVO.getOpenid()))
				.build();
		//获取token
    	String token = this.getToken(JSON.toJSONString(wxPrePayMainDTO), "POST", "/v3/pay/transactions/jsapi");
    	HttpHeaders headers = new HttpHeaders();
        headers.set("Content-Type","application/json");
        headers.set("Authorization", token);
        System.out.println(JSON.toJSONString(wxPrePayMainDTO));
        System.out.println(token);
        HttpEntity<String> request = new HttpEntity<String>(
        		JSON.toJSONString(wxPrePayMainDTO), headers);
        try {
        	JSONObject response = restTemplate.postForObject("https://api.mch.weixin.qq.com/v3/pay/transactions/jsapi", 
            		request, JSONObject.class);
			String prepay_id = response.getString("prepay_id");
			if(null == prepay_id) {
				return ResponseResult.failure(ResponseResultEnum.pre_pay_error);
			}
        	//需要再次签名,返给前端用来唤起支付
        	System.out.println(response);
        	orderMapper.insert(order);
        	AwakenPayDTO awakenPayDTO = this.getAwakenPaySign("prepay_id="+prepay_id);
        	awakenPayDTO.setOrderCode(order.getCode());
        	return ResponseResult.success(awakenPayDTO);
        } catch (HttpStatusCodeException e) {
        	System.out.println( e.getStatusCode());
        	System.out.println( e.getResponseBodyAsString());
        	System.out.println( e.getResponseHeaders());
        }
    	return ResponseResult.failure(ResponseResultEnum.pre_pay_error);
	}
private String getToken(String body,String method,String url) {
		//1.加载证书
		KeyPair keyPair = wxPayUtil.createPKCS12("/apiclient_cert.p12", 
				"Tenpay Certificate", mchid);
		String nonceStr = wxPayUtil.generateNonceStr();
    	long timestamp = System.currentTimeMillis()/1000;
    	System.out.println(url);
    	//2.获取签名
    	String sign = wxPayUtil.requestSign(method, url, timestamp, nonceStr, 
    			body,keyPair);
    	//3.封装token
		String token = wxPayUtil.token(mchid,nonceStr,timestamp,certno, sign);
		return token;
	}
	
	private AwakenPayDTO getAwakenPaySign(String body) {
		//1.加载证书
		KeyPair keyPair = wxPayUtil.createPKCS12("/apiclient_cert.p12", 
				"Tenpay Certificate", mchid);
		AwakenPayDTO awakenPayDTO = new AwakenPayDTO();
		String nonceStr = wxPayUtil.generateNonceStr();
    	long timestamp = System.currentTimeMillis()/1000;
    	//2.获取签名
    	String sign = wxPayUtil.awakenPaySign(appid, timestamp, nonceStr, body, keyPair);
    	//3.封装token
    	awakenPayDTO.setNonceStr(nonceStr);
    	awakenPayDTO.setPackageInfo(body);
    	awakenPayDTO.setSignType("RSA");
    	awakenPayDTO.setTimeStamp(timestamp);
    	awakenPayDTO.setPaySign(sign);
		return awakenPayDTO;
	}
public class AwakenPayDTO 
	
	private long timeStamp;
	private String nonceStr;
	private String packageInfo;
	private String signType;
	private String paySign;
	private String orderCode;

剩下的就是前端的事情啦

附录

import java.nio.charset.StandardCharsets;
import java.security.GeneralSecurityException;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.KeyPair;
import java.security.KeyStore;
import java.security.NoSuchAlgorithmException;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.SecureRandom;
import java.security.Signature;
import java.security.cert.X509Certificate;
import java.util.Random;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import javax.crypto.Cipher;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.spec.GCMParameterSpec;
import javax.crypto.spec.SecretKeySpec;

import org.springframework.core.io.ClassPathResource;
import org.springframework.stereotype.Component;
import org.springframework.util.Base64Utils;

import lombok.SneakyThrows;
@Component
public class WxPayUtil {
	
	 private KeyStore store;
	 private static final String SYMBOLS = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
	 private static final Random RANDOM = new SecureRandom();
	 private final Object lock = new Object();
	    /**
	     * 获取公私钥.
	     *
	     * @param keyPath  the key path
	     * @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();
	            // 证书的序列号
//	            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       请求方法  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    当前时间戳   因为要配置到TOKEN 中所以 签名中的要跟TOKEN 保持一致
	     * @param nonceStr     随机字符串  要和TOKEN中的保持一致
	     * @param body         请求体 GET 为 "" POST 为JSON
	     * @param keyPair      商户API 证书解析的密钥对  实际使用的是其中的私钥
	     * @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 商户号
	     * @param nonceStr   随机字符串 
	     * @param timestamp  时间戳
	     * @param serialNo   证书序列号
	     * @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);
	    }
	    
	    /**
	     * 解密响应体.
	     *
	     * @param apiV3Key       API V3 KEY  API v3密钥 商户平台设置的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);
	        }
	    }
	    
	    
	    public static void main(String[] args) {
	    	WxPayUtil KeyPairFactory = new WxPayUtil();
	    	KeyPair createPKCS12 = KeyPairFactory.createPKCS12("/apiclient_cert.p12", "Tenpay Certificate", "商户号");
	    	String nonceStr = KeyPairFactory.generateNonceStr();
	    	long timestamp = System.currentTimeMillis()/1000;
	    	String sign = KeyPairFactory.requestSign("GET", "/v3/certificates", 
	    			timestamp, nonceStr, 
	    			""
	    			, createPKCS12);
	    	String token = KeyPairFactory.token("商户号", nonceStr, timestamp,
	    			"证书号", sign);
	    	
	    	System.out.println(token);
		}
	    
}

微信支付我只写了两天,比较匆忙,希望能给大家帮助

  • 10
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 6
    评论
微信开发使用的cert,里面微信支付API共四份(证书pkcs12格式、证书pem格式、证书密钥pem格式、CA证书),为接口中强制要求时需携带的证书文件。 证书属于敏感信息,请妥善保管不要泄露和被他人复制。 不同开发语言下的证书格式不同,以下为说明指引: 证书pkcs12格式(apiclient_cert.p12) 包含了私钥信息的证书文件,为p12(pfx)格式,由微信支付签发给您用来标识和界定您的身份 部分安全性要求较高的API需要使用该证书来确认您的调用身份 windows上可以直接双击导入系统,导入过程中会提示输入证书密码,证书密码默认为您的商户ID(如:10010000) 证书pem格式(apiclient_cert.pem) 从apiclient_cert.p12中导出证书部分的文件,为pem格式,请妥善保管不要泄漏和被他人复制 部分开发语言和环境,不能直接使用p12文件,而需要使用pem,所以为了方便您使用,已为您直接提供 您也可以使用openssl命令来自己导出:openssl pkcs12 -clcerts -nokeys -in apiclient_cert.p12 -out apiclient_cert.pem 证书密钥pem格式(apiclient_key.pem) 从apiclient_cert.p12中导出密钥部分的文件,为pem格式 部分开发语言和环境,不能直接使用p12文件,而需要使用pem,所以为了方便您使用,已为您直接提供 您也可以使用openssl命令来自己导出:openssl pkcs12 -nocerts -in apiclient_cert.p12 -out apiclient_key.pem CA证书(rootca.pem) 微信支付api服务器上也部署了证明微信支付身份的服务器证书,您在使用api进行调用时也需要验证所调用服务器及域名的真实性 该文件为签署微信支付证书的权威机构的根证书,可以用来验证微信支付服务器证书的真实性 某些环境和工具已经内置了若干权威机构的根证书,无需引用该证书也可以正常进行验证,这里提供给您在未内置所必须根证书的环境中载入使用
评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值