概述
阿里云官方给出了增值税发票识别的使用API说明和多语言SDK调用的使用方式,没有给出使用签名+API方式调用的方式,这里演示使用NET代码调用原生API接口的方式。
Code
using System.Globalization; using System.Net; using System.Net.Http.Headers; using System.Security.Cryptography; using System.Text; using System.Web; using Newtonsoft.Json; namespace SignatureDemo { public class Request { public string HttpMethod { get; private set; } public string CanonicalUri { get; private set; } public string Host { get; private set; } public string XAcsAction { get; private set; } public string XAcsVersion { get; private set; } public SortedDictionary<string, object> Headers { get; private set; } public byte[] Body { get; set; } public Dictionary<string, object> QueryParam { get; set; } public Request(string httpMethod, string canonicalUri, string host, string xAcsAction, string xAcsVersion) { HttpMethod = httpMethod; CanonicalUri = canonicalUri; Host = host; XAcsAction = xAcsAction; XAcsVersion = xAcsVersion; Headers = []; QueryParam = []; Body = null; InitHeader(); } private void InitHeader() { Headers["host"] = Host; Headers["x-acs-action"] = XAcsAction; Headers["x-acs-version"] = XAcsVersion; DateTime utcNow = DateTime.UtcNow; Headers["x-acs-date"] = utcNow.ToString("yyyy-MM-dd'T'HH:mm:ss'Z'", CultureInfo.InvariantCulture); Headers["x-acs-signature-nonce"] = Guid.NewGuid().ToString(); } } public class Program { private static readonly string AccessKeyId = "《AccessKey》"; private static readonly string AccessKeySecret = "《AccessSecret》"; private const string Algorithm = "ACS3-HMAC-SHA256"; private const string ContentType = "content-type"; /** * 签名示例,您需要根据实际情况替换main方法中的示例参数。 * ROA接口和RPC接口只有canonicalUri取值逻辑是完全不同,其余内容都是相似的。 * * 通过API元数据获取请求方法(methods)、请求参数名称(name)、请求参数类型(type)、请求参数位置(in),并将参数封装到SignatureRequest中。 * 1. 请求参数在元数据中显示"in":"query",通过queryParam传参。 * 2. 请求参数在元数据中显示"in": "body",通过body传参。 * 3. 请求参数在元数据中显示"in": "formData",通过body传参。 */ public static void Main(string[] args) { // RPC接口请求示例一:请求参数"in":"query" string httpMethod = "POST"; // 请求方式,大部分RPC接口同时支持POST和GET,此处以POST为例 string canonicalUri = "/"; // RPC接口无资源路径,故使用正斜杠(/)作为CanonicalURI string host = "ocr.cn-shanghai.aliyuncs.com"; // 云产品服务接入点 string xAcsAction = "RecognizeVATInvoice"; // API名称 string xAcsVersion = "2019-12-30"; // API版本号 var request = new Request(httpMethod, canonicalUri, host, xAcsAction, xAcsVersion); // DescribeInstanceStatus请求参数如下: // RegionId在元数据中显示的类型是String,"in":"query",必填 request.QueryParam["RegionId"] = "cn-shanghai"; request.QueryParam["FileURL"] = "http://viapi-test.oss-cn-shanghai.aliyuncs.com/viapi-3.0domepic/ocr/RecognizeVATInvoice/RecognizeVATInvoice3.jpg"; request.QueryParam["FileType"] = "jpg"; GetAuthorization(request); // 调用API var result = CallApiAsync(request); Console.WriteLine($"result:{result.Result}"); } private static async Task<string?> CallApiAsync(Request request) { try { // 声明 httpClient using var httpClient = new HttpClient(); // 构建 URL string url = $"https://{request.Host}{request.CanonicalUri}"; var uriBuilder = new UriBuilder(url); var query = new List<string>(); // 添加请求参数 foreach (var entry in request.QueryParam.OrderBy(e => e.Key.ToLower())) { string value = entry.Value?.ToString() ?? ""; query.Add($"{entry.Key}={Uri.EscapeDataString(value)}"); } uriBuilder.Query = string.Join("&", query); Console.WriteLine(uriBuilder.Uri); var requestMessage = new HttpRequestMessage { Method = new HttpMethod(request.HttpMethod), RequestUri = uriBuilder.Uri, }; // 设置请求头 foreach (var entry in request.Headers) { if (entry.Key == "Authorization") { requestMessage.Headers.TryAddWithoutValidation("Authorization", entry.Value.ToString()); ; } else if (entry.Key == ContentType) // 与main中定义的要一致 { continue; } else { requestMessage.Headers.Add(entry.Key, entry.Value.ToString()); } } if (request.Body != null) { HttpContent content = new ByteArrayContent(request.Body); string contentType = request.Headers["content-type"].ToString(); content.Headers.ContentType = MediaTypeHeaderValue.Parse(contentType); requestMessage.Content = content; } // 发送请求 HttpResponseMessage response = await httpClient.SendAsync(requestMessage); // 读取响应内容 string result = await response.Content.ReadAsStringAsync(); return result; } catch (UriFormatException e) { Console.WriteLine("Invalid URI syntax"); Console.WriteLine(e.Message); return null; } catch (Exception e) { Console.WriteLine("Failed to send request"); Console.WriteLine(e); return null; } } private static void GetAuthorization(Request request) { try { // 处理queryParam中参数值为List、Map类型的参数,将参数平铺 request.QueryParam = FlattenDictionary(request.QueryParam); // 步骤 1:拼接规范请求串 StringBuilder canonicalQueryString = new(); foreach (var entry in request.QueryParam.OrderBy(e => e.Key.ToLower())) { if (canonicalQueryString.Length > 0) { canonicalQueryString.Append('&'); } canonicalQueryString.Append($"{PercentCode(entry.Key)}={PercentCode(entry.Value?.ToString() ?? "")}"); } byte[] requestPayload = request.Body == null ? Encoding.UTF8.GetBytes("") : request.Body; string hashedRequestPayload = Sha256Hash(requestPayload); request.Headers["x-acs-content-sha256"] = hashedRequestPayload; StringBuilder canonicalHeaders = new(); StringBuilder signedHeadersSb = new(); foreach (var entry in request.Headers.OrderBy(e => e.Key.ToLower())) { if (entry.Key.StartsWith("x-acs-", StringComparison.CurrentCultureIgnoreCase) || entry.Key.Equals("host", StringComparison.OrdinalIgnoreCase) || entry.Key.Equals(ContentType, StringComparison.OrdinalIgnoreCase)) { string lowerKey = entry.Key.ToLower(); string value = (entry.Value?.ToString() ?? "").Trim(); canonicalHeaders.Append($"{lowerKey}:{value}\n"); signedHeadersSb.Append($"{lowerKey};"); } } string signedHeaders = signedHeadersSb.ToString().TrimEnd(';'); string canonicalRequest = $"{request.HttpMethod}\n{request.CanonicalUri}\n{canonicalQueryString}\n{canonicalHeaders}\n{signedHeaders}\n{hashedRequestPayload}"; Console.WriteLine($"canonicalRequest:{canonicalRequest}"); // 步骤 2:拼接待签名字符串 string hashedCanonicalRequest = Sha256Hash(Encoding.UTF8.GetBytes(canonicalRequest)); string stringToSign = $"{Algorithm}\n{hashedCanonicalRequest}"; Console.WriteLine($"stringToSign:{stringToSign}"); // 步骤 3:计算签名 string signature = HmacSha256(AccessKeySecret, stringToSign); // 步骤 4:拼接 Authorization string authorization = $"{Algorithm} Credential={AccessKeyId},SignedHeaders={signedHeaders},Signature={signature}"; request.Headers["Authorization"] = authorization; Console.WriteLine($"authorization:{authorization}"); } catch (Exception ex) { Console.WriteLine("Failed to get authorization"); Console.WriteLine(ex.Message); } } private static string FormDataToString(Dictionary<string, object> formData) { Dictionary<string, object> tileMap = FlattenDictionary(formData); StringBuilder result = new StringBuilder(); bool first = true; string symbol = "&"; foreach (var entry in tileMap) { string value = entry.Value.ToString(); if (!string.IsNullOrEmpty(value)) { if (!first) { result.Append(symbol); } first = false; result.Append(PercentCode(entry.Key)); result.Append("="); result.Append(PercentCode(value)); } } return result.ToString(); } private static Dictionary<string, object> FlattenDictionary(Dictionary<string, object> dictionary, string prefix = "") { var result = new Dictionary<string, object>(); foreach (var kvp in dictionary) { string key = string.IsNullOrEmpty(prefix) ? kvp.Key : $"{prefix}.{kvp.Key}"; if (kvp.Value is Dictionary<string, object> nestedDict) { var nestedResult = FlattenDictionary(nestedDict, key); foreach (var nestedKvp in nestedResult) { result[nestedKvp.Key] = nestedKvp.Value; } } else if (kvp.Value is List<string> list) { for (int i = 0; i < list.Count; i++) { result[$"{key}.{i + 1}"] = list[i]; } } else { result[key] = kvp.Value; } } return result; } private static string HmacSha256(string key, string message) { using (var hmac = new HMACSHA256(Encoding.UTF8.GetBytes(key))) { byte[] hashMessage = hmac.ComputeHash(Encoding.UTF8.GetBytes(message)); return BitConverter.ToString(hashMessage).Replace("-", "").ToLower(); } } private static string Sha256Hash(byte[] input) { byte[] hashBytes = SHA256.HashData(input); return BitConverter.ToString(hashBytes).Replace("-", "").ToLower(); } private static string PercentCode(string str) { if (string.IsNullOrEmpty(str)) { throw new ArgumentException("输入字符串不可为null或空"); } return Uri.EscapeDataString(str).Replace("+", "%20").Replace("*", "%2A").Replace("%7E", "~"); } } }
替换参数位置
运行结果