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 请求

流程:

  1. 验证参数有效性
  2. 确定使用的 bucket(存储桶)
  3. 规范化对象键(文件路径)
  4. 构建 COS 主机地址
  5. URL 编码路径和查询参数
  6. 生成完整的请求 URI
  7. 添加 Host 头和 Authorization 头

2. BuildAuthorization 方法 - 构建授权签名

这是最核心的方法,实现了腾讯云 COS 的签名算法:

签名步骤:

  1. 生成时间戳: 当前时间和过期时间(默认5分钟)
  2. 构建签名字符串:
    • HTTP 方法(小写)
    • URI 路径
    • 规范化的查询字符串
    • 规范化的请求头
  3. 计算签名:
    • 使用 SHA1 对 HTTP 字符串进行哈希
    • 使用 HMAC-SHA1 和密钥生成签名密钥
    • 再次使用 HMAC-SHA1 生成最终签名
  4. 返回授权字符串: 包含算法、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 加密并转小写十六进制

安全特性

  1. 时间戳验证: 防止重放攻击
  2. HMAC-SHA1 签名: 确保请求未被篡改
  3. SecretKey 保护: 只在服务端使用,不暴露给客户端

使用场景

这个类通常用于:

  • 上传文件到腾讯云 COS
  • 下载私有文件
  • 删除对象
  • 其他需要签名认证的 COS API 操作

示例流程

HTTP请求 → 规范化 → 生成签名字符串 → HMAC加密 → 构建Authorization头 → 发送请求

这个实现严格遵循腾讯云 COS 的签名规范,确保请求能够通过服务端验证。

评论加载中...