using System.Globalization;
using System.Security.Cryptography;
using System.Text;
namespace Dpz.Core.Service.ObjectStorage.Services.Impl;
internal sealed class CosRequestSigner(S3ObjectStorageOptions options) : ICosRequestSigner
{
private static readonly TimeSpan DefaultExpiration = TimeSpan.FromMinutes(5);
public HttpRequestMessage CreateRequest(
HttpMethod method,
string pathToFile,
IReadOnlyCollection<CosQueryParameter> queryParameters,
string? bucket = null,
TimeSpan? expires = null
)
{
ArgumentNullException.ThrowIfNull(method);
ArgumentNullException.ThrowIfNull(queryParameters);
var effectiveBucket = string.IsNullOrWhiteSpace(bucket) ? options.Bucket : bucket;
if (string.IsNullOrWhiteSpace(effectiveBucket))
{
throw new ArgumentException("COS bucket is required", nameof(bucket));
}
var key = S3ObjectKeyHelper.NormalizeKey(pathToFile);
var host = BuildCosHost(effectiveBucket);
var encodedKey = EncodePath(key);
var query = BuildQueryString(queryParameters);
var requestUri = new Uri(
string.IsNullOrEmpty(query)
? $"https://{host}/{encodedKey}"
: $"https://{host}/{encodedKey}?{query}"
);
var request = new HttpRequestMessage(method, requestUri);
request.Headers.Host = host;
request.Headers.TryAddWithoutValidation(
"Authorization",
BuildAuthorization(method, encodedKey, host, queryParameters, expires)
);
return request;
}
private string BuildCosHost(string bucket)
{
return $"{bucket}.cos.{options.Region}.myqcloud.com";
}
private string BuildAuthorization(
HttpMethod method,
string encodedKey,
string host,
IReadOnlyCollection<CosQueryParameter> queryParameters,
TimeSpan? expires
)
{
var now = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
var end = now + (long)(expires ?? DefaultExpiration).TotalSeconds;
var signTime = $"{now};{end}";
var keyTime = signTime;
const string signAlgorithm = "sha1";
const string headerList = "host";
var urlParamList = BuildParameterList(queryParameters);
var canonicalHeaders = $"host={Encode(host)}";
var canonicalQuery = BuildCanonicalQueryString(queryParameters);
var httpString =
$"{method.Method.ToLowerInvariant()}\n/{encodedKey}\n{canonicalQuery}\n{canonicalHeaders}\n";
var stringToSign =
$"{signAlgorithm}\n{signTime}\n{Sha1Hex(Encoding.UTF8.GetBytes(httpString))}\n";
var signKey = HmacSha1Hex(options.SecretKey!, keyTime);
var signature = HmacSha1Hex(signKey, stringToSign);
return string.Join(
"&",
[
$"q-sign-algorithm={signAlgorithm}",
$"q-ak={Encode(options.SecretId!)}",
$"q-sign-time={signTime}",
$"q-key-time={keyTime}",
$"q-header-list={headerList}",
$"q-url-param-list={urlParamList}",
$"q-signature={signature}",
]
);
}
private static string BuildQueryString(IReadOnlyCollection<CosQueryParameter> parameters)
{
return string.Join(
"&",
parameters.Select(x =>
x.Value == null ? Encode(x.Name) : $"{Encode(x.Name)}={Encode(x.Value)}"
)
);
}
private static string BuildCanonicalQueryString(
IReadOnlyCollection<CosQueryParameter> parameters
)
{
return string.Join(
"&",
parameters
.Select(x => new
{
Name = x.Name.ToLowerInvariant(),
Value = x.Value ?? string.Empty,
})
.OrderBy(x => x.Name, StringComparer.Ordinal)
.Select(x => $"{Encode(x.Name)}={Encode(x.Value)}")
);
}
private static string BuildParameterList(IReadOnlyCollection<CosQueryParameter> parameters)
{
return string.Join(
";",
parameters.Select(x => x.Name.ToLowerInvariant()).Order(StringComparer.Ordinal)
);
}
private static string EncodePath(string key)
{
return string.Join("/", key.Split('/').Select(Encode));
}
private static string Encode(string value)
{
var bytes = Encoding.UTF8.GetBytes(value);
var builder = new StringBuilder();
foreach (var item in bytes)
{
var valueChar = (char)item;
if (
valueChar is >= 'A' and <= 'Z'
|| valueChar is >= 'a' and <= 'z'
|| valueChar is >= '0' and <= '9'
|| valueChar is '-' or '_' or '.' or '~'
)
{
builder.Append(valueChar);
}
else
{
builder.Append('%');
builder.Append(item.ToString("X2", CultureInfo.InvariantCulture));
}
}
return builder.ToString();
}
private static string Sha1Hex(byte[] bytes)
{
return Convert.ToHexString(SHA1.HashData(bytes)).ToLowerInvariant();
}
private static string HmacSha1Hex(string key, string message)
{
using var hmac = new HMACSHA1(Encoding.UTF8.GetBytes(key));
return Convert
.ToHexString(hmac.ComputeHash(Encoding.UTF8.GetBytes(message)))
.ToLowerInvariant();
}
}
⚠⚠ 以下内容为AI分析的结果,请根据实际情况进行判断。
代码解释
这是一个用于腾讯云对象存储(COS - Cloud Object Storage)请求签名的实现类。主要功能是为 COS API 请求生成符合规范的签名和授权头。
类结构
internal sealed class CosRequestSigner : ICosRequestSigner
- sealed: 密封类,不可被继承
- internal: 仅在程序集内可见
- 主构造函数: 接收
S3ObjectStorageOptions配置对象
核心功能
1. CreateRequest 方法 - 创建已签名的 HTTP 请求
public HttpRequestMessage CreateRequest(...)
作用: 构建一个完整的、带签名的 HTTP 请求
流程:
- 验证参数有效性
- 确定使用的 bucket(存储桶)
- 规范化对象键(文件路径)
- 构建 COS 主机地址
- URL 编码路径和查询参数
- 生成完整的请求 URI
- 添加 Host 头和 Authorization 头
2. BuildAuthorization 方法 - 构建授权签名
这是最核心的方法,实现了腾讯云 COS 的签名算法:
签名步骤:
- 生成时间戳: 当前时间和过期时间(默认5分钟)
- 构建签名字符串:
- HTTP 方法(小写)
- URI 路径
- 规范化的查询字符串
- 规范化的请求头
- 计算签名:
- 使用 SHA1 对 HTTP 字符串进行哈希
- 使用 HMAC-SHA1 和密钥生成签名密钥
- 再次使用 HMAC-SHA1 生成最终签名
- 返回授权字符串: 包含算法、AK、时间、头列表等信息
3. 辅助方法
BuildCosHost
"{bucket}.cos.{region}.myqcloud.com"
构建 COS 的域名格式
BuildQueryString / BuildCanonicalQueryString
BuildQueryString: 构建普通查询字符串BuildCanonicalQueryString: 构建规范化查询字符串(排序、小写)
Encode
自定义 URL 编码实现:
- 保留字符:
A-Z,a-z,0-9,-,_,.,~ - 其他字符转换为
%XX格式
Sha1Hex / HmacSha1Hex
Sha1Hex: SHA1 哈希并转小写十六进制HmacSha1Hex: HMAC-SHA1 加密并转小写十六进制
安全特性
- 时间戳验证: 防止重放攻击
- HMAC-SHA1 签名: 确保请求未被篡改
- SecretKey 保护: 只在服务端使用,不暴露给客户端
使用场景
这个类通常用于:
- 上传文件到腾讯云 COS
- 下载私有文件
- 删除对象
- 其他需要签名认证的 COS API 操作
示例流程
HTTP请求 → 规范化 → 生成签名字符串 → HMAC加密 → 构建Authorization头 → 发送请求
这个实现严格遵循腾讯云 COS 的签名规范,确保请求能够通过服务端验证。
AI 正在分析代码…
评论加载中...