使用.NET Framework 4.8调用Zuora API创建用户时Base64转换报错
.NET Framework 4.8调用Zuora API的HMAC签名移植问题
我需要用.NET Framework 4.8调用Zuora API创建用户,但将Java版HMAC请求签名代码移植到C#时遇到问题:执行var secretKeyByteArray = Convert.FromBase64String(APIKey);时出现错误:输入字符串不是有效的Base64。同时当前C#的签名逻辑与Java实现完全不符,也会导致API验证失败。
Java版HMAC签名实现
package io.blaize.api.utilities.security; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.util.Objects; import io.blaize.api.exception.HmacException; public class HmacSigner { public static final String TWO_DIGIT_HEX_FORMAT = "%1$02x"; private final String algorithm; public HmacSigner(String algorithm) { if ("SHA256".equals(algorithm)) { this.algorithm = "SHA-256"; } else { this.algorithm = algorithm; } } public String signRequest(String secretKey, String body, String path, String query, String method, String timestamp, String nonce) throws HmacException { Objects.requireNonNull(secretKey); Objects.requireNonNull(body); Objects.requireNonNull(path); Objects.requireNonNull(query); Objects.requireNonNull(method); Objects.requireNonNull(timestamp); Objects.requireNonNull(nonce); try { MessageDigest messageDigest = MessageDigest.getInstance(algorithm); messageDigest.update(secretKey.getBytes()); messageDigest.update(body.getBytes()); messageDigest.update(path.getBytes()); messageDigest.update(query.getBytes()); messageDigest.update(method.getBytes()); messageDigest.update(timestamp.getBytes()); messageDigest.update(nonce.getBytes()); byte[] digest = messageDigest.digest(); StringBuffer hash = new StringBuffer(); for (byte digestByte : digest) { Integer unsignedInteger = new Integer(Byte.toUnsignedInt(digestByte)); hash.append(String.format(TWO_DIGIT_HEX_FORMAT, unsignedInteger)); } return hash.toString(); } catch (NoSuchAlgorithmException e) { throw new HmacException(e); } } }
Java创建用户示例代码
String protocol = "http"; String host = "admin.test.blaize.io"; String path = "/v3/users"; String method = "POST"; String body = "{\"identifiers\": { \"email_address\": \"test@test.com\" }, \"validators\": { \"password\": \"sup3rsecre!10t\" }}"; String accessKey = "xyz"; String secretKey = loadSecretKeySecurely(accessKey); String timestamp = String.valueOf(new Date().getTime()); String nonce = UUID.randomUUID().toString(); String query = ""; String hash = new HmacSigner("SHA-256"). signRequest(secretKey, body, path, query, method, timestamp, nonce); String authorizationHeaderValue = "ZEPHR-HMAC-SHA256 " + accessKey + ":" + timestamp + ":" + nonce + ":" + hash; // This is a standard library implementation for illustration only HttpURLConnection connection = (HttpURLConnection) new URL(protocol + "://" + host + path + "?" + query).openConnection(); connection.setRequestMethod(method); connection.addRequestProperty("Authorization", authorizationHeaderValue); connection.addRequestProperty("Content-Type", "application/json"); connection.setDoOutput(true); DataOutputStream outputStream = new DataOutputStream(connection.getOutputStream()); outputStream.writeBytes(body); outputStream.flush(); outputStream.close(); int status = connection.getResponseCode(); if (status >= 200 && status < 400) { System.out.println(new BufferedReader(new InputStreamReader(connection.getInputStream())).lines().collect(Collectors.joining("\n"))); } else { System.err.println(status); }
我编写的C#代码
HMACDelegatingHandler实现
using System; using System.Net.Http; using System.Net.Http.Headers; using System.Security.Cryptography; using System.Text; using System.Threading; using System.Threading.Tasks; using System.Web; namespace HMACClient { public class HMACDelegatingHandler : DelegatingHandler { // First obtained the APP ID and API Key from the server // The APIKey MUST be stored securely in db or in the App.Config private string APPId = "xxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"; private string APIKey = "xxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"; protected async override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) { HttpResponseMessage response = null; string requestContentBase64String = string.Empty; //Get the Request URI string requestUri = HttpUtility.UrlEncode(request.RequestUri.AbsoluteUri.ToLower()); //Get the Request HTTP Method type string requestHttpMethod = request.Method.Method; //Calculate UNIX time DateTime epochStart = new DateTime(1970, 01, 01, 0, 0, 0, 0, DateTimeKind.Utc); TimeSpan timeSpan = DateTime.UtcNow - epochStart; string requestTimeStamp = Convert.ToUInt64(timeSpan.TotalSeconds).ToString(); //Create the random nonce for each request string nonce = Guid.NewGuid().ToString("N"); //Checking if the request contains body, usually will be null wiht HTTP GET and DELETE if (request.Content != null) { // Hashing the request body, so any change in request body will result a different hash // we will achieve message integrity byte[] content = await request.Content.ReadAsByteArrayAsync(); MD5 md5 = MD5.Create(); byte[] requestContentHash = md5.ComputeHash(content); requestContentBase64String = Convert.ToBase64String(requestContentHash); } //Creating the raw signature string by combinging //APPId, request Http Method, request Uri, request TimeStamp, nonce, request Content Base64 String string signatureRawData = String.Format("{0}{1}{2}{3}{4}{5}", APPId, requestHttpMethod, requestUri, requestTimeStamp, nonce, requestContentBase64String); //Converting the APIKey into byte array var secretKeyByteArray = Convert.FromBase64String(APIKey); //Converting the signatureRawData into byte array byte[] signature = Encoding.UTF8.GetBytes(signatureRawData); //Generate the hmac signature and set it in the Authorization header using (HMACSHA256 hmac = new HMACSHA256(secretKeyByteArray)) { byte[] signatureBytes = hmac.ComputeHash(signature); string requestSignatureBase64String = Convert.ToBase64String(signatureBytes); //Setting the values in the Authorization header using custom scheme (hmacauth) request.Headers.Authorization = new AuthenticationHeaderValue("hmacauth", string.Format("{0}:{1}:{2}:{3}", APPId, requestSignatureBase64String, nonce, requestTimeStamp)); } response = await base.SendAsync(request, cancellationToken); return response; } }
创建用户的C#代码
private async void createUser() { Console.WriteLine("Calling the back-end API"); string apiBaseAddress = "https://mybusiness.api.zephr.com/"; HMACDelegatingHandler customDelegatingHandler = new HMACDelegatingHandler(); HttpClient client = HttpClientFactory.Create(customDelegatingHandler); string body = "{\"identifiers\": {\"email_address\": \"joe.blow@company.com\"},\"attributes\": {\"first_name\": \"Joe\",\"surname\": \"Blow\"},\"foreign_keys\" : {\"other_id\" : \"0030C00000Xu1LYQAZ\"}}"; HttpResponseMessage response = await client.PostAsync(apiBaseAddress + "v3/users", new StringContent(body, Encoding.UTF8, "application/json")); if (response.IsSuccessStatusCode) { string responseString = await response.Content.ReadAsStringAsync(); Console.WriteLine(responseString); Console.WriteLine("HTTP Status: {0}, Reason {1}. Press ENTER to exit", response.StatusCode, response.ReasonPhrase); } else { Console.WriteLine("Failed to call the API. HTTP Status: {0}, Reason {1}", response.StatusCode, response.ReasonPhrase); } }
问题解决
1. 错误直接原因:APIKey不是Base64格式
你的APIKey是类似xxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx的UUID格式,不是Base64字符串,所以Convert.FromBase64String(APIKey)会报错。更关键的是,当前C#的签名逻辑和Java实现完全不一致,必须对齐Java的签名逻辑。
2. 对齐Java的签名逻辑
Java代码的核心是按顺序拼接secretKey、body、path、query、method、timestamp、nonce的字节,然后做SHA-256哈希,最后转为小写十六进制字符串,并非标准HMAC算法。以下是修正后的C#实现:
修正后的HMAC签名工具类
public static class HmacSigner { public static string SignRequest(string secretKey, string body, string path, string query, string method, string timestamp, string nonce) { using (var sha256 = SHA256.Create()) { // 按Java顺序依次写入字节 sha256.ComputeHash(Encoding.UTF8.GetBytes(secretKey)); sha256.TransformBlock(Encoding.UTF8.GetBytes(body), 0, Encoding.UTF8.GetBytes(body).Length, null, 0); sha256.TransformBlock(Encoding.UTF8.GetBytes(path), 0, Encoding.UTF8.GetBytes(path).Length, null, 0); sha256.TransformBlock(Encoding.UTF8.GetBytes(query), 0, Encoding.UTF8.GetBytes(query).Length, null, 0); sha256.TransformBlock(Encoding.UTF8.GetBytes(method), 0, Encoding.UTF8.GetBytes(method).Length, null, 0); sha256.TransformBlock(Encoding.UTF8.GetBytes(timestamp), 0, Encoding.UTF8.GetBytes(timestamp).Length, null, 0); sha256.TransformFinalBlock(Encoding.UTF8.GetBytes(nonce), 0, Encoding.UTF8.GetBytes(nonce).Length); var hashBytes = sha256.Hash; var hashBuilder = new StringBuilder(); foreach (var b in hashBytes) { // 转为小写十六进制,对齐Java的%02x格式 hashBuilder.Append(b.ToString("x2")); } return hashBuilder.ToString(); } } }
修正后的HMACDelegatingHandler
using System; using System.Net.Http; using System.Net.Http.Headers; using System.Security.Cryptography; using System.Text; using System.Threading; using System.Threading.Tasks; namespace HMACClient { public class HMACDelegatingHandler : DelegatingHandler { private string _accessKey = "xxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"; private string _secretKey = "xxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"; protected async override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) { // 提取path和query,对齐Java逻辑 var path = request.RequestUri.AbsolutePath; var query = request.RequestUri.Query.TrimStart('?'); // 获取请求方法 var method = request.Method.Method; // 获取请求body,注意要和发送的body完全一致 string body = string.Empty; if (request.Content != null) { body = await request.Content.ReadAsStringAsync(); // 重新设置Content,避免读取后内容为空 request.Content = new StringContent(body, Encoding.UTF8, "application/json"); } // 生成毫秒级时间戳,对齐Java的Date.getTime() var timestamp = ((DateTimeOffset)DateTime.UtcNow).ToUnixTimeMilliseconds().ToString(); // 生成nonce,对齐Java的UUID格式 var nonce = Guid.NewGuid().ToString(); // 计算签名 var hash = HmacSigner.SignRequest(_secretKey, body, path, query, method, timestamp, nonce); // 设置Authorization header,对齐Java的格式 var authHeaderValue = $"{_accessKey}:{timestamp}:{nonce}:{hash}"; request.Headers.Authorization = new AuthenticationHeaderValue("ZEPHR-HMAC-SHA256", authHeaderValue); return await base.SendAsync(request, cancellationToken); } } }
3. 验证要点
- 时间戳必须是毫秒级,Java用的是
Date.getTime(),C#对应ToUnixTimeMilliseconds() - Nonce格式必须和Java一致:
Guid.NewGuid().ToString()(带连字符),而非ToString("N") - 签名时的path和query要分开提取,不能用整个UrlEncode后的Uri
- Authorization header的scheme是
ZEPHR-HMAC-SHA256,参数格式为accessKey:timestamp:nonce:hash - 签名时使用的body必须和实际发送的body完全一致,避免编码差异
内容的提问来源于stack exchange,提问作者FDB
相关产品推荐
相关产品推荐

