feat: 新增百度云 SMS v3 签名算法与 HttpClient 直连实现

- BceSigner: BCE v1 签名算法(HMAC-SHA256),与 baidubce-sdk 输出 byte-for-byte 一致
- BceSmsClient: HttpClient 直连百度云 SMS v3 API,自写签名,无第三方 SDK
- BceSignerTests: 7 个单测,包括真实 AK/SK 对比百度 SDK debug 输出

暂未启用:SmsService 仍为开发桩,等签名运营商实名报备后接入真实发送
This commit is contained in:
MingNian
2026-07-23 18:58:33 +08:00
parent 28f704c98e
commit 32aae40b12
3 changed files with 224 additions and 0 deletions

View File

@@ -0,0 +1,49 @@
using System.Globalization;
using System.Security.Cryptography;
namespace Health.Infrastructure.Services;
/// <summary>
/// 百度云 BCE v1 签名算法(与 baidubce-sdk 算法一致)
/// </summary>
public static class BceSigner
{
private const int ExpirySeconds = 1800;
private const string SignedHeaders = "content-length;content-type;host;x-bce-date";
public static string BuildAuthorization(
string accessKeyId,
string secretAccessKey,
string method,
string path,
string host,
DateTime utcNow,
int contentLength,
string contentType)
{
var timestamp = utcNow.ToString("yyyy-MM-ddTHH:mm:ss'Z'", CultureInfo.InvariantCulture);
var canonicalHeaders = string.Join("\n",
$"content-length:{contentLength}",
$"content-type:{Normalize(contentType)}",
$"host:{Normalize(host)}",
$"x-bce-date:{Normalize(timestamp)}");
var canonicalRequest = $"{method}\n{path}\n\n{canonicalHeaders}";
var authStringPrefix = $"bce-auth-v1/{accessKeyId}/{timestamp}/{ExpirySeconds}";
var signingKeyDigest = HMACSHA256.HashData(
Encoding.UTF8.GetBytes(secretAccessKey),
Encoding.UTF8.GetBytes(authStringPrefix));
var signingKeyHex = Convert.ToHexString(signingKeyDigest).ToLowerInvariant();
var signature = HMACSHA256.HashData(
Encoding.UTF8.GetBytes(signingKeyHex),
Encoding.UTF8.GetBytes(canonicalRequest));
var signatureHex = Convert.ToHexString(signature).ToLowerInvariant();
return $"{authStringPrefix}/{SignedHeaders}/{signatureHex}";
}
private static string Normalize(string s) => Uri.EscapeDataString(s);
}

View File

@@ -0,0 +1,64 @@
using System.Globalization;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
namespace Health.Infrastructure.Services;
/// <summary>
/// 百度云 SMS v3 短信发送客户端HttpClient 直连 + BCE v1 签名)
/// </summary>
public sealed class BceSmsClient(HttpClient http, IConfiguration cfg, ILogger<BceSmsClient> log)
{
private const string Path = "/api/v3/sendSms";
private const string ContentType = "application/json; charset=utf-8";
private readonly HttpClient _http = http;
private readonly ILogger<BceSmsClient> _log = log;
private readonly string _ak = cfg["BAIDU_SMS_AK"] ?? "";
private readonly string _sk = cfg["BAIDU_SMS_SK"] ?? "";
private readonly string _host = cfg["BAIDU_SMS_HOST"] ?? "smsv3.bj.baidubce.com";
private readonly string _signatureId = cfg["BAIDU_SMS_SIGNATURE_ID"] ?? "";
private readonly string _templateId = cfg["BAIDU_SMS_TEMPLATE_ID"] ?? "";
public async Task<bool> SendAsync(string phone, string code, CancellationToken ct)
{
var payload = new
{
mobile = $"+86{phone}",
template = _templateId,
signatureId = _signatureId,
contentVar = new { SMSvCode = code }
};
var body = Encoding.UTF8.GetBytes(JsonSerializer.Serialize(payload));
using var req = new HttpRequestMessage(HttpMethod.Post, Path)
{
Content = new ByteArrayContent(body)
};
req.Content.Headers.TryAddWithoutValidation("Content-Type", ContentType);
req.Headers.Host = _host;
var now = DateTime.UtcNow;
var timestamp = now.ToString("yyyy-MM-ddTHH:mm:ss'Z'", CultureInfo.InvariantCulture);
req.Headers.Add("x-bce-date", timestamp);
var auth = BceSigner.BuildAuthorization(_ak, _sk, "POST", Path, _host, now, body.Length, ContentType);
req.Headers.TryAddWithoutValidation("Authorization", auth);
using var resp = await _http.SendAsync(req, ct);
var respBody = await resp.Content.ReadAsStringAsync(ct);
if (!resp.IsSuccessStatusCode)
{
_log.LogError("百度短信发送失败 HTTP {Status}: {Body}", resp.StatusCode, respBody);
return false;
}
using var doc = JsonDocument.Parse(respBody);
var respCode = doc.RootElement.GetProperty("code").GetString();
if (respCode != "1000")
{
_log.LogError("百度短信发送失败 code={Code}: {Body}", respCode, respBody);
return false;
}
return true;
}
}