信号量释放后,确保字段仅由一个线程更新C#

时间:2019-02-04 17:17:10

标签: c# asp.net wcf dotnet-httpclient

我有一个DelegatingHandler,它在单例HttpClient的构造函数中传递。

此处理程序负责进行基本身份验证,以获取承载令牌,该令牌将用于后续请求,直到令牌过期。令牌过期后,将再次触发基本身份验证,以刷新令牌,依此类推。

public class MyMessageHandler : DelegatingHandler
{
    private readonly string baseAddress;
    private readonly string user;
    private readonly string pass;

    private readonly SemaphoreSlim sem;
    private Token token;

    public MyMessageHandler() : base()
    {
        // validation/assignment of baseAddress, userName, password
        // ..omitted for brevity

        sem = new SemaphoreSlim(1);
        // this is the first time, so get the token
        token = GetTokenAsync().GetAwaiter().GetResult();
    }

    protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
    {
        if (!request.Headers.Contains("Authorization"))
        {
            request.Headers.Add("Authorization", $"Bearer {token.AccessToken}");
        }

        var response = await base.SendAsync(request, cancellationToken).ConfigureAwait(false);

        // if a token refresh is needed
        if (response.StatusCode == HttpStatusCode.Unauthorized && whateverOtherCheckToTriggerTokenRefresh
        {
            try
            {
                // don't want multiple requests refreshing the token
                await sem.WaitAsync().ConfigureAwait(false);
                token = await GetTokenAsync().ConfigureAwait(false);

                // we have the token, now set the headers to the new values
                request.Headers.Remove("Authorization");
                request.Headers.Add("Authorization", $"Bearer {token.AccessToken}");

                // replay the request with the new token
                response = await base.SendAsync(request, cancellationToken).ConfigureAwait(false);
            }
            catch (Exception ex)
            {
            }
            finally
            {
                sem.Release();
            }
        }
        return response;
    }

    private async Task<Token> GetTokenAsync()
    {
        var authBytes = Encoding.UTF8.GetBytes($"{user}:{pass}");
        var basicAuthToken = Convert.ToBase64String(authBytes);

        var pairs = new List<KeyValuePair<string, string>>
        {
            new KeyValuePair<string, string>("grant_type", "client_credentials")
        };

        // get ourselves a token using basic auth
        var message = new HttpRequestMessage(HttpMethod.Post, new Uri(new Uri(baseAddress), "/token"))
        {
            Content = new FormUrlEncodedContent(pairs)
        };

        message.Headers.Authorization = new AuthenticationHeaderValue("Basic", basicAuthToken);

        var response = await base.SendAsync(message, new CancellationToken()).ConfigureAwait(false);

        response.EnsureSuccessStatusCode();

        var result = await response.Content.ReadAsStringAsync().ConfigureAwait(false);

       // return our token
       return JsonConvert.DeserializeObject<Token>(result);
    }
}

我在刷新/更新令牌步骤中使用了Semaphore,因为这是一个并发的WCF应用程序,并且我不希望多个请求都请求一个新令牌。刷新步骤完成后,token的{​​{1}}字段将设置为MyMessageHandler返回的新Token对象,并释放信号量,因此其他等待的请求将进入代码块。

1)现在如何防止在GetTokenAsync()行上等待的请求自己执行刷新令牌步骤?

2)我是否应该担心传入请求在信号灯中被更新时试图抢占await sem.WaitAsync().ConfigureAwait(false);字段的值?如果是这样,我应该在获取新令牌后立即执行token之类的事情吗?

更新

在Damien_The_Unbeliever的答案之后,这是我实现他的答案的方法。我仍然很难理解如何实现答案。

Interlocked.Exchange(ref token, newlyFetchedToken);

1 个答案:

答案 0 :(得分:2)

不考虑其他一些问题(空catch块以及单个HttpRequestMessage实例实际上可以被发送多次的假设),我通常在字段中遇到的是{ {1}}。

在传出请求中,将此字段的副本抓取到局部变量中,然后Task<Token>实际标记。如果您收到未经授权的回复,请创建一个新的await并执行一个TaskCompletionSource以交换该字段中的InterlockedCompareExchange。如果交换成功,则现在是您的责任来续订令牌并完成Task

但是,如果Task失败,则意味着其他人已经或正在替换令牌。循环回到您的方法顶部,并InterlockedCompareExchange来代替这个新的await

没有信号灯,很简单的行为就可以推断出来。甚至在您尝试使用新Task<Token>时,它也可能已经过期-因此,请准备多次循环并制定一些策略,以便在出现其他情况时不会永远循环玩,令牌没有真正的问题。

并删除空的Token