【问题标题】:Refresh Token using Static HttpClient使用静态 HttpClient 刷新令牌
【发布时间】:2018-03-15 16:12:48
【问题描述】:

使用 VS 2017 .Net 4.5.2

我有以下课程

public static class MyHttpClient
{
    //fields
    private static Lazy<Task<HttpClient>> _Client = new Lazy<Task<HttpClient>>(async () =>
    {
        var client = new HttpClient();
        await InitClient(client).ConfigureAwait(false);
        return client;
    });

    //properties
    public static Task<HttpClient> ClientTask => _Client.Value;

    //methods
    private static async Task InitClient(HttpClient client)
    {
        //resey headers
        client.DefaultRequestHeaders.Clear();
        //Set base URL, NOT thread safe, which is why this method is only accessed via lazy initialization
        client.BaseAddress = new Uri(ConfigurationManager.AppSettings["baseAddress"]);//TODO: get from web.config? File? DB?
        //create new request to obtain auth token
        var request = new HttpRequestMessage(HttpMethod.Post, "/ouath2/token"); //TODO: get from web.config? File? DB? prob consts
        //Encode secret and ID 
        var byteArray = new UTF8Encoding().GetBytes($"{ConfigurationManager.AppSettings["ClientId"]}:{ConfigurationManager.AppSettings["ClientSecret"]}");
        //Form data
        var formData = new List<KeyValuePair<string, string>>();
        formData.Add(new KeyValuePair<string, string>("grant_type", "refresh_token"));
        formData.Add(new KeyValuePair<string, string>("refresh_token", ConfigurationManager.AppSettings["RefreshToken"]));
        //set content and headers
        request.Content = new FormUrlEncodedContent(formData);
        request.Headers.Authorization = new AuthenticationHeaderValue("Basic", Convert.ToBase64String(byteArray));
        //make request
        var result = await HttpPost(request, client).ConfigureAwait(false);
        //set bearer token
        client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", (string)result.access_token);

        //TODO: error handle
    }

    private static async Task<dynamic> HttpPost(HttpRequestMessage formData, HttpClient client)
    {
        using (var response = await client.SendAsync(formData).ConfigureAwait(false))
        {
            response.EnsureSuccessStatusCode();//TODO: handle this

            return await response.Content.ReadAsAsync<dynamic>().ConfigureAwait(false);
        }

    }
}

仍在进行中,但我遇到了障碍。

如果令牌只需要在应用程序生命周期中获取一次,这很好用,但是我正在与之交谈的 API 使用短暂的不记名令牌(15 分钟)。

由于我将 HttpClient 用作要重用的静态对象,因此我无法更改默认请求标头,因为它们不是线程安全的。但我需要每 15 分钟请求一次 Bearer 令牌。

在这种特定情况下,我将如何获得新的不记名令牌并设置默认标头?

【问题讨论】:

  • 通常刷新令牌的寿命比访问令牌长。如果是这样,一个常见的场景是在收到 HTTP403 后请求一个新的令牌。使用 http 客户端,这很容易在处理程序中实现。所以;你有一个实际的刷新令牌吗?如果是这样;持续时间够长吗?
  • 刷新令牌是长寿命的,用它来获取一个新的不记名令牌,不记名令牌是短命的(15分钟)。我把“刷新”而不是“承载”,错字是固定的

标签: c# rest dotnet-httpclient


【解决方案1】:

更新:添加 SemaphoreSlim 以“锁定”刷新事务

免责声明:未经测试,可能需要一些调整

注意 1:信号量需要在 try/catch/finaly 块中,以确保在抛出错误时释放。

注意 2:此版本会将刷新令牌调用排队,如果负载高,则会显着降低性能。 解决此问题;使用 bool 指示器检查是否发生了刷新。例如,这可能是一个静态布尔值

目标是仅在需要时使用刷新令牌。固定的时间间隔对您没有帮助,因为有一天,这个时间间隔可能会改变。处理此问题的正确方法是在出现 403 时重试。

您可以使用 HttpClientHandler 来处理您的 HttpClient。

重写 SendAsync,处理并重试 403。

为此,您需要constructor of httpclient

从我的(半)头顶看,它一定是这样的:

HttpMessageHandler

public class MyHttpMessageHandler : HttpMessageHandler
{
    private static SemaphoreSlim sem = new SemaphoreSlim(1);

    protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
    {  
    var response = await base.SendAsync(request, cancellationToken);

    //test for 403 and actual bearer token in initial request
    if (response.StatusCode == HttpStatusCode.Unauthorized &&
        request.Headers.Where(c => c.Key == "Authorization")
                .Select(c => c.Value)
                .Any(c => c.Any(p => p.StartsWith("Bearer"))))
        {

            //going to request refresh token: enter or start wait
            await sem.WaitAsync();

            //some typical stuff
            var pairs = new List<KeyValuePair<string, string>>
            {
                new KeyValuePair<string, string>("grant_type", "refresh_token"),
                new KeyValuePair<string, string>("refresh_token", yourRefreshToken),
                new KeyValuePair<string, string>("client_id", yourApplicationId),
            };

            //retry do to token request
            using ( var refreshResponse = await base.SendAsync(
                new HttpRequestMessage(HttpMethod.Post, 
                   new Uri(new Uri(Host), "Token")) 
                   { 
                      Content = new FormUrlEncodedContent(pairs) 
                   }, cancellationToken))
            {
                var rawResponse = await refreshResponse.Content.ReadAsStringAsync();
                var x = JsonConvert.DeserializeObject<RefreshToken>(rawResponse);

                //new tokens here!
                //x.access_token;
                //x.refresh_token;

                //to be sure
                request.Headers.Remove("Authorization");
                request.Headers.Add("Authorization", "Bearer " + x.access_token);

                //headers are set, so release:
                sem.Release();  

                //retry actual request with new tokens
                response = await base.SendAsync(request, cancellationToken);

            }
        }

        return response;
    }
}
}

发送示例,使用 SendAsync(也可以是 GetAsync)等。

public async Task<int> RegisterAsync(Model model)
{
    var response = await YourHttpClient
       .SendAsync(new HttpRequestMessage(HttpMethod.Post, new Uri(BaseUri, "api/Foo/Faa"))
    {  
        Content = new StringContent(
           JsonConvert.SerializeObject(model),
           Encoding.UTF8, "application/json")
    });

    var result = await response.Content.ReadAsStringAsync();
    return 0;
}

【讨论】:

  • 我使用 httpclient 的 postasync 和 getasync 方法。改用 sendasync 并改用 requestmessage 更有意义吗?设置请求消息头线程安全吗?
  • 对于你的HttpClientPostAsyncGetAsync 等,只是SendAsync 的外观/包装方法,所以,GetAsync/PostAsync 很好。 RequestMessage 是线程安全的,因为在某种程度上,它是本地的。我会稍微扩展一下这个例子
  • 第一次浏览应该可以工作,通过,验证失败,去获取新令牌并再次尝试,成功并返回。 1分钟后,根据您的示例完成了一个新请求,但又不存在承载令牌,因为我们只更改了单个请求的承载?还是我读错了?
  • 如果一切顺利,在 403 上,您将在new tokens here! 行获得一个新的访问令牌一个新的刷新令牌。您应该存储它们并在后续请求中使用它们,一般请求的访问令牌,刷新令牌请求的刷新令牌。
  • “您应该存储它们并在后续请求中使用它们”这就是我的观点,因为请求是我的调用方法中的新请求,并且我不能根据问题使用默认标头?
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2018-12-20
  • 2019-06-29
  • 2016-10-07
  • 2022-10-31
  • 2020-07-12
  • 1970-01-01
  • 2016-04-23
相关资源
最近更新 更多