- BceSigner: BCE v1 签名算法(HMAC-SHA256),与 baidubce-sdk 输出 byte-for-byte 一致 - BceSmsClient: HttpClient 直连百度云 SMS v3 API,自写签名,无第三方 SDK - BceSignerTests: 7 个单测,包括真实 AK/SK 对比百度 SDK debug 输出 暂未启用:SmsService 仍为开发桩,等签名运营商实名报备后接入真实发送
65 lines
2.5 KiB
C#
65 lines
2.5 KiB
C#
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;
|
||
}
|
||
}
|