此方法结合了 Microsoft 关于如何将令牌传递给 Blazor 服务器应用程序 (here) 的建议,以及为所有用户在 Singleton 服务中的服务器端存储令牌(灵感来自 Dominick Baier 在GitHub here)。
总之,我们的User 的sub 声明被捕获在_Host.cshtml 文件中,并以类似于Microsoft 捕获令牌的方式传递到App 组件在他们上面的示例代码中,然后将其保存到Scoped 服务中,我们称之为UserSubProvider。我们不是在 _Host.cshtml 文件中捕获令牌并将它们存储在 Scoped 服务中,而是以与 Dominick Baier 的示例类似的方式使用 OnTokenValidated 事件,将令牌存储在 @ 987654332@ 服务持有所有Users 的令牌,我们将此服务称为ServerSideTokenStore。
当我们使用 HttpClient 调用 API 并且它需要 access_token(或 refresh_token)时,它会从 UserSubProvider 检索 User 的子,使用它来调用 @987654340 @,它返回包含令牌的 UserTokenProvider(类似于 Microsoft 的 TokenProvider)。如果HttpClient 需要刷新令牌,那么它会填充UserTokenProvider 并通过调用ServerSideTokenStore.SetTokensAsync() 来保存它。
我们遇到的另一个问题是,如果在应用程序重新启动时打开了一个单独的 Web 浏览器实例(因此会丢失 ServerSideTokenStore 中保存的数据),用户仍将使用 cookie 进行身份验证,但我们丢失了access_token 和 refresh_token。如果应用程序重新启动,这可能在生产中发生,但在开发环境中发生的频率要高得多。如果我们无法获得合适的access_token,我们会通过处理OnValidatePrincipal 并调用RejectPrincipal() 来解决此问题。这会强制往返于 IdentityServer,它提供了一个新的 access_token 和 refresh_token。这个方法来自this stack overflow thread。
(为了清晰/重点,下面的一些代码不包括一些标准错误处理、日志记录等)
UserSubProvider
public class UserSubProvider
{
public string Sub { get; set; }
}
用户令牌提供者
public class UserTokenProvider
{
public string AccessToken { get; set; }
public string RefreshToken { get; set; }
public DateTimeOffset Expiration { get; set; }
}
ServerSideTokenStore
public class ServerSideTokenStore
{
private readonly ConcurrentDictionary<string, UserTokenProvider> UserTokenProviders = new();
public Task ClearTokensAsync(string userSub)
{
UserTokenProviders.TryRemove(userSub, out _);
return Task.CompletedTask;
}
public Task<UserTokenProvider> GetTokensAsync(string userSub)
{
UserTokenProviders.TryGetValue(userSub, out var value);
return Task.FromResult(value);
}
public Task StoreTokensAsync(string userSub, UserTokenProvider userTokenProvider)
{
UserTokenProviders[userSub] = userTokenProvider;
Return Task.CompletedTask;
}
}
_Host.cshtml
@using Microsoft.AspNetCore.Authentication
// ...
@{
var userSub = HttpContext.User.Identity.Name;
}
<component type="typeof(App)" param-UserSub="userSub" render-mode="ServerPrerendered" />
App.razor
@inject IUserSubProvider UserSubProvider
// ...
@code {
[Parameter]
Public string UserSub { get; set; }
protected override Task OnInitializedAsync()
{
UserSubProvider.Sub = UserSub;
return base.OnInitializedAsync();
}
}
Startup.cs ConfigureServices(或使用 .NET 6 / 其他的等效位置)
public void ConfigureServices(IServiceCollection services)
{
// …
services.AddAuthentication(…)
.AddCookie(“Cookies”, options =>
{
// …
options.Events.OnValidatePrincipal = async context =>
{
if (context.Principal.Identity.IsAuthenticated)
{
// get user sub and store in UserSubProvider (API client relies on it when refreshing tokens)
var userSubProvider =
context.HttpContext.RequestServices.GetRequiredService<IUserSubProvider>();
userSubProvider.UserSub = context.Principal.FindFirst(“sub”).Value;
// get user's tokens from server side token store
var tokenStore =
context.HttpContext.RequestServices.GetRequiredService<IServerSideTokenStore>();
var tokens = await tokenStore.GetTokenAsync(userSubProvider.UserSub);
if (tokens?.AccessToken == null
|| tokens?.Expiration == null
|| tokens?.RefreshToken == null)
{
// if we lack either an access or refresh token,
// then reject the Principal (forcing a round trip to the id server)
context.RejectPrincipal();
return;
}
// if the access token has expired, attempt to refresh it
if (tokens.Expiration < DateTimeOffset.UtcNow)
{
// we have a custom API client that takes care of refreshing our tokens
// and storing them in ServerSideTokenStore, we call that here
// …
// check the tokens have been updated
var newTokens = await tokenStore.GetTokenAsync(userSubProvider.UserSub);
if (newTokens?.AccessToken == null
|| newTokens?.Expiration == null
|| newTokens.Expiration < DateTimeOffset.UtcNow)
{
// if we lack an access token or it was not successfully renewed,
// then reject the Principal (forcing a round trip to the id server)
context.RejectPrincipal();
return;
}
}
}
}
}
.AddOpenIdConnect(“oidc”, options =>
{
// …
options.Events.OnTokenValidated = async n =>
{
var svc = n.HttpContext.RequestServices.GetRequiredService<IServerSideTokenStore>();
var culture = new CultureInfo(“EN”) ;
var exp = DateTimeOffset
.UtcNow
.AddSeconds(double.Parse(n.TokenEndpointResponse !.ExpiresIn, culture));
var userTokenProvider = new UserTokenProvider()
{
AcessToken = n.TokenEndpointResponse.AccessToken,
Expiration = exp,
RefreshToken = n.TokenEndpointResponse.RefreshToken
}
await svc.StoreTokensAsync(n.Principal.FindFirst(“sub”).Value, userTokenProvider);
};
// …
});
// …
}