【发布时间】:2019-11-10 09:15:18
【问题描述】:
对于在特定情况下使用单个页面应用程序同时进行多个 api 调用来更新刷新令牌,我有点头疼。我有一个 SPA,它的堆栈由以下内容组成。
Html/JS SPA -> MVC 应用程序 -> WebAPI
我使用混合流,当用户登录页面时,我将id_token、access_token 和refresh_token 存储在会话 cookie 中。
我使用一个HttpClient,它有两个DelegatingHandlers 与Web API 对话。委托处理程序之一只是将访问令牌添加到 Authorization 标头。另一个在此之前运行并检查访问令牌上剩余的生命周期。如果访问令牌的剩余时间有限,则使用 refresh_token 获取新凭据并将其保存回我的会话。
这是 OidcTokenRefreshHandler 的代码。
public class OidcTokenRefreshHandler : DelegatingHandler
{
private readonly IHttpContextAccessor _httpContextAccessor;
private readonly OidcTokenRefreshHandlerParams _handlerParams;
public OidcTokenRefreshHandler(IHttpContextAccessor httpContextAccessor, OidcTokenRefreshHandlerParams handlerParams)
{
_httpContextAccessor = httpContextAccessor;
_handlerParams = handlerParams;
}
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request,
CancellationToken cancellationToken)
{
var accessToken = await _httpContextAccessor.HttpContext.GetTokenAsync("access_token");
var handler = new JwtSecurityTokenHandler();
var accessTokenObj = handler.ReadJwtToken(accessToken);
var expiry = accessTokenObj.ValidTo;
if (expiry - TimeSpan.FromMinutes(_handlerParams.AccessTokenThresholdTimeInMinutes) < DateTime.UtcNow )
{
await RefreshTokenAsync(cancellationToken);
}
return await base.SendAsync(request, cancellationToken);
}
private async Task RefreshTokenAsync(CancellationToken cancellationToken)
{
var client = new HttpClient();
var discoveryResponse = await client.GetDiscoveryDocumentAsync(_handlerParams.OidcAuthorityUrl, cancellationToken);
if (discoveryResponse.IsError)
{
throw new Exception(discoveryResponse.Error);
}
var refreshToken = await _httpContextAccessor.HttpContext.GetTokenAsync(OpenIdConnectParameterNames.RefreshToken);
var tokenResponse = await client.RequestRefreshTokenAsync(new RefreshTokenRequest
{
Address = discoveryResponse.TokenEndpoint,
ClientId = _handlerParams.OidcClientId,
ClientSecret = _handlerParams.OidcClientSecret,
RefreshToken = refreshToken
}, cancellationToken);
if (tokenResponse.IsError)
{
throw new Exception(tokenResponse.Error);
}
var tokens = new List<AuthenticationToken>
{
new AuthenticationToken
{
Name = OpenIdConnectParameterNames.IdToken,
Value = tokenResponse.IdentityToken
},
new AuthenticationToken
{
Name = OpenIdConnectParameterNames.AccessToken,
Value = tokenResponse.AccessToken
},
new AuthenticationToken
{
Name = OpenIdConnectParameterNames.RefreshToken,
Value = tokenResponse.RefreshToken
}
};
// Sign in the user with a new refresh_token and new access_token.
var info = await _httpContextAccessor.HttpContext.AuthenticateAsync("Cookies");
info.Properties.StoreTokens(tokens);
await _httpContextAccessor.HttpContext.SignInAsync("Cookies", info.Principal, info.Properties);
}
}
问题是很多电话几乎同时打到这个。然后,所有这些调用将同时到达刷新端点。他们都将检索新的有效访问令牌,并且应用程序将继续工作。但是,如果同时发生 3 个请求,则将创建三个新的刷新令牌,其中只有一个有效。由于应用程序的异步特性,我无法保证存储在我的会话中的刷新令牌实际上是最新的刷新令牌。下次我需要刷新刷新令牌时可能是无效的(而且经常是)。
到目前为止我对可能解决方案的想法。
在使用 Mutex 或类似工具检查访问令牌时锁定。但是,当它被具有不同会话的不同用户使用时(据我所知),这有可能被阻止。如果我的 MVC 应用程序跨多个实例,它也不起作用。
更改以使刷新令牌在使用后保持有效。所以使用这三个中的哪一个并不重要。
任何关于以上哪一个更好的想法,或者有人有一个非常聪明的选择。
非常感谢!
【问题讨论】:
标签: asp.net asp.net-core identityserver4 asp.net-core-identity openid-connect