1111
This commit is contained in:
@@ -0,0 +1,194 @@
|
||||
using Aliyun.Credentials.Models;
|
||||
using Aliyun.OSS;
|
||||
using CredentialClient = Aliyun.Credentials.Client;
|
||||
|
||||
namespace SimpleAdmin.System;
|
||||
|
||||
/// <summary>
|
||||
/// 在单机进程内缓存阿里云 STS 凭证及 OSS 客户端。
|
||||
/// </summary>
|
||||
public sealed class AliyunOssClientProvider : IAliyunOssClientProvider, IDisposable
|
||||
{
|
||||
/// <summary>
|
||||
/// STS 临时凭证有效期,单位为秒。
|
||||
/// </summary>
|
||||
private const int RoleSessionExpirationSeconds = 3600;
|
||||
|
||||
/// <summary>
|
||||
/// 单次刷新失败时的最大尝试次数。
|
||||
/// </summary>
|
||||
private const int MaxRefreshAttempts = 3;
|
||||
|
||||
/// <summary>
|
||||
/// 在凭证到期前提前刷新,官方凭证客户端会继续维护其内部缓存。
|
||||
/// </summary>
|
||||
private static readonly TimeSpan RefreshAhead = TimeSpan.FromMinutes(2);
|
||||
|
||||
/// <summary>
|
||||
/// 防止多个上传或下载请求同时刷新临时凭证。
|
||||
/// </summary>
|
||||
private readonly SemaphoreSlim _refreshLock = new(1, 1);
|
||||
|
||||
private readonly ILogger<AliyunOssClientProvider> _logger;
|
||||
private readonly OssSettingOptions _setting;
|
||||
private readonly string _endpoint;
|
||||
private readonly CredentialClient _credentialClient;
|
||||
private OssClient _cachedClient;
|
||||
private DateTimeOffset _expiresAt = DateTimeOffset.MinValue;
|
||||
|
||||
public AliyunOssClientProvider(ILogger<AliyunOssClientProvider> logger)
|
||||
{
|
||||
_logger = logger;
|
||||
_setting = GetAndValidateSetting();
|
||||
_endpoint = NormalizeEndpoint(_setting.EndPoint);
|
||||
|
||||
if (string.IsNullOrWhiteSpace(_setting.RoleArn))
|
||||
{
|
||||
_cachedClient = new OssClient(
|
||||
_endpoint,
|
||||
_setting.AccessKeyID,
|
||||
_setting.AccessKeySecret);
|
||||
_expiresAt = DateTimeOffset.MaxValue;
|
||||
return;
|
||||
}
|
||||
|
||||
var credentialConfig = new Config
|
||||
{
|
||||
Type = "ram_role_arn",
|
||||
AccessKeyId = _setting.AccessKeyID,
|
||||
AccessKeySecret = _setting.AccessKeySecret,
|
||||
RoleArn = _setting.RoleArn,
|
||||
RoleSessionName = string.IsNullOrWhiteSpace(_setting.RoleSessionName)
|
||||
? "onboardx-server"
|
||||
: _setting.RoleSessionName,
|
||||
RoleSessionExpiration = RoleSessionExpirationSeconds,
|
||||
STSEndpoint = _setting.StsEndpoint,
|
||||
ConnectTimeout = 10000,
|
||||
Timeout = 10000
|
||||
};
|
||||
_credentialClient = new CredentialClient(credentialConfig);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<OssClient> GetClientAsync()
|
||||
{
|
||||
if (_credentialClient == null)
|
||||
return _cachedClient;
|
||||
|
||||
var now = DateTimeOffset.UtcNow;
|
||||
if (_cachedClient != null && now < _expiresAt - RefreshAhead)
|
||||
return _cachedClient;
|
||||
|
||||
await _refreshLock.WaitAsync();
|
||||
try
|
||||
{
|
||||
now = DateTimeOffset.UtcNow;
|
||||
if (_cachedClient != null && now < _expiresAt - RefreshAhead)
|
||||
return _cachedClient;
|
||||
|
||||
try
|
||||
{
|
||||
var credential = await GetCredentialWithRetryAsync();
|
||||
var credentialIncomplete = credential == null
|
||||
|| string.IsNullOrWhiteSpace(credential.AccessKeyId)
|
||||
|| string.IsNullOrWhiteSpace(credential.AccessKeySecret)
|
||||
|| string.IsNullOrWhiteSpace(credential.SecurityToken);
|
||||
if (credentialIncomplete)
|
||||
throw Oops.Bah("获取阿里云 OSS 临时凭证失败");
|
||||
|
||||
_cachedClient = new OssClient(
|
||||
_endpoint,
|
||||
credential.AccessKeyId,
|
||||
credential.AccessKeySecret,
|
||||
credential.SecurityToken);
|
||||
_expiresAt = ResolveExpiration(credential.Expiration);
|
||||
return _cachedClient;
|
||||
}
|
||||
catch (Exception exception) when (
|
||||
_cachedClient != null && DateTimeOffset.UtcNow < _expiresAt)
|
||||
{
|
||||
// 刷新失败时继续使用尚未过期的旧凭证,避免短时网络波动中断业务。
|
||||
_logger.LogWarning(
|
||||
exception,
|
||||
"刷新阿里云 OSS STS 凭证失败,将继续使用未过期的内存凭证,有效期至 {ExpiresAt}",
|
||||
_expiresAt);
|
||||
return _cachedClient;
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
_refreshLock.Release();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取凭证时执行有限重试,避免一次瞬时 TLS 抖动直接中断业务。
|
||||
/// </summary>
|
||||
private async Task<CredentialModel> GetCredentialWithRetryAsync()
|
||||
{
|
||||
for (var attempt = 1; ; attempt++)
|
||||
{
|
||||
try
|
||||
{
|
||||
return await _credentialClient.GetCredentialAsync();
|
||||
}
|
||||
catch (Exception exception) when (attempt < MaxRefreshAttempts)
|
||||
{
|
||||
_logger.LogWarning(
|
||||
exception,
|
||||
"第 {Attempt} 次获取阿里云 OSS STS 凭证失败,准备重试",
|
||||
attempt);
|
||||
await Task.Delay(TimeSpan.FromMilliseconds(200 * attempt));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 将官方凭证返回的 Unix 时间戳转换为到期时间。
|
||||
/// </summary>
|
||||
private static DateTimeOffset ResolveExpiration(long expiration)
|
||||
{
|
||||
if (expiration <= 0)
|
||||
return DateTimeOffset.UtcNow.AddMinutes(30);
|
||||
|
||||
return expiration > 253402300799
|
||||
? DateTimeOffset.FromUnixTimeMilliseconds(expiration)
|
||||
: DateTimeOffset.FromUnixTimeSeconds(expiration);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 读取并校验创建客户端所需的配置。
|
||||
/// </summary>
|
||||
private static OssSettingOptions GetAndValidateSetting()
|
||||
{
|
||||
var setting = App.GetConfig<OssSettingOptions>("OssSetting", true);
|
||||
var configurationIncomplete = setting == null
|
||||
|| string.IsNullOrWhiteSpace(setting.AccessKeyID)
|
||||
|| string.IsNullOrWhiteSpace(setting.AccessKeySecret)
|
||||
|| string.IsNullOrWhiteSpace(setting.EndPoint);
|
||||
if (configurationIncomplete)
|
||||
throw Oops.Bah("阿里云 OSS 配置不完整");
|
||||
|
||||
return setting;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 统一补充 OSS Endpoint 的 HTTPS 协议。
|
||||
/// </summary>
|
||||
private static string NormalizeEndpoint(string endpoint)
|
||||
{
|
||||
endpoint = endpoint.Trim();
|
||||
if (!endpoint.StartsWith("http://", StringComparison.OrdinalIgnoreCase)
|
||||
&& !endpoint.StartsWith("https://", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
endpoint = "https://" + endpoint;
|
||||
}
|
||||
|
||||
return endpoint;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_refreshLock.Dispose();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user