✨ 添加C#实现的API签名工具,包括核心库、命令行工具和使用文档,支持多种签名算法和环境变量配置。
This commit is contained in:
180
csharp/README.md
Normal file
180
csharp/README.md
Normal file
@@ -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<string, string>(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`文件或环境变量读取配置
|
||||
23
csharp/src/ApiSigner.Cli/ApiSigner.Cli.csproj
Normal file
23
csharp/src/ApiSigner.Cli/ApiSigner.Cli.csproj
Normal file
@@ -0,0 +1,23 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<RootNamespace>SoundForce.ApiSigner.Cli</RootNamespace>
|
||||
<AssemblyName>SoundForce.ApiSigner.Cli</AssemblyName>
|
||||
<LangVersion>8.0</LangVersion>
|
||||
<Nullable>enable</Nullable>
|
||||
<Authors>Sound Force</Authors>
|
||||
<Description>API签名工具命令行接口</Description>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="DotNetEnv" Version="3.1.1" />
|
||||
<PackageReference Include="System.CommandLine" Version="2.0.0-beta4.22272.1" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\ApiSigner\ApiSigner.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
159
csharp/src/ApiSigner.Cli/Program.cs
Normal file
159
csharp/src/ApiSigner.Cli/Program.cs
Normal file
@@ -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<int> Main(string[] args)
|
||||
{
|
||||
var rootCommand = new RootCommand("API签名工具");
|
||||
|
||||
var algorithmOption = new Option<string>(
|
||||
aliases: new[] { "--algorithm", "-a" },
|
||||
description: "签名算法: MD5, SHA1, SHA256, HMAC-SHA256",
|
||||
getDefaultValue: () => Environment.GetEnvironmentVariable("SIGN_ALGORITHM") ?? "MD5");
|
||||
|
||||
var urlOption = new Option<string>(
|
||||
aliases: new[] { "--url", "-u" },
|
||||
description: "基础URL地址",
|
||||
getDefaultValue: () =>
|
||||
Environment.GetEnvironmentVariable("API_BASE_URL") ?? "https://api.example.com/v1/data");
|
||||
|
||||
var paramOption = new Option<string[]>(
|
||||
aliases: new[] { "--param", "-p" },
|
||||
description: "请求参数,格式为key=value,可多次指定")
|
||||
{
|
||||
AllowMultipleArgumentsPerToken = true
|
||||
};
|
||||
|
||||
var accessKeyIdOption = new Option<string>(
|
||||
aliases: new[] { "--key", "-k" },
|
||||
description: "访问密钥ID",
|
||||
getDefaultValue: () => Environment.GetEnvironmentVariable("ACCESS_KEY_ID") ?? "test-access-key-id");
|
||||
|
||||
var secretKeyOption = new Option<string>(
|
||||
aliases: new[] { "--secret", "-s" },
|
||||
description: "密钥",
|
||||
getDefaultValue: () => Environment.GetEnvironmentVariable("SECRET_KEY") ?? "test-secret-key");
|
||||
|
||||
var channelIdOption = new Option<string>(
|
||||
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<string, string>(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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 演示不同算法的签名结果
|
||||
/// </summary>
|
||||
private static void DemonstrateAlgorithms(
|
||||
Dictionary<string, string> 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<string, string>(parameters)
|
||||
{
|
||||
["AccessKeyId"] = accessKeyId,
|
||||
["channelId"] = channelId
|
||||
};
|
||||
|
||||
var signature = signer.CalculateSignature(signParams, secretKey);
|
||||
Console.WriteLine($" {alg}: {signature}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
30
csharp/src/ApiSigner.sln
Normal file
30
csharp/src/ApiSigner.sln
Normal file
@@ -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
|
||||
312
csharp/src/ApiSigner/ApiSigner.cs
Normal file
312
csharp/src/ApiSigner/ApiSigner.cs
Normal file
@@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// API签名工具
|
||||
/// </summary>
|
||||
public class ApiSigner
|
||||
{
|
||||
private readonly SignOptions _options;
|
||||
private static readonly Regex AlphaNumericPattern = new Regex("^[a-zA-Z0-9]*$", RegexOptions.Compiled);
|
||||
|
||||
/// <summary>
|
||||
/// 创建API签名工具
|
||||
/// </summary>
|
||||
/// <param name="options">签名选项,如为null则使用默认选项</param>
|
||||
public ApiSigner(SignOptions? options = null)
|
||||
{
|
||||
_options = options ?? new SignOptions();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 生成随机字符串
|
||||
/// </summary>
|
||||
/// <returns>一个基于当前时间的随机字符串</returns>
|
||||
public string GenerateNonce()
|
||||
{
|
||||
return string.Concat(DateTimeOffset.UtcNow.ToUnixTimeMilliseconds().ToString(),
|
||||
Guid.NewGuid().ToString("N").AsSpan(0, 8));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取当前时间戳(毫秒)
|
||||
/// </summary>
|
||||
/// <returns>当前的Unix时间戳(毫秒)</returns>
|
||||
public long GetTimestamp()
|
||||
{
|
||||
return DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 对请求进行签名
|
||||
/// </summary>
|
||||
/// <param name="parameters">请求参数</param>
|
||||
/// <param name="accessKeyId">访问密钥ID</param>
|
||||
/// <param name="secretKey">密钥</param>
|
||||
/// <param name="channelId">合作渠道方ID</param>
|
||||
/// <returns>添加了签名的完整参数</returns>
|
||||
public Dictionary<string, string> SignRequest(
|
||||
IDictionary<string, string> parameters,
|
||||
string accessKeyId,
|
||||
string secretKey,
|
||||
string channelId)
|
||||
{
|
||||
// 创建用于签名的参数副本
|
||||
var signParams = new Dictionary<string, string>(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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 对URL进行签名
|
||||
/// </summary>
|
||||
/// <param name="baseUrl">基础URL地址</param>
|
||||
/// <param name="parameters">请求参数</param>
|
||||
/// <param name="accessKeyId">访问密钥ID</param>
|
||||
/// <param name="secretKey">密钥</param>
|
||||
/// <param name="channelId">合作渠道方ID</param>
|
||||
/// <returns>添加了签名的完整URL</returns>
|
||||
public string SignUrl(
|
||||
string baseUrl,
|
||||
IDictionary<string, string> 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}";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 构建URL查询字符串
|
||||
/// </summary>
|
||||
/// <param name="parameters">参数</param>
|
||||
/// <returns>查询字符串</returns>
|
||||
private string BuildQueryString(IDictionary<string, string> 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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 计算签名
|
||||
/// </summary>
|
||||
/// <param name="parameters">请求参数</param>
|
||||
/// <param name="secretKey">密钥</param>
|
||||
/// <returns>签名字符串</returns>
|
||||
public string CalculateSignature(IDictionary<string, string> 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)
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 创建用于签名的规范化字符串
|
||||
/// </summary>
|
||||
/// <param name="parameters">请求参数</param>
|
||||
/// <returns>按键名排序并拼接的字符串</returns>
|
||||
public string CreateSigningString(IDictionary<string, string> 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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 判断是否需要对字符串进行URL编码
|
||||
/// </summary>
|
||||
/// <param name="value">需要判断的字符串</param>
|
||||
/// <returns>如果包含非字母数字字符,返回true,否则返回false</returns>
|
||||
private bool NeedsUrlEncode(string value)
|
||||
{
|
||||
return !AlphaNumericPattern.IsMatch(value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 验证签名
|
||||
/// </summary>
|
||||
/// <param name="parameters">所有请求参数,包括签名</param>
|
||||
/// <param name="secretKey">密钥</param>
|
||||
/// <param name="maxAgeMs">允许的最大时间差(毫秒)</param>
|
||||
/// <returns>验证结果</returns>
|
||||
public SignVerifyResult VerifySignature(IDictionary<string, string> 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 签名算法实现
|
||||
|
||||
/// <summary>
|
||||
/// 计算MD5签名
|
||||
/// </summary>
|
||||
/// <param name="content">内容</param>
|
||||
/// <returns>MD5签名</returns>
|
||||
private string CalculateMd5(string content)
|
||||
{
|
||||
var inputBytes = Encoding.UTF8.GetBytes(content);
|
||||
var hashBytes = MD5.HashData(inputBytes);
|
||||
return BytesToHex(hashBytes);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 计算SHA1签名
|
||||
/// </summary>
|
||||
/// <param name="content">内容</param>
|
||||
/// <returns>SHA1签名</returns>
|
||||
private string CalculateSha1(string content)
|
||||
{
|
||||
var inputBytes = Encoding.UTF8.GetBytes(content);
|
||||
var hashBytes = SHA1.HashData(inputBytes);
|
||||
return BytesToHex(hashBytes);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 计算SHA256签名
|
||||
/// </summary>
|
||||
/// <param name="content">内容</param>
|
||||
/// <returns>SHA256签名</returns>
|
||||
private string CalculateSha256(string content)
|
||||
{
|
||||
var inputBytes = Encoding.UTF8.GetBytes(content);
|
||||
var hashBytes = SHA256.HashData(inputBytes);
|
||||
return BytesToHex(hashBytes);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 计算HMAC-SHA256签名
|
||||
/// </summary>
|
||||
/// <param name="content">内容</param>
|
||||
/// <param name="key">密钥</param>
|
||||
/// <returns>HMAC-SHA256签名</returns>
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 将字节数组转换为十六进制字符串
|
||||
/// </summary>
|
||||
/// <param name="bytes">字节数组</param>
|
||||
/// <returns>十六进制字符串</returns>
|
||||
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
|
||||
}
|
||||
}
|
||||
13
csharp/src/ApiSigner/ApiSigner.csproj
Normal file
13
csharp/src/ApiSigner/ApiSigner.csproj
Normal file
@@ -0,0 +1,13 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<RootNamespace>SoundForce.ApiSigner</RootNamespace>
|
||||
<AssemblyName>SoundForce.ApiSigner</AssemblyName>
|
||||
<LangVersion>8.0</LangVersion>
|
||||
<Nullable>enable</Nullable>
|
||||
<Authors>Sound Force</Authors>
|
||||
<Description>API签名计算与验证工具</Description>
|
||||
</PropertyGroup>
|
||||
|
||||
</Project>
|
||||
55
csharp/src/ApiSigner/SignOptions.cs
Normal file
55
csharp/src/ApiSigner/SignOptions.cs
Normal file
@@ -0,0 +1,55 @@
|
||||
namespace SoundForce.ApiSigner
|
||||
{
|
||||
/// <summary>
|
||||
/// 签名选项
|
||||
/// </summary>
|
||||
public class SignOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// 签名算法
|
||||
/// </summary>
|
||||
public SignatureAlgorithm Algorithm { get; set; } = SignatureAlgorithm.Md5;
|
||||
|
||||
/// <summary>
|
||||
/// AccessKeyId参数名
|
||||
/// </summary>
|
||||
public string KeyName { get; set; } = "AccessKeyId";
|
||||
|
||||
/// <summary>
|
||||
/// 合作渠道方ID参数名
|
||||
/// </summary>
|
||||
public string ChannelIdName { get; set; } = "channelId";
|
||||
|
||||
/// <summary>
|
||||
/// 时间戳参数名
|
||||
/// </summary>
|
||||
public string TimestampName { get; set; } = "timestamp";
|
||||
|
||||
/// <summary>
|
||||
/// 随机字符串参数名
|
||||
/// </summary>
|
||||
public string NonceName { get; set; } = "nonce";
|
||||
|
||||
/// <summary>
|
||||
/// 签名参数名 (注意:这里使用sign而不是signature)
|
||||
/// </summary>
|
||||
public string SignatureName { get; set; } = "sign";
|
||||
|
||||
/// <summary>
|
||||
/// 创建默认签名选项的新实例
|
||||
/// </summary>
|
||||
public SignOptions()
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 返回表示当前对象的字符串
|
||||
/// </summary>
|
||||
/// <returns>表示当前对象的字符串</returns>
|
||||
public override string ToString()
|
||||
{
|
||||
return
|
||||
$"SignOptions{{Algorithm={Algorithm}, KeyName='{KeyName}', ChannelIdName='{ChannelIdName}', TimestampName='{TimestampName}', NonceName='{NonceName}', SignatureName='{SignatureName}'}}";
|
||||
}
|
||||
}
|
||||
}
|
||||
42
csharp/src/ApiSigner/SignVerifyResult.cs
Normal file
42
csharp/src/ApiSigner/SignVerifyResult.cs
Normal file
@@ -0,0 +1,42 @@
|
||||
namespace SoundForce.ApiSigner
|
||||
{
|
||||
/// <summary>
|
||||
/// 签名验证结果
|
||||
/// </summary>
|
||||
public class SignVerifyResult
|
||||
{
|
||||
/// <summary>
|
||||
/// 签名是否有效
|
||||
/// </summary>
|
||||
public bool IsValid { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 错误信息,如果没有错误则为null
|
||||
/// </summary>
|
||||
public string? Error { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 创建表示成功的验证结果
|
||||
/// </summary>
|
||||
/// <returns>成功的验证结果</returns>
|
||||
public static SignVerifyResult Success() => new SignVerifyResult(true, null);
|
||||
|
||||
/// <summary>
|
||||
/// 创建表示失败的验证结果
|
||||
/// </summary>
|
||||
/// <param name="error">错误信息</param>
|
||||
/// <returns>失败的验证结果</returns>
|
||||
public static SignVerifyResult Failure(string error) => new SignVerifyResult(false, error);
|
||||
|
||||
/// <summary>
|
||||
/// 创建验证结果
|
||||
/// </summary>
|
||||
/// <param name="isValid">签名是否有效</param>
|
||||
/// <param name="error">错误信息</param>
|
||||
private SignVerifyResult(bool isValid, string? error)
|
||||
{
|
||||
IsValid = isValid;
|
||||
Error = error;
|
||||
}
|
||||
}
|
||||
}
|
||||
76
csharp/src/ApiSigner/SignatureAlgorithm.cs
Normal file
76
csharp/src/ApiSigner/SignatureAlgorithm.cs
Normal file
@@ -0,0 +1,76 @@
|
||||
using System;
|
||||
|
||||
namespace SoundForce.ApiSigner
|
||||
{
|
||||
/// <summary>
|
||||
/// 签名算法类型
|
||||
/// </summary>
|
||||
public enum SignatureAlgorithm
|
||||
{
|
||||
/// <summary>
|
||||
/// MD5算法(默认、最快)
|
||||
/// </summary>
|
||||
Md5,
|
||||
|
||||
/// <summary>
|
||||
/// SHA1算法
|
||||
/// </summary>
|
||||
Sha1,
|
||||
|
||||
/// <summary>
|
||||
/// SHA256算法
|
||||
/// </summary>
|
||||
Sha256,
|
||||
|
||||
/// <summary>
|
||||
/// HMAC-SHA256算法(最安全)
|
||||
/// </summary>
|
||||
HmacSha256
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 签名算法扩展方法
|
||||
/// </summary>
|
||||
public static class SignatureAlgorithmExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// 从字符串解析算法类型
|
||||
/// </summary>
|
||||
/// <param name="algorithm">算法字符串</param>
|
||||
/// <returns>签名算法枚举</returns>
|
||||
/// <exception cref="ArgumentException">如果算法无效</exception>
|
||||
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))
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取算法的字符串表示
|
||||
/// </summary>
|
||||
/// <param name="algorithm">签名算法</param>
|
||||
/// <returns>算法的字符串表示</returns>
|
||||
public static string ToString(this SignatureAlgorithm algorithm)
|
||||
{
|
||||
return algorithm switch
|
||||
{
|
||||
SignatureAlgorithm.Md5 => "MD5",
|
||||
SignatureAlgorithm.Sha1 => "SHA1",
|
||||
SignatureAlgorithm.Sha256 => "SHA256",
|
||||
SignatureAlgorithm.HmacSha256 => "HMAC-SHA256",
|
||||
_ => "UNKNOWN"
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user