公司要搞服务号商户卷功能,所以用到了创建商户号接口,有很多数据需要平台证书单独加密,并且图片上传接口返回数据也需要平台证书验签。
就搞了一下,现在说一下具体细节:
2 看到微信说加密需要 平台证书
这里你要搞清楚:商户号证书 和 平台证书不是一个东西,
商户号证书:是服务商下载的证书
平台证书:微信支付证书
不废话了,直接上获取平台证书代码:
//获取 获取平台证书列表 测试成功
public static void main(String[] args) throws Exception {
//时间戳
long timestamp = System.currentTimeMillis() / 1000;
//随机字符串(用UUID去掉-就行)
String nonce = IdUtil.fastSimpleUUID().toUpperCase();
String body = "";
//拼接要签名的字符串
PrivateKey privateKey = PayKit.getPrivateKey("这里填商户平台私钥证书");
//拼接要签名的字符串
String orgSignText = "GET\n"
+ "/v3/certificates\n"
+ timestamp + "\n"
+ nonce + "\n"
+ body + "\n";
// 生成签名
String sign = RsaKit.encryptByPrivateKey(orgSignText, privateKey);
//要放在HttpHeader中的auth信息
// 获取商户证书序列号 这里填写公钥路径 也就是apiclient_cert.pem这个文件的路径
//证书序列号
X509Certificate certificate = PayKit.getCertificate(FileUtil.getInputStream("填你的apiclient_cert.pem"));
String serialNo = certificate.getSerialNumber().toString(16).toUpperCase();
String auth = "WECHATPAY2-SHA256-RSA2048 "
+ "mchid=\"商户号\",nonce_str=\""
+ nonce + "\",timestamp=\"" + timestamp
+ "\",serial_no=\"" + serialNo + "\",signature=\"" + sign + "\"";
String url = "https://api.mch.weixin.qq.com/v3/certificates";
HashMap tmap = new HashMap();
tmap.put("Authorization",auth);//tmap.put("token","tonken值");
tmap.put("Accept","application/json");
tmap.put("User-Agent","https://zh.wikipedia.org/wiki/User_agent");
String vmsg= httpGet(url,tmap);//获取请求的返回结果
String rs = HttpClientUtil.sendGetRequest(url, null);
System.out.println("获取平台证书"+vmsg);
}
下面是具体用到的工具类:
public class RsaKit {
/**
* 加密算法RSA
*/
private static final String KEY_ALGORITHM = "RSA";
/**
* 私钥签名
*
* @param data 需要加密的数据
* @param privateKey 私钥
* @return 加密后的数据
* @throws Exception 异常信息
*/
public static String encryptByPrivateKey(String data, PrivateKey privateKey) throws Exception {
java.security.Signature signature = java.security.Signature.getInstance("SHA256WithRSA");
signature.initSign(privateKey);
signature.update(data.getBytes(StandardCharsets.UTF_8));
byte[] signed = signature.sign();
return StrUtil.str(Base64.encode(signed));
}
/**
* 从字符串中加载私钥
* 加载时使用的是PKCS8EncodedKeySpec(PKCS#8编码的Key指令)。
*
* @param privateKeyStr 私钥
* @return {@link PrivateKey}
* @throws Exception 异常信息
*/
public static PrivateKey loadPrivateKey(String privateKeyStr) throws Exception {
try {
byte[] buffer = Base64.decode(privateKeyStr);
PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(buffer);
KeyFactory keyFactory = KeyFactory.getInstance(KEY_ALGORITHM);
return keyFactory.generatePrivate(keySpec);
} catch (NoSuchAlgorithmException e) {
throw new Exception("无此算法");
} catch (InvalidKeySpecException e) {
throw new Exception("私钥非法");
} catch (NullPointerException e) {
throw new Exception("私钥数据为空");
}
}
/**
* 公钥验证签名
*
* @param data 需要加密的数据
* @param sign 签名
* @param publicKey 公钥
* @return 验证结果
* @throws Exception 异常信息
*/
public static boolean checkByPublicKey(String data, String sign, PublicKey publicKey) throws Exception {
java.security.Signature signature = java.security.Signature.getInstance("SHA256WithRSA");
signature.initVerify(publicKey);
signature.update(data.getBytes(StandardCharsets.UTF_8));
return signature.verify(Base64.decode(sign.getBytes(StandardCharsets.UTF_8)));
}
}
public class PayKit {
/**
* 获取证书
*
* @param inputStream 证书文件
* @return {@link X509Certificate} 获取证书
*/
public static X509Certificate getCertificate(InputStream inputStream) {
try {
CertificateFactory cf = CertificateFactory.getInstance("X509");
X509Certificate cert = (X509Certificate) cf.generateCertificate(inputStream);
cert.checkValidity();
return cert;
} catch (CertificateExpiredException e) {
throw new RuntimeException("证书已过期", e);
} catch (CertificateNotYetValidException e) {
throw new RuntimeException("证书尚未生效", e);
} catch (CertificateException e) {
throw new RuntimeException("无效的证书", e);
}
}
/**
* 简化的UUID,去掉了横线,使用性能更好的 ThreadLocalRandom 生成UUID
*
* @return 简化的 UUID,去掉了横线
*/
public static String generateStr() {
return IdUtil.fastSimpleUUID();
}
/**
* 构造签名串
*
* @param method {@link RequestMethod} GET,POST,PUT等
* @param url 请求接口 /v3/certificates
* @param timestamp 获取发起请求时的系统当前时间戳
* @param nonceStr 随机字符串
* @param body 请求报文主体
* @return 待签名字符串
*/
public static String buildSignMessage(String method, String url, String timestamp, String nonceStr, String body) {
ArrayList arrayList = new ArrayList<>();
arrayList.add(method);
arrayList.add(url);
arrayList.add(String.valueOf(timestamp));
arrayList.add(nonceStr);
arrayList.add(body);
return buildSignMessage(arrayList);
}
/**
* 构造签名串
*
* @param signMessage 待签名的参数
* @return 构造后带待签名串
*/
public static String buildSignMessage(ArrayList signMessage) {
if (signMessage == null || signMessage.size() <= 0) {
return null;
}
StringBuilder sbf = new StringBuilder();
for (String str : signMessage) {
sbf.append(str).append("\n");
}
System.out.println("待签名字符串内容:" + sbf.toString());
System.out.println("待签名字符串长度:" + sbf.toString().length());
return sbf.toString();
}
/**
* v3 接口创建签名
*
* @param signMessage 待签名的参数
* @param keyPath key.pem 证书路径
* @return 生成 v3 签名
* @throws Exception 异常信息
*/
public static String createSign(String signMessage, String keyPath) throws Exception {
if (StrUtil.isEmpty(signMessage)) {
return null;
}
// 获取商户私钥
PrivateKey privateKey = PayKit.getPrivateKey(keyPath);
// 生成签名
return RsaKit.encryptByPrivateKey(signMessage, privateKey);
}
/**
* 获取商户私钥
*
* @param keyPath 商户私钥证书路径
* @return {@link PrivateKey} 商户私钥
* @throws Exception 异常信息
*/
public static PrivateKey getPrivateKey(String keyPath) throws Exception {
String originalKey = FileUtil.readUtf8String(keyPath);
String privateKey = originalKey
.replace("-----BEGIN PRIVATE KEY-----", "")
.replace("-----END PRIVATE KEY-----", "")
.replaceAll("\\s+", "");
return RsaKit.loadPrivateKey(privateKey);
}
/**
* 获取授权认证信息
*
* @param mchId 商户号
* @param serialNo 商户API证书序列号
* @param nonceStr 请求随机串
* @param timestamp 时间戳
* @param signature 签名值
* @param authType 认证类型,目前为WECHATPAY2-SHA256-RSA2048
* @return 请求头 Authorization
*/
public static String getAuthorization(String mchId, String serialNo, String nonceStr, String timestamp, String signature, String authType) {
Map params = new HashMap<>(5);
params.put("mchid", mchId);
params.put("serial_no", serialNo);
params.put("nonce_str", nonceStr);
params.put("timestamp", timestamp);
params.put("signature", signature);
return authType.concat(" ").concat(createLinkString(params, ",", false, true));
}
/**
* @param params 需要排序并参与字符拼接的参数组
* @param encode 是否进行URLEncoder
* @return 拼接后字符串
*/
public static String createLinkString(Map params, boolean encode) {
return createLinkString(params, "&", encode);
}
/**
* @param params 需要排序并参与字符拼接的参数组
* @param connStr 连接符号
* @param encode 是否进行URLEncoder
* @return 拼接后字符串
*/
public static String createLinkString(Map params, String connStr, boolean encode) {
return createLinkString(params, connStr, encode, false);
}
public static String createLinkString(Map params, String connStr, boolean encode, boolean quotes) {
List keys = new ArrayList<>(params.keySet());
Collections.sort(keys);
StringBuilder content = new StringBuilder();
for (int i = 0; i < keys.size(); i++) {
String key = keys.get(i);
String value = params.get(key);
// 拼接时,不包括最后一个&字符
if (i == keys.size() - 1) {
if (quotes) {
content.append(key).append("=").append('"').append(encode ? urlEncode(value) : value).append('"');
} else {
content.append(key).append("=").append(encode ? urlEncode(value) : value);
}
} else {
if (quotes) {
content.append(key).append("=").append('"').append(encode ? urlEncode(value) : value).append('"').append(connStr);
} else {
content.append(key).append("=").append(encode ? urlEncode(value) : value).append(connStr);
}
}
}
return content.toString();
}
/**
* URL 编码
*
* @param src 需要编码的字符串
* @return 编码后的字符串
*/
public static String urlEncode(String src) {
try {
return URLEncoder.encode(src, CharsetUtil.UTF_8).replace("+", "%20");
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
return null;
}
}
/**
* 构造签名串
*
* @param timestamp 应答时间戳
* @param nonceStr 应答随机串
* @param body 应答报文主体
* @return 应答待签名字符串
*/
public static String buildSignMessage(String timestamp, String nonceStr, String body) {
ArrayList arrayList = new ArrayList<>();
arrayList.add(timestamp);
arrayList.add(nonceStr);
arrayList.add(body);
return buildSignMessage(arrayList);
}
/**
* 公钥加密
*
* @param data 待加密数据
* @param certificate 平台公钥证书
* @return 加密后的数据
* @throws Exception 异常信息
*/
public static String rsaEncryptOAEP(String data, X509Certificate certificate) throws Exception {
try {
Cipher cipher = Cipher.getInstance("RSA/ECB/OAEPWithSHA-1AndMGF1Padding");
cipher.init(Cipher.ENCRYPT_MODE, certificate.getPublicKey());
byte[] dataByte = data.getBytes(StandardCharsets.UTF_8);
byte[] cipherData = cipher.doFinal(dataByte);
return Base64.encode(cipherData);
} catch (NoSuchAlgorithmException | NoSuchPaddingException e) {
throw new RuntimeException("当前Java环境不支持RSA v1.5/OAEP", e);
} catch (InvalidKeyException e) {
throw new IllegalArgumentException("无效的证书", e);
} catch (IllegalBlockSizeException | BadPaddingException e) {
throw new IllegalBlockSizeException("加密原串的长度不能超过214字节");
}
}
/**
* 私钥解密
*
* @param cipherText 加密字符
* @param privateKey 私钥
* @return 解密后的数据
* @throws Exception 异常信息
*/
public static String rsaDecryptOAEP(String cipherText, PrivateKey privateKey) throws Exception {
try {
Cipher cipher = Cipher.getInstance("RSA/ECB/OAEPWithSHA-1AndMGF1Padding");
cipher.init(Cipher.DECRYPT_MODE, privateKey);
byte[] data = Base64.decode(cipherText);
return new String(cipher.doFinal(data), StandardCharsets.UTF_8);
} catch (NoSuchPaddingException | NoSuchAlgorithmException e) {
throw new RuntimeException("当前Java环境不支持RSA v1.5/OAEP", e);
} catch (InvalidKeyException e) {
throw new IllegalArgumentException("无效的私钥", e);
} catch (BadPaddingException | IllegalBlockSizeException e) {
throw new BadPaddingException("解密失败");
}
}
}
public class HttpClientUtil
{
public static final String SunX509 = "SunX509";
public static final String JKS = "JKS";
public static final String PKCS12 = "PKCS12";
public static final String TLS = "TLS";
public static HttpURLConnection getHttpURLConnection(String strUrl)
throws IOException
{
URL url = new URL(strUrl);
HttpURLConnection httpURLConnection = (HttpURLConnection)url
.openConnection();
return httpURLConnection;
}
/**
* 发送HTTP_GET请求
*
* @see 该方法会自动关闭连接,释放资源
* @param reqURL
* 请求地址(含参数)
* @param decodeCharset
* 解码字符集,解析响应数据时用之,其为null时默认采用UTF-8解码
* @return 远程主机响应正文
*/
public static String sendGetRequest(String reqURL, String decodeCharset) {
long responseLength = 0; // 响应长度
String responseContent = null; // 响应内容
HttpClient httpClient = new DefaultHttpClient(); // 创建默认的httpClient实例
HttpGet httpGet = new HttpGet(reqURL); // 创建org.apache.http.client.methods.HttpGet
try {
HttpResponse response = httpClient.execute(httpGet); // 执行GET请求
HttpEntity entity = response.getEntity(); // 获取响应实体
if (null != entity) {
responseLength = entity.getContentLength();
responseContent = EntityUtils.toString(entity, decodeCharset == null ? "UTF-8" : decodeCharset);
EntityUtils.consume(entity); // Consume response content
}
System.out.println("请求地址: " + httpGet.getURI());
System.out.println("响应状态: " + response.getStatusLine());
System.out.println("响应长度: " + responseLength);
System.out.println("响应内容: " + responseContent);
} catch (ClientProtocolException e) {
System.out.println("该异常通常是协议错误导致,比如构造HttpGet对象时传入的协议不对(将'http'写成'htp')或者服务器端返回的内容不符合HTTP协议要求等,堆栈信息如下");
} catch (ParseException e) {
System.out.println(e.getMessage());
} catch (IOException e) {
System.out.println("该异常通常是网络原因引起的,如HTTP服务器未启动等,堆栈信息如下");
} finally {
httpClient.getConnectionManager().shutdown(); // 关闭连接,释放资源
}
return responseContent;
}
/**
* 发送HTTP_POST请求
*
* @see 该方法为
* sendPostRequest(String,String,boolean,String,String)
* 的简化方法
* @see 该方法在对请求数据的编码和响应数据的解码时,所采用的字符集均为UTF-8
* @see 当isEncoder=true
时,其会自动对sendData
中的[中文][|][
* ]等特殊字符进行URLEncoder.encode(string,"UTF-8")
* @param isEncoder
* 用于指明请求数据是否需要UTF-8编码,true为需要
*/
public static String sendPostRequest(String reqURL, String sendData, boolean isEncoder) {
return sendPostRequest(reqURL, sendData, isEncoder, null, null,"application/json");
}
/**
* 发送HTTP_POST请求
*
* @see 该方法会自动关闭连接,释放资源
* @see 当isEncoder=true
时,其会自动对sendData
中的[中文][|][
* ]等特殊字符进行URLEncoder.encode(string,encodeCharset)
* @param reqURL
* 请求地址
* @param sendData
* 请求参数,若有多个参数则应拼接成param11=value11¶m22=value22¶m33=value33的形式后,
* 传入该参数中
* @param isEncoder
* 请求数据是否需要encodeCharset编码,true为需要
* @param encodeCharset
* 编码字符集,编码请求数据时用之,其为null时默认采用UTF-8解码
* @param decodeCharset
* 解码字符集,解析响应数据时用之,其为null时默认采用UTF-8解码
* @return 远程主机响应正文
*/
public static String sendPostRequest(String reqURL, String sendData, boolean isEncoder, String encodeCharset,
String decodeCharset,String contentType) {
String responseContent = null;
HttpClient httpClient = new DefaultHttpClient();
HttpPost httpPost = new HttpPost(reqURL);
// httpPost.setHeader(HTTP.CONTENT_TYPE,
// "application/x-www-form-urlencoded; charset=UTF-8");
httpPost.setHeader(HTTP.CONTENT_TYPE, contentType);
try {
if (isEncoder) {
httpPost.setEntity(new StringEntity(sendData, encodeCharset == null ? "UTF-8" : encodeCharset));
} else {
httpPost.setEntity(new StringEntity(sendData));
}
// 设置请求超时时间
httpClient.getParams().setParameter(CoreConnectionPNames.CONNECTION_TIMEOUT, 5000);
HttpResponse response = httpClient.execute(httpPost);
HttpEntity entity = response.getEntity();
if (null != entity) {
responseContent = EntityUtils.toString(entity, decodeCharset == null ? "UTF-8" : decodeCharset);
EntityUtils.consume(entity);
}
} catch (Exception e) {
System.out.println("与[" + reqURL + "]通信过程中发生异常,堆栈信息如下");
e.printStackTrace();
} finally {
httpClient.getConnectionManager().shutdown();
}
return responseContent;
}
public static HttpsURLConnection getHttpsURLConnection(String strUrl)
throws IOException
{
URL url = new URL(strUrl);
HttpsURLConnection httpsURLConnection = (HttpsURLConnection)url
.openConnection();
return httpsURLConnection;
}
public static String getURL(String strUrl)
{
if (strUrl != null) {
int indexOf = strUrl.indexOf("?");
if (-1 != indexOf) {
return strUrl.substring(0, indexOf);
}
return strUrl;
}
return strUrl;
}
public static String getQueryString(String strUrl)
{
if (strUrl != null) {
int indexOf = strUrl.indexOf("?");
if (-1 != indexOf) {
return strUrl.substring(indexOf + 1, strUrl.length());
}
return "";
}
return strUrl;
}
public static Map queryString2Map(String queryString)
{
if ((queryString == null) || ("".equals(queryString))) {
return null;
}
Map m = new HashMap();
String[] strArray = queryString.split("&");
for (int index = 0; index < strArray.length; index++) {
String pair = strArray[index];
putMapByPair(pair, m);
}
return m;
}
public static void putMapByPair(String pair, Map m)
{
if ((pair == null) || ("".equals(pair))) {
return;
}
int indexOf = pair.indexOf("=");
if (-1 != indexOf) {
String k = pair.substring(0, indexOf);
String v = pair.substring(indexOf + 1, pair.length());
if ((k != null) && (!"".equals(k))) {
m.put(k, v);
}
} else {
m.put(pair, "");
}
}
public static String bufferedReader2String(BufferedReader reader)
throws IOException
{
StringBuffer buf = new StringBuffer();
String line = null;
while ((line = reader.readLine()) != null) {
buf.append(line);
buf.append("\r\n");
}
return buf.toString();
}
public static void doOutput(OutputStream out, byte[] data, int len)
throws IOException
{
int dataLen = data.length;
int off = 0;
while (off < dataLen) {
if (len >= dataLen) {
out.write(data, off, dataLen);
} else {
out.write(data, off, len);
}
out.flush();
off += len;
dataLen -= len;
}
}
public static SSLContext getSSLContext(FileInputStream trustFileInputStream, String trustPasswd, FileInputStream keyFileInputStream, String keyPasswd)
throws NoSuchAlgorithmException, KeyStoreException, CertificateException, IOException, UnrecoverableKeyException, KeyManagementException
{
TrustManagerFactory tmf =
TrustManagerFactory.getInstance("SunX509");
KeyStore trustKeyStore = KeyStore.getInstance("JKS");
trustKeyStore.load(trustFileInputStream,
str2CharArray(trustPasswd));
tmf.init(trustKeyStore);
char[] kp = str2CharArray(keyPasswd);
KeyManagerFactory kmf =
KeyManagerFactory.getInstance("SunX509");
KeyStore ks = KeyStore.getInstance("PKCS12");
ks.load(keyFileInputStream, kp);
kmf.init(ks, kp);
SecureRandom rand = new SecureRandom();
SSLContext ctx = SSLContext.getInstance("TLS");
ctx.init(kmf.getKeyManagers(), tmf.getTrustManagers(), rand);
return ctx;
}
public static Certificate getCertificate(File cafile)
throws CertificateException, IOException
{
CertificateFactory cf = CertificateFactory.getInstance("X.509");
FileInputStream in = new FileInputStream(cafile);
Certificate cert = cf.generateCertificate(in);
in.close();
return cert;
}
public static char[] str2CharArray(String str)
{
if (str == null) {
return null;
}
return str.toCharArray();
}
public static void storeCACert(Certificate cert, String alias, String password, OutputStream out)
throws KeyStoreException, NoSuchAlgorithmException, CertificateException, IOException
{
KeyStore ks = KeyStore.getInstance("JKS");
ks.load(null, null);
ks.setCertificateEntry(alias, cert);
ks.store(out, str2CharArray(password));
}
public static InputStream String2Inputstream(String str)
{
return new ByteArrayInputStream(str.getBytes());
}
}
返回结果:
这就获取到了微信平台证书,但是要注意,这只是获取到了平台证书加密密文 还要进行解密,直接用微信支付文档里面的解密代码就可以。
然后解析出真正的微信证书。
注意这里解密有个问题:解密出现Illegal key size错误
解决:微信支付V3支付通知JAVA解密出现Illegal key size错误 | 微信开放社区
可以用微信证书去加/解密指定内容:
好了,结束,希望对你有所帮助!