最近在写特约服务商进件的由于微信官方没有DEMO,导致踩了很多坑,特把自己经验分享给大家。
注意几点:
1、上传图片签名不是把所有body内容都进行签名,只需签名计算的请求主体为meta的json串:{ "filename": "filea.jpg", "sha256": "hjkahkjsjkfsjk78687dhjahdajhk" }
2、签名的是私钥, 私钥不包括私钥文件起始的-----BEGIN PRIVATE KEY----- 亦不包括结尾的-----END PRIVATE KEY-----
3、上传参数meta、file、文件名必须新增双引号
4、MultipartFormDataContent 必须添加头文件multipart/form-data,不然一直签名错误。
5、头文件必须添加"user-agent"、"application/json“
代码如下:
string private_key = @"商户私钥";
string mchid = "商户号";
string serialNo = "商户API证书序列号";
string filePath = @"H:\1.jpg";
string boundary = string.Format("--{0}", DateTime.Now.Ticks.ToString("x"));
var sha256 = SHAFile.SHA256File(filePath);
meta meta = new meta()
{
sha256 = sha256,
filename = System.IO.Path.GetFileName(serialNo)
};
var json = JsonConvert.SerializeObject(meta);
var httpHandler = new HttpHandler(mchid, serialNo, private_key, json);
HttpClient client = new HttpClient(httpHandler);
using (var requestContent = new MultipartFormDataContent(boundary))
{
requestContent.Headers.ContentType = MediaTypeHeaderValue.Parse("multipart/form-data"); //这里必须添加
requestContent.Add(new StringContent(json, Encoding.UTF8, "application/json"), "\"meta\""); //这里主要必须要双引号
var fileInfo = new FileInfo(filePath);
using (var fileStream = fileInfo.OpenRead())
{
var content = new byte[fileStream.Length];
fileStream.Read(content, 0, content.Length);
var byteArrayContent = new ByteArrayContent(content);
byteArrayContent.Headers.ContentType = new MediaTypeHeaderValue("image/jpg");
requestContent.Add(byteArrayContent, "\"file\"", "\"" + meta.filename + "\""); //这里主要必须要双引号
using (var response = await client.PostAsync("https://api.mch.weixin.qq.com/v3/merchant/media/upload", requestContent)) //上传
using (var responseContent = response.Content)
{
string responseBody = await responseContent.ReadAsStringAsync(); //这里就可以拿到图片id了
return ResultHelper.QuickReturn(responseBody);
}
}
SHAFile 类:
using System;
using System.Collections.Generic;
using System.IO;
using System.Security.Cryptography;
using System.Text;
namespace Cloud.Pay.Common
{
public class SHAFile
{
/// 计算文件的 SHA256 值
///
/// 要计算 SHA256 值的文件名和路径
/// SHA256值16进制字符串
public static string SHA256File(string fileName)
{
return HashFile(fileName, "sha256");
}
///
/// 计算文件的哈希值
///
/// 要计算哈希值的文件名和路径
/// 算法:sha1,md5
/// 哈希值16进制字符串
private static string HashFile(string fileName, string algName)
{
if (!System.IO.File.Exists(fileName))
return string.Empty;
FileStream fs = new FileStream(fileName, FileMode.Open, FileAccess.Read);
byte[] hashBytes = HashData(fs, algName);
fs.Close();
return ByteArrayToHexString(hashBytes);
}
///
/// 字节数组转换为16进制表示的字符串
///
private static string ByteArrayToHexString(byte[] buf)
{
string returnStr = "";
if (buf != null)
{
for (int i = 0; i < buf.Length; i++)
{
returnStr += buf[i].ToString("X2");
}
}
return returnStr;
}
///
/// 计算哈希值
///
/// 要计算哈希值的 Stream
/// 算法:sha1,md5
/// 哈希值字节数组
private static byte[] HashData(Stream stream, string algName)
{
HashAlgorithm algorithm;
if (algName == null)
{
throw new ArgumentNullException("algName 不能为 null");
}
if (string.Compare(algName, "sha256", true) == 0)
{
algorithm = SHA256.Create();
}
else
{
if (string.Compare(algName, "md5", true) != 0)
{
throw new Exception("algName 只能使用 sha256 或 md5");
}
algorithm = MD5.Create();
}
return algorithm.ComputeHash(stream);
}
}
}
meta类:
public class meta
{
public string filename { get; set; }
public string sha256 { get; set; }
}
HttpHandler 头文件类
using System;
using System.IO;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Security.Authentication;
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
namespace Cloud.Pay.Common
{
///
/// 头文件
///
public class HttpHandler : DelegatingHandler
{
private readonly string merchantId;
private readonly string serialNo;
private readonly string privateKey;
private readonly string json;
///
/// 构造方法
///
/// 商户号
/// 证书序列号
/// 私钥不包括私钥文件起始的-----BEGIN PRIVATE KEY----- 亦不包括结尾的-----END PRIVATE KEY-----
/// 签名json数据,默认不需要传入,获取body内容,如传入签名传入参数上传图片时需传入
public HttpHandler(string merchantId, string merchantSerialNo, string privateKey, string json = "")
{
HttpClientHandler handler = new HttpClientHandler();
handler.ClientCertificateOptions = ClientCertificateOption.Manual;
handler.SslProtocols = SslProtocols.Tls12;
try
{
string certPath = System.IO.Path.Combine(Environment.CurrentDirectory, @"Cert\apiclient_cert.p12");
handler.ClientCertificates.Add(new X509Certificate2(certPath, "1487076932",
X509KeyStorageFlags.PersistKeySet | X509KeyStorageFlags.MachineKeySet));
}
catch (Exception e)
{
throw new Exception("ca err(证书错误)");
}
handler.SslProtocols = SslProtocols.Tls12 | SslProtocols.Tls11 | SslProtocols.Tls;
handler.ServerCertificateCustomValidationCallback = (message, cert, chain, errors) => true;
InnerHandler = handler;
this.merchantId = merchantId;
this.serialNo = merchantSerialNo;
this.privateKey = privateKey;
this.json = json;
}
protected async override Task SendAsync(
HttpRequestMessage request,
CancellationToken cancellationToken)
{
var auth = await BuildAuthAsync(request);
string value = $"WECHATPAY2-SHA256-RSA2048 {auth}";
request.Headers.Add("Authorization", value);
request.Headers.Add("user-agent", "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/36.0.1985.143 Safari/537.36");
MediaTypeWithQualityHeaderValue mediaTypeWithQualityHeader = new MediaTypeWithQualityHeaderValue("application/json");
request.Headers.Accept.Add(mediaTypeWithQualityHeader);
request.Headers.AcceptCharset.Add(new StringWithQualityHeaderValue("utf-8"));
return await base.SendAsync(request, cancellationToken);
}
protected async Task BuildAuthAsync(HttpRequestMessage request)
{
string method = request.Method.ToString();
string body = "";
if (method == "POST" || method == "PUT" || method == "PATCH")
{
if (!string.IsNullOrEmpty(json))
body = json;
else
{
var content = request.Content;
body = await content.ReadAsStringAsync();
}
}
string uri = request.RequestUri.PathAndQuery;
var timestamp = DateTimeOffset.Now.ToUnixTimeSeconds();
string nonce = Guid.NewGuid().ToString("n");
string message = $"{method}\n{uri}\n{timestamp}\n{nonce}\n{body}\n";
string signature = Sign(message);
return $"mchid=\"{merchantId}\",nonce_str=\"{nonce}\",timestamp=\"{timestamp}\",serial_no=\"{serialNo}\",signature=\"{signature}\"";
}
protected string Sign(string message)
{
using (var cert = new X509Certificate2(@"证书路径", "商户号"))
{
var rsa = cert.GetRSAPrivateKey();
byte[] data = System.Text.Encoding.UTF8.GetBytes(message);
return Convert.ToBase64String(rsa.SignData(data, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1));
}
//下面这个方式在liunx 下不支持 windwos 可以使用
//byte[] keyData = Convert.FromBase64String(privateKey);
//using (CngKey cngKey = CngKey.Import(keyData, CngKeyBlobFormat.Pkcs8PrivateBlob))
//using (RSACng rsa = new RSACng(cngKey))
//{
// byte[] data = System.Text.Encoding.UTF8.GetBytes(message);
// return Convert.ToBase64String(rsa.SignData(data, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1));
//}
}
}
}
但你写的这个跟文档的格式不一样啊,微信文档的坑么
谢谢,图片能上传成功了
Node.js 版本可以参考 此篇文章
Java 版本可以参考 此篇文章
文章评论区有源码链接
如果对你有帮助请点击「有用」告知