使用静态 HttpClient 刷新令牌
Refresh Token using Static HttpClient
使用 VS 2017 .Net 4.5.2
我有以下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);
}
}
}
仍在进行中,但我遇到了障碍。
如果令牌在应用程序生命周期中只需要获取一次,则此方法工作正常,但是我正在与之交谈的 API 使用短暂的不记名令牌(15 分钟)。
由于我将 HttpClient 用作要重复使用的静态,因此我无法更改默认请求 headers,因为它们不是线程安全的。但是我需要每 15 分钟请求一个 Bearer 令牌。
在这种特定情况下,我将如何获得新的不记名令牌并设置默认值 header?
更新:将 SemaphoreSlim 添加到 "lock" 刷新事务
免责声明:未经测试,可能需要一些调整
注意 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;
}
使用 VS 2017 .Net 4.5.2
我有以下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);
}
}
}
仍在进行中,但我遇到了障碍。
如果令牌在应用程序生命周期中只需要获取一次,则此方法工作正常,但是我正在与之交谈的 API 使用短暂的不记名令牌(15 分钟)。
由于我将 HttpClient 用作要重复使用的静态,因此我无法更改默认请求 headers,因为它们不是线程安全的。但是我需要每 15 分钟请求一个 Bearer 令牌。
在这种特定情况下,我将如何获得新的不记名令牌并设置默认值 header?
更新:将 SemaphoreSlim 添加到 "lock" 刷新事务
免责声明:未经测试,可能需要一些调整
注意 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;
}