最近在寫特約服務商進件的由於微信官方沒有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 值 /// </summary> /// <param name="fileName">要計算 SHA256 值的文件名和路徑</param> /// <returns>SHA256值16進制字符串</returns> public static string SHA256File(string fileName) { return HashFile(fileName, "sha256"); } /// <summary> /// 計算文件的哈希值 /// </summary> /// <param name="fileName">要計算哈希值的文件名和路徑</param> /// <param name="algName">算法:sha1,md5</param> /// <returns>哈希值16進制字符串</returns> 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); } /// <summary> /// 字節數組轉換為16進制表示的字符串 /// </summary> 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; } /// <summary> /// 計算哈希值 /// </summary> /// <param name="stream">要計算哈希值的 Stream</param> /// <param name="algName">算法:sha1,md5</param> /// <returns>哈希值字節數組</returns> 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; } }
頭文件
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 { /// <summary> /// 頭文件 /// </summary> public class HttpHandler : DelegatingHandler { private readonly string merchantId; private readonly string serialNo; private readonly string privateKey; private readonly string json; /// <summary> /// 構造方法 /// </summary> /// <param name="merchantId">商戶號</param> /// <param name="merchantSerialNo">證書序列號</param> /// <param name="privateKey"> 私鑰不包括私鑰文件起始的-----BEGIN PRIVATE KEY----- 亦不包括結尾的-----END PRIVATE KEY-----</param> /// <param name="json">簽名json數據,默認不需要傳入,獲取body內容,如傳入簽名傳入參數上傳圖片時需傳入</param> 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<HttpResponseMessage> 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<string> 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) { 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)); } } } }
