You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

使用.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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.05 00:29:50