Refresh Token using Static HttpClient

2019-07-03 23:56发布

问题:

Using VS 2017 .Net 4.5.2

I have the following class

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);
        }

    }
}

Still in progress but I've hit a snag.

This works fine if the token only needs to be fetched once in an applications life, however the API I'm talking to uses short lived bearer tokens (15mins).

As I'm using HttpClient as a static to be reused, I cannot change the Default request headers as they are not threadsafe. But I'm required to request a Bearer token every 15mins.

How would I achieve obtaining a new bearer token and setting the default header in this particular scenario?

回答1:

update: added the SemaphoreSlim to "lock" the refresh transaction

disclaimer: not tested, might need some tweaking

note 1: the semaphore needs to be in a try/catch/finaly block to ensure release if an error is thrown.

note 2: this version will queue the refresh token calls which significant degrades performance if load is high. To fix this; use a bool indicator to check if the refresh is occurred. This might be a static bool for example

The goal is to only use the refresh token if needed. A fixed interval won't help you because, one day, this interval might change. The correct way to handle this is to retry if a 403 occurs.

You can use a HttpClientHandler to work with your HttpClient.

Override the SendAsync, to handle and retry the 403.

For this to work you'll need this constructor of httpclient:

From the top of my (semi) head, it must be something like this:

the HttpMessageHandler

public class MyHttpMessageHandler : HttpMessageHandler
{
    private 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;
    }
}
}

send example, with SendAsync (could also be GetAsync) etc.

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;
}