【发布时间】:2016-09-20 10:59:11
【问题描述】:
在我的一个应用程序中,我已与 Infusionsoft 集成,并且访问令牌会在特定时间后过期。
现在前端发出多个请求以获取不同的数据。当令牌过期时,它会刷新令牌并获取新的访问和刷新令牌。但是在我获得新的访问和刷新令牌之前,来自 UI 的后续请求会尝试使用旧的刷新令牌刷新令牌,它们都会导致错误。
解决此问题的最佳方法是什么?
【问题讨论】:
标签: api oauth infusionsoft
在我的一个应用程序中,我已与 Infusionsoft 集成,并且访问令牌会在特定时间后过期。
现在前端发出多个请求以获取不同的数据。当令牌过期时,它会刷新令牌并获取新的访问和刷新令牌。但是在我获得新的访问和刷新令牌之前,来自 UI 的后续请求会尝试使用旧的刷新令牌刷新令牌,它们都会导致错误。
解决此问题的最佳方法是什么?
【问题讨论】:
标签: api oauth infusionsoft
(我的回答并非针对 Infusionsoft)。
当 Web 服务客户端可能发出使用相同承载令牌的并发请求(以防止每个请求线程或异步上下文产生它自己单独的刷新请求)。
诀窍是使用缓存的Task<AuthResponseDto>(其中AuthResponseDto是包含最新成功获得的access_token的DTO类型)在lock中交换(你不能await 在 lock 内,但您可以在 lock 内复制 Task 引用,然后在 lock 外复制 await。
// NOTE: `ConfigureAwait(false)` calls omitted for brevity. You should re-add them back.
class MyHttpClientWrapper
{
private readonly String refreshTokenOrClientCredentialsOrWhatever;
private readonly IHttpClientFactory hcf;
private readonly Object lastAuthTaskLock = new Object();
private Task<AuthResponseDto> lastAuthTask;
private DateTime lastAuthTaskAt;
public MyHttpClientWrapper( IHttpClientFactory hcf )
{
this.hcf = hcf ?? throw new ArgumentNullException( nameof(hcf) );
this.refreshTokenOrClientCredentialsOrWhatever = LoadFromSavedConfig();
}
private async Task<AuthResponseDto> RefreshBearerTokenAsync()
{
using( HttpClient hc = this.hcf.CreateClient() )
using( HttpResponseMessage resp = await hc.PostAsync( this.refreshTokenOrClientCredentialsOrWhatever ) )
{
AuthResponseDto ar = await DeserializeJsonResponseAsync( resp );
this.lastAuthTaskExpiresAt = DateTime.UtcNow.Add( ar.MaxAge );
return ar;
}
}
private async Task<String> RefreshBearerTokenIfNecessaryAsync()
{
Task<AuthResponseDto> task;
lock( this.lastAuthTaskLock )
{
if( this.lastAuthTask is null )
{
// e.g. This is the first ever request.
task = this.lastAuthTask = this.RefreshBearerTokenAsync();
}
else
{
task = this.lastAuthTask;
// Is the task currently active? If it's currently busy then just await it (thus preventing duplicate requests!)
if( task.IsCompleted )
{
// If the current bearer-token is definitely expired, then replace it:
if( this.lastAuthTaskExpiresAt <= DateTime.UtcNow )
{
task = this.lastAuthTask = this.RefreshBearerTokenAsync();
}
}
else
{
// Continue below.
}
}
}
AuthResponseDto ar = await task;
return ar.BearerToken;
}
//
public async Task<CustomerDto> GetCustomerAsync( Int32 customerId )
{
// Always do this in every request to ensure you have the latest bearerToken:
String bearerToken = await this.RefreshBearerTokenIfNecessaryAsync();
using( HttpClient hc = this.hcf.Create() )
using( HttpRequestMessage req = new HttpRequestMessage() )
{
req.Headers.Add( "Authorization", "Bearer " + bearerToken );
using( HttpResponseMessage resp = await hc.SendAsync( req ) )
{
if( resp.StatusCode == 401 )
{
// Authentication error before the token expired - invoke and await `RefreshBearerTokenAsync` (rather than `RefreshBearerTokenIfNecessaryAsync`) and see what happens. If it succeeds then re-run `req`) otherwise throw/fail because that's an unrecoverable error.
}
// etc
}
}
}
}
【讨论】:
【讨论】:
refresh_token 或承载令牌到期时(因为它可能是单方面的)期满前撤销)。在处理“引用令牌”样式的 access_token 值时,这也是一个真正的问题,这些值只是像 GUID 这样不公开任何到期日期的短不透明字符串 - 你不会知道 access_token 不起作用直到您从服务器收到错误消息。