From f6186868b788c192cd13d59e3470b043d0046f67 Mon Sep 17 00:00:00 2001 From: SF-bytebytebrew Date: Wed, 21 May 2025 16:39:06 +0800 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20=E6=B7=BB=E5=8A=A0C#=E5=AE=9E?= =?UTF-8?q?=E7=8E=B0=E7=9A=84API=E7=AD=BE=E5=90=8D=E5=B7=A5=E5=85=B7?= =?UTF-8?q?=EF=BC=8C=E5=8C=85=E6=8B=AC=E6=A0=B8=E5=BF=83=E5=BA=93=E3=80=81?= =?UTF-8?q?=E5=91=BD=E4=BB=A4=E8=A1=8C=E5=B7=A5=E5=85=B7=E5=92=8C=E4=BD=BF?= =?UTF-8?q?=E7=94=A8=E6=96=87=E6=A1=A3=EF=BC=8C=E6=94=AF=E6=8C=81=E5=A4=9A?= =?UTF-8?q?=E7=A7=8D=E7=AD=BE=E5=90=8D=E7=AE=97=E6=B3=95=E5=92=8C=E7=8E=AF?= =?UTF-8?q?=E5=A2=83=E5=8F=98=E9=87=8F=E9=85=8D=E7=BD=AE=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- csharp/README.md | 180 ++++++++++ csharp/src/ApiSigner.Cli/ApiSigner.Cli.csproj | 23 ++ csharp/src/ApiSigner.Cli/Program.cs | 159 +++++++++ csharp/src/ApiSigner.sln | 30 ++ csharp/src/ApiSigner/ApiSigner.cs | 312 ++++++++++++++++++ csharp/src/ApiSigner/ApiSigner.csproj | 13 + csharp/src/ApiSigner/SignOptions.cs | 55 +++ csharp/src/ApiSigner/SignVerifyResult.cs | 42 +++ csharp/src/ApiSigner/SignatureAlgorithm.cs | 76 +++++ 9 files changed, 890 insertions(+) create mode 100644 csharp/README.md create mode 100644 csharp/src/ApiSigner.Cli/ApiSigner.Cli.csproj create mode 100644 csharp/src/ApiSigner.Cli/Program.cs create mode 100644 csharp/src/ApiSigner.sln create mode 100644 csharp/src/ApiSigner/ApiSigner.cs create mode 100644 csharp/src/ApiSigner/ApiSigner.csproj create mode 100644 csharp/src/ApiSigner/SignOptions.cs create mode 100644 csharp/src/ApiSigner/SignVerifyResult.cs create mode 100644 csharp/src/ApiSigner/SignatureAlgorithm.cs diff --git a/csharp/README.md b/csharp/README.md new file mode 100644 index 0000000..efdf930 --- /dev/null +++ b/csharp/README.md @@ -0,0 +1,180 @@ +# API签名工具 - C#实现 + +本目录包含API签名机制的C#语言实现,提供了签名计算与验证的完整功能。 + +## 功能特点 + +- 支持多种签名算法(MD5, SHA1, SHA256, HMAC-SHA256) +- 支持自定义签名参数名称 +- 支持URL参数签名 +- 内置防重放攻击机制(时间戳+nonce) +- 跨平台(.NET Standard 2.0 兼容) +- 提供命令行工具进行测试 +- 无外部依赖,仅使用.NET标准库 + +## 项目结构 + +根据实际项目文件结构: + +```plaintext +csharp/ +├── src/ +│ ├── ApiSigner/ +│ │ ├── ApiSigner.cs # 签名工具实现 +│ │ ├── ApiSigner.csproj # 项目文件 +│ │ ├── SignatureAlgorithm.cs # 签名算法定义 +│ │ ├── SignOptions.cs # 签名配置 +│ │ └── SignVerifyResult.cs # 验证结果类型 +│ │ +│ ├── ApiSigner.Cli/ +│ │ ├── ApiSigner.Cli.csproj # 命令行项目文件 +│ │ └── Program.cs # 命令行入口 +│ │ +│ └── ApiSigner.sln # 解决方案文件 +``` + +## 使用方法 + +### 构建项目 + +```bash +dotnet build src/ApiSigner.sln +``` + +### 运行命令行工具 + +```bash +dotnet run --project src/ApiSigner.Cli/ApiSigner.Cli.csproj [选项] +``` + +### 命令行选项 + +| 选项 | 描述 | +|------|------| +| `-a, --algorithm` | 签名算法: MD5, SHA1, SHA256, HMAC-SHA256 | +| `-u, --url` | API基础URL | +| `-p, --param` | 请求参数,格式为key=value,可多次使用 | +| `-k, --key` | 访问密钥ID | +| `-s, --secret` | 密钥 | +| `-c, --channel` | 合作渠道方ID | +| `-h, --help` | 显示帮助信息 | + +### 常用命令示例 + +**基本用法** + +```bash +dotnet run --project src/ApiSigner.Cli/ApiSigner.Cli.csproj +``` + +**自定义参数** + +```bash +dotnet run --project src/ApiSigner.Cli/ApiSigner.Cli.csproj \ + -u "https://api.example.com/user/info" \ + -p "userId=12345" -p "action=getInfo" \ + -k "YOUR_ACCESS_KEY" \ + -s "YOUR_SECRET_KEY" \ + -c "3" +``` + +**指定签名算法** + +```bash +dotnet run --project src/ApiSigner.Cli/ApiSigner.Cli.csproj -a SHA256 +``` + +**帮助信息** + +```bash +dotnet run --project src/ApiSigner.Cli/ApiSigner.Cli.csproj --help +``` + +### API接口测试实例 + +使用真实API接口进行测试: + +```bash +# 未签名的API调用测试 - 返回错误 +curl "https://api-v1.sound-force.com:8443/p/album/single/media-url?channelId=3&singleId=381980" +# 返回: {"code":400,"data":null,"msg":"Missing AccessKeyId","success":false} + +# 生成访问https://api-v1.sound-force.com:8443/p/album/single/media-url的签名URL +dotnet run --project src/ApiSigner.Cli/ApiSigner.Cli.csproj \ + -a MD5 \ + -u "https://api-v1.sound-force.com:8443/p/album/single/media-url" \ + -p "singleId=381980" \ + -k "YOUR_ACCESS_KEY" \ + -s "YOUR_SECRET_KEY" \ + -c "3" + +# 使用curl测试API接口 +signedUrl=$(dotnet run --project src/ApiSigner.Cli/ApiSigner.Cli.csproj \ + -a MD5 \ + -u "https://api-v1.sound-force.com:8443/p/album/single/media-url" \ + -p "singleId=381980" \ + -k "YOUR_ACCESS_KEY" \ + -s "YOUR_SECRET_KEY" \ + -c "3" | grep -A 1 "签名后的URL:" | tail -n 1) +curl -v "$signedUrl" +``` + +请注意: + +- 替换`YOUR_ACCESS_KEY`为实际的访问密钥ID +- 替换`YOUR_SECRET_KEY`为实际的密钥 +- 示例使用的渠道ID为`3`,请根据实际情况调整 + +使用有效的密钥和签名后,API接口将返回成功响应(状态码200)并提供媒体URL数据。 + +### 代码集成 + +```csharp +using SoundForce.ApiSigner; + +// 创建签名选项 +var options = new SignOptions { Algorithm = SignatureAlgorithm.Md5 }; + +// 创建签名工具 +var signer = new ApiSigner(options); + +// 准备请求参数 +var parameters = new Dictionary(StringComparer.OrdinalIgnoreCase) +{ + ["singleId"] = "381980" +}; + +// 执行签名 +var signedParams = signer.SignRequest( + parameters, + "YOUR_ACCESS_KEY", + "YOUR_SECRET_KEY", + "3" +); + +// 或签名URL +var signedUrl = signer.SignUrl( + "https://api-v1.sound-force.com:8443/p/album/single/media-url", + parameters, + "YOUR_ACCESS_KEY", + "YOUR_SECRET_KEY", + "3" +); +``` + +### 环境变量 + +该工具支持从`.env`文件加载以下配置: + +- `ACCESS_KEY_ID`: 访问密钥ID +- `SECRET_KEY`: 密钥 +- `CHANNEL_ID`: 渠道ID +- `SIGN_ALGORITHM`: 签名算法 +- `API_BASE_URL`: API基础URL + +## 注意事项 + +1. 在生产环境中,应该妥善保管SecretKey,不要硬编码在代码中 +2. 服务端需要自行实现nonce存储与验证以防止重放攻击 +3. 时间戳验证需要考虑服务器与客户端之间可能存在的时间差 +4. 默认情况下,命令行工具从`.env`文件或环境变量读取配置 diff --git a/csharp/src/ApiSigner.Cli/ApiSigner.Cli.csproj b/csharp/src/ApiSigner.Cli/ApiSigner.Cli.csproj new file mode 100644 index 0000000..879535e --- /dev/null +++ b/csharp/src/ApiSigner.Cli/ApiSigner.Cli.csproj @@ -0,0 +1,23 @@ + + + + Exe + net8.0 + SoundForce.ApiSigner.Cli + SoundForce.ApiSigner.Cli + 8.0 + enable + Sound Force + API签名工具命令行接口 + + + + + + + + + + + + \ No newline at end of file diff --git a/csharp/src/ApiSigner.Cli/Program.cs b/csharp/src/ApiSigner.Cli/Program.cs new file mode 100644 index 0000000..fa99549 --- /dev/null +++ b/csharp/src/ApiSigner.Cli/Program.cs @@ -0,0 +1,159 @@ +using System; +using System.Collections.Generic; +using System.CommandLine; +using System.Threading.Tasks; + +namespace SoundForce.ApiSigner.Cli +{ + class Program + { + static async Task Main(string[] args) + { + var rootCommand = new RootCommand("API签名工具"); + + var algorithmOption = new Option( + aliases: new[] { "--algorithm", "-a" }, + description: "签名算法: MD5, SHA1, SHA256, HMAC-SHA256", + getDefaultValue: () => Environment.GetEnvironmentVariable("SIGN_ALGORITHM") ?? "MD5"); + + var urlOption = new Option( + aliases: new[] { "--url", "-u" }, + description: "基础URL地址", + getDefaultValue: () => + Environment.GetEnvironmentVariable("API_BASE_URL") ?? "https://api.example.com/v1/data"); + + var paramOption = new Option( + aliases: new[] { "--param", "-p" }, + description: "请求参数,格式为key=value,可多次指定") + { + AllowMultipleArgumentsPerToken = true + }; + + var accessKeyIdOption = new Option( + aliases: new[] { "--key", "-k" }, + description: "访问密钥ID", + getDefaultValue: () => Environment.GetEnvironmentVariable("ACCESS_KEY_ID") ?? "test-access-key-id"); + + var secretKeyOption = new Option( + aliases: new[] { "--secret", "-s" }, + description: "密钥", + getDefaultValue: () => Environment.GetEnvironmentVariable("SECRET_KEY") ?? "test-secret-key"); + + var channelIdOption = new Option( + aliases: new[] { "--channel", "-c" }, + description: "合作渠道方ID", + getDefaultValue: () => Environment.GetEnvironmentVariable("CHANNEL_ID") ?? "test-channel-id"); + + rootCommand.AddOption(algorithmOption); + rootCommand.AddOption(urlOption); + rootCommand.AddOption(paramOption); + rootCommand.AddOption(accessKeyIdOption); + rootCommand.AddOption(secretKeyOption); + rootCommand.AddOption(channelIdOption); + + rootCommand.SetHandler((algorithmStr, url, paramArr, accessKeyId, secretKey, channelId) => + { + SignatureAlgorithm algorithm = SignatureAlgorithm.Md5; + if (Enum.TryParse(algorithmStr, true, out SignatureAlgorithm parsedAlgorithm)) + { + algorithm = parsedAlgorithm; + } + else + { + Console.WriteLine($"警告: 无效的签名算法: {algorithmStr},使用默认MD5算法"); + } + + var options = new SignOptions + { + Algorithm = algorithm, + SignatureName = "sign" // 使用"sign"而不是"signature" + }; + + var signer = new ApiSigner(options); + + var parameters = new Dictionary(StringComparer.OrdinalIgnoreCase); + if (paramArr.Length > 0) + { + foreach (var param in paramArr) + { + var parts = param.Split('=', 2); + if (parts.Length == 2) + { + parameters[parts[0]] = parts[1]; + } + } + } + else + { + parameters["userId"] = "12345"; + parameters["action"] = "getData"; + parameters["data"] = "测试数据"; + } + + Console.WriteLine("===================== API签名示例 ====================="); + Console.WriteLine($"AccessKeyId: {accessKeyId}"); + Console.WriteLine($"ChannelId: {channelId}"); + Console.WriteLine($"SecretKey: {secretKey}"); + Console.WriteLine($"签名算法: {algorithm}"); + Console.WriteLine($"基础URL: {url}"); + Console.WriteLine("请求参数:"); + foreach (var param in parameters) + { + Console.WriteLine($" {param.Key}: {param.Value}"); + } + + var signedUrl = signer.SignUrl(url, parameters, accessKeyId, secretKey, channelId); + Console.WriteLine("\n签名后的URL:"); + Console.WriteLine(signedUrl); + + var signedParams = signer.SignRequest(parameters, accessKeyId, secretKey, channelId); + Console.WriteLine("\n签名后的参数:"); + foreach (var param in signedParams) + { + Console.WriteLine($" {param.Key}: {param.Value}"); + } + + DemonstrateAlgorithms(parameters, accessKeyId, secretKey, channelId); + }, + algorithmOption, urlOption, paramOption, accessKeyIdOption, secretKeyOption, channelIdOption); + + return await rootCommand.InvokeAsync(args); + } + + /// + /// 演示不同算法的签名结果 + /// + private static void DemonstrateAlgorithms( + Dictionary parameters, + string accessKeyId, + string secretKey, + string channelId) + { + Console.WriteLine("\n不同算法的签名结果:"); + + var algorithms = new[] + { + SignatureAlgorithm.Md5, + SignatureAlgorithm.Sha1, + SignatureAlgorithm.Sha256, + SignatureAlgorithm.HmacSha256 + }; + + foreach (var alg in algorithms) + { + var options = new SignOptions { Algorithm = alg }; + var signer = new ApiSigner(options); + + // 添加必要的参数用于签名 + var signParams = new Dictionary(parameters) + { + ["AccessKeyId"] = accessKeyId, + ["channelId"] = channelId + }; + + var signature = signer.CalculateSignature(signParams, secretKey); + Console.WriteLine($" {alg}: {signature}"); + } + } + } +} \ No newline at end of file diff --git a/csharp/src/ApiSigner.sln b/csharp/src/ApiSigner.sln new file mode 100644 index 0000000..d76a004 --- /dev/null +++ b/csharp/src/ApiSigner.sln @@ -0,0 +1,30 @@ +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.5.33627.172 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ApiSigner", "ApiSigner\ApiSigner.csproj", "{6DD01F88-4A7B-40EC-B3D0-48F4D394B237}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ApiSigner.Cli", "ApiSigner.Cli\ApiSigner.Cli.csproj", "{D74C4FB8-5723-4C94-8D2C-9FB26716872E}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {6DD01F88-4A7B-40EC-B3D0-48F4D394B237}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {6DD01F88-4A7B-40EC-B3D0-48F4D394B237}.Debug|Any CPU.Build.0 = Debug|Any CPU + {6DD01F88-4A7B-40EC-B3D0-48F4D394B237}.Release|Any CPU.ActiveCfg = Release|Any CPU + {6DD01F88-4A7B-40EC-B3D0-48F4D394B237}.Release|Any CPU.Build.0 = Release|Any CPU + {D74C4FB8-5723-4C94-8D2C-9FB26716872E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D74C4FB8-5723-4C94-8D2C-9FB26716872E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D74C4FB8-5723-4C94-8D2C-9FB26716872E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D74C4FB8-5723-4C94-8D2C-9FB26716872E}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {6F7E3A89-97D4-4D4F-ACFF-A12F36CFCB3E} + EndGlobalSection +EndGlobal \ No newline at end of file diff --git a/csharp/src/ApiSigner/ApiSigner.cs b/csharp/src/ApiSigner/ApiSigner.cs new file mode 100644 index 0000000..52e6e02 --- /dev/null +++ b/csharp/src/ApiSigner/ApiSigner.cs @@ -0,0 +1,312 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Security.Cryptography; +using System.Text; +using System.Text.RegularExpressions; +using System.Web; + +namespace SoundForce.ApiSigner +{ + /// + /// API签名工具 + /// + public class ApiSigner + { + private readonly SignOptions _options; + private static readonly Regex AlphaNumericPattern = new Regex("^[a-zA-Z0-9]*$", RegexOptions.Compiled); + + /// + /// 创建API签名工具 + /// + /// 签名选项,如为null则使用默认选项 + public ApiSigner(SignOptions? options = null) + { + _options = options ?? new SignOptions(); + } + + /// + /// 生成随机字符串 + /// + /// 一个基于当前时间的随机字符串 + public string GenerateNonce() + { + return string.Concat(DateTimeOffset.UtcNow.ToUnixTimeMilliseconds().ToString(), + Guid.NewGuid().ToString("N").AsSpan(0, 8)); + } + + /// + /// 获取当前时间戳(毫秒) + /// + /// 当前的Unix时间戳(毫秒) + public long GetTimestamp() + { + return DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(); + } + + /// + /// 对请求进行签名 + /// + /// 请求参数 + /// 访问密钥ID + /// 密钥 + /// 合作渠道方ID + /// 添加了签名的完整参数 + public Dictionary SignRequest( + IDictionary parameters, + string accessKeyId, + string secretKey, + string channelId) + { + // 创建用于签名的参数副本 + var signParams = new Dictionary(parameters); + + // 添加认证参数 + var timestamp = GetTimestamp(); + signParams[_options.KeyName] = accessKeyId; + signParams[_options.ChannelIdName] = channelId; + signParams[_options.TimestampName] = timestamp.ToString(); + signParams[_options.NonceName] = GenerateNonce(); + + // 计算签名 + var signature = CalculateSignature(signParams, secretKey); + + // 添加签名到参数 + signParams[_options.SignatureName] = signature; + + return signParams; + } + + /// + /// 对URL进行签名 + /// + /// 基础URL地址 + /// 请求参数 + /// 访问密钥ID + /// 密钥 + /// 合作渠道方ID + /// 添加了签名的完整URL + public string SignUrl( + string baseUrl, + IDictionary parameters, + string accessKeyId, + string secretKey, + string channelId) + { + // 对请求签名 + var signedParams = SignRequest(parameters, accessKeyId, secretKey, channelId); + + // 构建URL查询字符串 + var queryString = BuildQueryString(signedParams); + + // 处理已有参数的URL + return baseUrl.Contains('?') ? $"{baseUrl}&{queryString}" : $"{baseUrl}?{queryString}"; + } + + /// + /// 构建URL查询字符串 + /// + /// 参数 + /// 查询字符串 + private string BuildQueryString(IDictionary parameters) + { + // 按键排序 + var sortedParams = parameters.OrderBy(p => p.Key); + + // 构建查询字符串 + var queryParts = + sortedParams.Select(p => $"{HttpUtility.UrlEncode(p.Key)}={HttpUtility.UrlEncode(p.Value)}"); + return string.Join("&", queryParts); + } + + /// + /// 计算签名 + /// + /// 请求参数 + /// 密钥 + /// 签名字符串 + public string CalculateSignature(IDictionary parameters, string secretKey) + { + // 按键名排序并生成规范化的请求字符串 + var signingString = CreateSigningString(parameters); + + // 添加密钥 + signingString = $"{signingString}&key={secretKey}"; + + // 使用指定算法计算签名 + return _options.Algorithm switch + { + SignatureAlgorithm.Md5 => CalculateMd5(signingString), + SignatureAlgorithm.Sha1 => CalculateSha1(signingString), + SignatureAlgorithm.Sha256 => CalculateSha256(signingString), + SignatureAlgorithm.HmacSha256 => CalculateHmacSha256(signingString, secretKey), + _ => CalculateMd5(signingString) + }; + } + + /// + /// 创建用于签名的规范化字符串 + /// + /// 请求参数 + /// 按键名排序并拼接的字符串 + public string CreateSigningString(IDictionary parameters) + { + // 排除签名参数 + var filteredParams = parameters + .Where(p => p.Key != _options.SignatureName) + .OrderBy(p => p.Key) + .Select(p => + { + var value = p.Value; + // 仅对非字母数字字符进行URL编码 + if (NeedsUrlEncode(value)) + { + value = HttpUtility.UrlEncode(value); + } + + return $"{p.Key}={value}"; + }); + + return string.Join("&", filteredParams); + } + + /// + /// 判断是否需要对字符串进行URL编码 + /// + /// 需要判断的字符串 + /// 如果包含非字母数字字符,返回true,否则返回false + private bool NeedsUrlEncode(string value) + { + return !AlphaNumericPattern.IsMatch(value); + } + + /// + /// 验证签名 + /// + /// 所有请求参数,包括签名 + /// 密钥 + /// 允许的最大时间差(毫秒) + /// 验证结果 + public SignVerifyResult VerifySignature(IDictionary parameters, string secretKey, + long maxAgeMs = 300000) + { + // 获取并验证必要参数 + if (!parameters.TryGetValue(_options.KeyName, out _)) + { + return SignVerifyResult.Failure($"缺少参数: {_options.KeyName}"); + } + + if (!parameters.TryGetValue(_options.ChannelIdName, out _)) + { + return SignVerifyResult.Failure($"缺少参数: {_options.ChannelIdName}"); + } + + if (!parameters.TryGetValue(_options.TimestampName, out var timestampStr)) + { + return SignVerifyResult.Failure($"缺少参数: {_options.TimestampName}"); + } + + // 验证时间戳 + if (!long.TryParse(timestampStr, out var timestamp)) + { + return SignVerifyResult.Failure("无效的时间戳"); + } + + var now = GetTimestamp(); + if (Math.Abs(now - timestamp) > maxAgeMs) + { + return SignVerifyResult.Failure("时间戳过期"); + } + + if (!parameters.TryGetValue(_options.NonceName, out _)) + { + return SignVerifyResult.Failure($"缺少参数: {_options.NonceName}"); + } + + // 这里可以插入nonce验证逻辑,防止重放攻击 + // 服务端需要维护一个时效性的nonce存储 + + if (!parameters.TryGetValue(_options.SignatureName, out var providedSignature)) + { + return SignVerifyResult.Failure($"缺少参数: {_options.SignatureName}"); + } + + // 计算签名并比较 + var expectedSignature = CalculateSignature(parameters, secretKey); + + return string.Equals(expectedSignature, providedSignature, StringComparison.OrdinalIgnoreCase) + ? SignVerifyResult.Success() + : SignVerifyResult.Failure("签名不匹配"); + } + + #region 签名算法实现 + + /// + /// 计算MD5签名 + /// + /// 内容 + /// MD5签名 + private string CalculateMd5(string content) + { + var inputBytes = Encoding.UTF8.GetBytes(content); + var hashBytes = MD5.HashData(inputBytes); + return BytesToHex(hashBytes); + } + + /// + /// 计算SHA1签名 + /// + /// 内容 + /// SHA1签名 + private string CalculateSha1(string content) + { + var inputBytes = Encoding.UTF8.GetBytes(content); + var hashBytes = SHA1.HashData(inputBytes); + return BytesToHex(hashBytes); + } + + /// + /// 计算SHA256签名 + /// + /// 内容 + /// SHA256签名 + private string CalculateSha256(string content) + { + var inputBytes = Encoding.UTF8.GetBytes(content); + var hashBytes = SHA256.HashData(inputBytes); + return BytesToHex(hashBytes); + } + + /// + /// 计算HMAC-SHA256签名 + /// + /// 内容 + /// 密钥 + /// HMAC-SHA256签名 + private string CalculateHmacSha256(string content, string key) + { + using var hmac = new HMACSHA256(Encoding.UTF8.GetBytes(key)); + var inputBytes = Encoding.UTF8.GetBytes(content); + var hashBytes = hmac.ComputeHash(inputBytes); + return BytesToHex(hashBytes); + } + + /// + /// 将字节数组转换为十六进制字符串 + /// + /// 字节数组 + /// 十六进制字符串 + private string BytesToHex(byte[] bytes) + { + var builder = new StringBuilder(bytes.Length * 2); + foreach (var b in bytes) + { + builder.Append(b.ToString("x2")); + } + + return builder.ToString(); + } + + #endregion + } +} \ No newline at end of file diff --git a/csharp/src/ApiSigner/ApiSigner.csproj b/csharp/src/ApiSigner/ApiSigner.csproj new file mode 100644 index 0000000..153a42a --- /dev/null +++ b/csharp/src/ApiSigner/ApiSigner.csproj @@ -0,0 +1,13 @@ + + + + net8.0 + SoundForce.ApiSigner + SoundForce.ApiSigner + 8.0 + enable + Sound Force + API签名计算与验证工具 + + + \ No newline at end of file diff --git a/csharp/src/ApiSigner/SignOptions.cs b/csharp/src/ApiSigner/SignOptions.cs new file mode 100644 index 0000000..9f825c3 --- /dev/null +++ b/csharp/src/ApiSigner/SignOptions.cs @@ -0,0 +1,55 @@ +namespace SoundForce.ApiSigner +{ + /// + /// 签名选项 + /// + public class SignOptions + { + /// + /// 签名算法 + /// + public SignatureAlgorithm Algorithm { get; set; } = SignatureAlgorithm.Md5; + + /// + /// AccessKeyId参数名 + /// + public string KeyName { get; set; } = "AccessKeyId"; + + /// + /// 合作渠道方ID参数名 + /// + public string ChannelIdName { get; set; } = "channelId"; + + /// + /// 时间戳参数名 + /// + public string TimestampName { get; set; } = "timestamp"; + + /// + /// 随机字符串参数名 + /// + public string NonceName { get; set; } = "nonce"; + + /// + /// 签名参数名 (注意:这里使用sign而不是signature) + /// + public string SignatureName { get; set; } = "sign"; + + /// + /// 创建默认签名选项的新实例 + /// + public SignOptions() + { + } + + /// + /// 返回表示当前对象的字符串 + /// + /// 表示当前对象的字符串 + public override string ToString() + { + return + $"SignOptions{{Algorithm={Algorithm}, KeyName='{KeyName}', ChannelIdName='{ChannelIdName}', TimestampName='{TimestampName}', NonceName='{NonceName}', SignatureName='{SignatureName}'}}"; + } + } +} \ No newline at end of file diff --git a/csharp/src/ApiSigner/SignVerifyResult.cs b/csharp/src/ApiSigner/SignVerifyResult.cs new file mode 100644 index 0000000..b2aed89 --- /dev/null +++ b/csharp/src/ApiSigner/SignVerifyResult.cs @@ -0,0 +1,42 @@ +namespace SoundForce.ApiSigner +{ + /// + /// 签名验证结果 + /// + public class SignVerifyResult + { + /// + /// 签名是否有效 + /// + public bool IsValid { get; } + + /// + /// 错误信息,如果没有错误则为null + /// + public string? Error { get; } + + /// + /// 创建表示成功的验证结果 + /// + /// 成功的验证结果 + public static SignVerifyResult Success() => new SignVerifyResult(true, null); + + /// + /// 创建表示失败的验证结果 + /// + /// 错误信息 + /// 失败的验证结果 + public static SignVerifyResult Failure(string error) => new SignVerifyResult(false, error); + + /// + /// 创建验证结果 + /// + /// 签名是否有效 + /// 错误信息 + private SignVerifyResult(bool isValid, string? error) + { + IsValid = isValid; + Error = error; + } + } +} \ No newline at end of file diff --git a/csharp/src/ApiSigner/SignatureAlgorithm.cs b/csharp/src/ApiSigner/SignatureAlgorithm.cs new file mode 100644 index 0000000..c791a71 --- /dev/null +++ b/csharp/src/ApiSigner/SignatureAlgorithm.cs @@ -0,0 +1,76 @@ +using System; + +namespace SoundForce.ApiSigner +{ + /// + /// 签名算法类型 + /// + public enum SignatureAlgorithm + { + /// + /// MD5算法(默认、最快) + /// + Md5, + + /// + /// SHA1算法 + /// + Sha1, + + /// + /// SHA256算法 + /// + Sha256, + + /// + /// HMAC-SHA256算法(最安全) + /// + HmacSha256 + } + + /// + /// 签名算法扩展方法 + /// + public static class SignatureAlgorithmExtensions + { + /// + /// 从字符串解析算法类型 + /// + /// 算法字符串 + /// 签名算法枚举 + /// 如果算法无效 + public static SignatureAlgorithm FromString(string? algorithm) + { + if (string.IsNullOrEmpty(algorithm)) + return SignatureAlgorithm.Md5; + + return algorithm.ToUpperInvariant() switch + { + "MD5" => SignatureAlgorithm.Md5, + "SHA1" => SignatureAlgorithm.Sha1, + "SHA256" => SignatureAlgorithm.Sha256, + "HMAC_SHA256" => SignatureAlgorithm.HmacSha256, + "HMACSHA256" => SignatureAlgorithm.HmacSha256, + "HMAC-SHA256" => SignatureAlgorithm.HmacSha256, + _ => throw new ArgumentException($"无效的签名算法: {algorithm}", nameof(algorithm)) + }; + } + + /// + /// 获取算法的字符串表示 + /// + /// 签名算法 + /// 算法的字符串表示 + public static string ToString(this SignatureAlgorithm algorithm) + { + return algorithm switch + { + SignatureAlgorithm.Md5 => "MD5", + SignatureAlgorithm.Sha1 => "SHA1", + SignatureAlgorithm.Sha256 => "SHA256", + SignatureAlgorithm.HmacSha256 => "HMAC-SHA256", + _ => "UNKNOWN" + }; + } + } +} \ No newline at end of file