最近在折腾微信支付,证书仍是比较烦人的,因此有必要分享一些经验,减小你在开发微信支付时的踩坑。目前微信支付的API已经发展到V3版本,采用了流行的Restful风格。java
今天来分享微信支付的难点——签名,虽然有不少好用的SDK可是若是你想深刻了解微信支付仍是须要了解一下的。算法
为了保证资金敏感数据的安全性,确保咱们业务中的资金往来交易万无一失。目前微信支付第三方签发的权威的CA证书(API证书)中提供的私钥来进行签名。经过商户平台你能够设置并获取API证书。spring
切记在第一次设置的时候会提示下载,后面就再也不提供下载了,具体参考说明。segmentfault
设置后找到zip
压缩包解压,里面有不少文件,对于JAVA开发来讲只须要关注apiclient_cert.p12
这个证书文件就好了,它包含了公私钥
,咱们须要把它放在服务端并利用Java解析.p12
文件获取公钥私钥。api
务必保证证书在服务器端的安全,它涉及到资金安全。
接下来就是证书的解析了,证书的解析有网上不少方法,这里我使用比较“正规”的方法来解析,利用JDK安全包的java.security.KeyStore
来解析。数组
微信支付API证书使用了PKCS12
算法,咱们经过KeyStore
来获取公私钥对的载体KeyPair
以及证书序列号serialNumber
,我封装了工具类:安全
import org.springframework.core.io.ClassPathResource; import java.security.KeyPair; import java.security.KeyStore; import java.security.PrivateKey; import java.security.PublicKey; import java.security.cert.X509Certificate; /** * KeyPairFactory * * @author dax * @since 13:41 **/ public class KeyPairFactory { private KeyStore store; 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); } } }
眼熟的能够看出是胖哥Spring Security教程中JWT用的公私钥提取方法的修改版本,你能够对比下不一样之处。
这个方法中有三个参数,这里必需要说明一下:服务器
keyPath
API证书apiclient_cert.p12
的classpath
路径,通常咱们会放在resources
路径下,固然你能够修改获取证书输入流的方式。keyAlias
证书的别名,这个微信的文档是没有的,胖哥经过加载证书时进行DEBUG获取到该值固定为Tenpay Certificate
。keyPass
证书密码,这个默认就是商户号,在其它配置中也须要使用就是mchid
,就是你用超级管理员登陆微信商户平台在我的资料中的一串数字。微信支付V3版本的签名是咱们在调用具体的微信支付的API时在HTTP请求头中携带特定的编码串供微信支付服务器进行验证请求来源,确保请求是真实可信的。微信
签名串的具体格式,一共五行一行也不能少,每一行以换行符n
结束。app
HTTP请求方法n URLn 请求时间戳n 请求随机串n 请求报文主体n
POST
。https://api.mch.weixin.qq.com/v3/pay/transactions/app
,除去域名部分获得参与签名的URL。若是请求中有查询参数,URL末尾应附加有'?'和对应的查询字符串。这里为/v3/pay/transactions/app
。System.currentTimeMillis() / 1000
获取便可。593BEC0C930BF1AFEB40B4A08C8FB242
的字符串就好了。""
;当请求方法为POST
或PUT
时,请使用真实发送的JSON
报文。图片上传API,请使用meta
对应的JSON
报文。而后咱们使用商户私钥对按照上面格式的待签名串进行SHA256 with RSA签名,并对签名结果进行Base64编码获得签名值。对应的核心Java代码为:
/** * 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 String sign(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
放置到对应HTTP请求的Authorization
请求头中,格式为:
Authorization: WECHATPAY2-SHA256-RSA2048 {Token}
Token
由如下五部分组成:
mchid
serial_no
,用于[声明所使用的证书]()nonce_str
timestamp
signature
Token
生成的核心代码:
/** * 生成Token. * * @param mchId 商户号 * @param nonceStr 随机字符串 * @param timestamp 时间戳 * @param serialNo 证书序列号 * @param signature 签名 * @return the string */ String token(String mchId, String nonceStr, long timestamp, String serialNo, String signature) { final String TOKEN_PATTERN = "mchid="%s",nonce_str="%s",timestamp="%d",serial_no="%s",signature="%s""; // 生成token return String.format(TOKEN_PATTERN, wechatPayProperties.getMchId(), nonceStr, timestamp, serialNo, signature); }
将生成的Token
按照上述格式放入请求头中便可完成签名的使用。
本文咱们对微信支付V3版本的难点签名以及签名的使用进行了完整的分析,同时对API证书的解析也进行了讲解,相信可以帮助你在支付开发中解决一些具体的问题。后面有时间我还将对签名的验证进行讲解,关注:码农小胖哥 及时获取系列知识。
关注公众号:Felordcn 获取更多资讯