我应该使用 IMemoryCache 来存储持有者令牌,还是针对 Azure 函数中的 401 错误实现 Polly 重试策略?

Should I use IMemoryCache to store the bearer token or implement a Polly retry policy for 401 errors in Azure Function?

提问人:Rakesh Kumar 提问时间:11/3/2023 最后编辑:Peter CsalaRakesh Kumar 更新时间:11/5/2023 访问量:60

问:

我正在使用服务总线主题触发器 Azure 函数,在向外部 API 发出 HTTP 请求时,我需要处理 401(未经授权)错误,例如刷新持有者令牌并重试失败的请求。

我正在考虑两种方法,但不确定哪种方法更合适。 我将不胜感激有关哪种方法最好的指导。

这是我的重试策略类,其中包含 401 错误的未经授权的重试策略。

public class PollyRetryPolicy : IPollyRetryPolicy
{
    private readonly ILogger<PollyRetryPolicy> _logger;
    private readonly IOptions<RetryPolicyConfigOptions> _options;
   

    public PollyRetryPolicy(ILogger<PollyRetryPolicy> logger,
        IOptions<RetryPolicyConfigOptions> options)
    {
        _logger = logger;
        _options = options;
        _jitterer = new Random();
    }

    public IAsyncPolicy GetUnauthorizedRetryPolicy()
    {
        var policy = Policy
            .Handle<HttpRequestException>(ex => ex.StatusCode == HttpStatusCode.Unauthorized)
            .WaitAndRetryAsync(
                _options.Value.MaxRetryAttempts,
                retryAttempt => TimeSpan.Zero); // Set the delay to zero milliseconds for immediate retry
        return policy;
    }
}

这是我的 api 客户端,我在其中尝试调用第一个持有者令牌终结点并使用 IMemoryCache 存储持有者令牌,然后使用此持有者令牌进行实际调用。

public class NotificationApiClient : INotificationApiClient
{
    
    private readonly ILogger<NotificationApiClient> _logger;
    private readonly IHttpClientFactory _httpClientFactory;
    private readonly IMemoryCache _memoryCache;
    private readonly NotificationApiConfigOption _notificationApiConfigOption;

    
    public NotificationApiClient(ILogger<NotificationApiClient> logger,
        IHttpClientFactory httpClientFactory,
        IMemoryCache memoryCache,
        IOptions<NotificationApiConfigOption> notificationApiConfigOption)
    {
        _logger = logger;
        _httpClientFactory = httpClientFactory;
        _memoryCache = memoryCache;
        _notificationApiConfigOption = notificationApiConfigOption.Value;
    }

    
    public async Task<bool> CheckDeliveryAccessAsync(int code)
    {
        try
        {
            
            var httpClient = _httpClientFactory.CreateClient();
            httpClient.BaseAddress = new Uri(_notificationApiConfigOption.BaseUri);
            
            // Get the bearer token
            string bearerToken = await GetNotificationAccessTokenAsync();
            httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", bearerToken);
            var apiResponse = await httpClient.GetAsync($"{_notificationApiConfigOption.CheckDeliveryAccessApiUri}?code={code}");
            if (apiResponse.StatusCode == HttpStatusCode.OK)
            {
                string resultContent = await apiResponse.Content.ReadAsStringAsync();
                var result = resultContent.AsPoco<CheckInvoiceDeliveryAccessResponse>();
                return result.Data.IsActive;
            }
            else
            {
                return false;
            }
        }
        catch (Exception ex)
        {
            
            throw;
        }
    }

   
    public async Task<string> GetNotificationAccessTokenAsync()
    {
        try
        {
            
            if (_memoryCache.TryGetValue("BearerToken", out string cachedBearerToken))
            {
                return cachedBearerToken;
            }
            var httpClient = _httpClientFactory.CreateClient();
            httpClient.BaseAddress = new Uri(_notificationApiConfigOption.BaseUri);
            
            var formData = new Dictionary<string, string>
            {
                { "clientId", _notificationApiConfigOption.ClientId },
                { "clientSecret", _notificationApiConfigOption.ClientSecret }
            };
            
            var content = new FormUrlEncodedContent(formData);
            var apiResponse = await httpClient.PostAsync($"/{_notificationApiConfigOption.AccessTokenUri}", content);
            
            if (apiResponse.StatusCode == HttpStatusCode.OK)
            {
                string resultContent = await apiResponse.Content.ReadAsStringAsync();
                
                var result = resultContent.AsPoco<NotificationAccessTokenResponse>();
                
                _memoryCache.Set("BearerToken", result.AccessToken, new MemoryCacheEntryOptions
                {
                    AbsoluteExpirationRelativeToNow = TimeSpan.FromSeconds(result.ExpiresIn)
                });
                
                return result.AccessToken;
            }
            else
            {
               
                return string.Empty;
            }
        }
        catch (Exception ex)
        {
            
            throw;
        }
    }
}
c# azure-functions 波莉 重试逻辑 内存缓存

评论

1赞 codebrane 11/3/2023
您能否只使用 AbsoluteExpirationRelativeToNow 并在每次向 API 请求之前对其进行检查?使用 oauth2 访问令牌时,我会存储到期日期/时间,如果对服务的下一个请求在到期时间的大约 2 分钟内,我会获得一个新令牌,并在调用服务之前使用令牌和新的到期时间更新缓存,因此如果您知道持有者令牌何时到期,则无需进行 401 检查。

答:

1赞 Peter Csala 11/3/2023 #1

如评论部分所述@codebrane,您还可以在新令牌过期之前主动检索新令牌。

主动和被动方法都有其优点和缺点。如果您的服务面向客户,我建议使用主动方法。使用此方法时,不应因服务令牌过期而重试用户请求。

如果服务之间的通信不是连续的,而是按需进行的,则响应式方法可能很有用。重试施加的延迟是可以接受的。

在这里,我详细介绍了 3 种不同的实现方法: 将 Polly 与 Named Client 结合使用 Refresh Token


关于这件作品的一点说明:

var policy = Policy
    .Handle<HttpRequestException>(ex => ex.StatusCode == HttpStatusCode.Unauthorized)
    .WaitAndRetryAsync(
        _options.Value.MaxRetryAttempts,
        retryAttempt => TimeSpan.Zero); // Set the delay to zero milliseconds for immediate retry

如果您不想在重试尝试之间等待,则可以使用RetryAsync

var policy = Policy
    .Handle<HttpRequestException>(ex => ex.StatusCode == HttpStatusCode.Unauthorized)
    .RetryAsync(_options.Value.MaxRetryAttempts);

更新 #1

我的应用程序不是面向客户的,它是一种后台工作,可以根据事件触发通知。所以在这种情况下,我应该使用主动还是应该使用被动?

一如既往,视情况而定。:)让我试着帮助你进行权衡分析。

反应式方法

优点

  • 它是按需触发的。如果没有流量,则不执行令牌刷新
  • 如果您的系统具有反应性,那么对这种方法进行推理就更容易了

缺点

  • 如果令牌检索/刷新通常比原始请求花费的时间更长,则增加的延迟(由于令牌服务调用)变得有形
  • 如果令牌在突发期间过期,则许多请求将暂停(排队),直到新令牌可用

积极主动的方法

优点

  • 从传入请求处理的角度来看,您的系统的行为就像您拥有长期存期的令牌一样
  • 根据令牌服务实现,它可能允许一次检索多个令牌(每个下游服务一个令牌)

缺点

  • 如果令牌服务是速率限制/强加配额的,那么这种方法比反应式方法更有可能超过限制
  • 可能还需要使用重试来修饰令牌服务调用,以克服暂时性故障

旁注:在这两种情况下,您都必须确保没有并发检索调用。换言之,您只能通过单个线程刷新令牌。通过积极主动的方法更容易保证这一点。

评论

0赞 Rakesh Kumar 11/5/2023
谢谢!彼得的建议。我的应用程序不是面向客户的,它是一种后台工作,可以根据事件触发通知。所以在这种情况下,我应该使用主动还是应该使用被动?
0赞 Peter Csala 11/5/2023
@RakeshKumar我已经更新了我的帖子以反映您的问题。请检查一下!
1赞 Rakesh Kumar 11/5/2023
谢谢!很多解释:)现在我明白了。