我对Blazor身份验证有疑问。我具有AuthenticationStateProvider
的实现,并且一切正常,但是登录或注销后,我需要手动刷新页面以更新AuthenticationState
。
例如,我有一个带有@attribute [Authorize]
的Profile.razor页面组件。登录后我无法打开该页面,就像我未被授权一样,但是在页面重新加载后一切都很好。退出也是一样。
我怀疑NotifyAuthenticationStateChanged(GetAuthenticationStateAsync())
无能为力,但我不明白这是怎么回事。
TokenAuthenticationStateProvider.cs-AuthenticationStateProvider
public class TokenAuthenticationStateProvider : AuthenticationStateProvider
{
private readonly TokenStorage tokenStorage;
public TokenAuthenticationStateProvider(TokenStorage tokenStorage)
{
this.tokenStorage = tokenStorage;
}
public void StateChanged()
{
NotifyAuthenticationStateChanged(GetAuthenticationStateAsync()); // <- Does nothing
}
public override async Task<AuthenticationState> GetAuthenticationStateAsync()
{
var token = await tokenStorage.GetAccessToken();
var identity = string.IsNullOrEmpty(token)
? new ClaimsIdentity()
: new ClaimsIdentity(ParseClaimsFromJwt(token), "jwt");
return new AuthenticationState(new ClaimsPrincipal(identity));
}
private static IEnumerable<Claim> ParseClaimsFromJwt(string jwt)
{
var payload = jwt.Split('.')[1];
var jsonBytes = ParseBase64WithoutPadding(payload);
var keyValuePairs = JsonSerializer.Deserialize<Dictionary<string, object>>(jsonBytes);
return keyValuePairs.Select(kvp => new Claim(kvp.Key, kvp.Value.ToString()));
}
private static byte[] ParseBase64WithoutPadding(string base64)
{
switch (base64.Length % 4)
{
case 2: base64 += "=="; break;
case 3: base64 += "="; break;
}
return Convert.FromBase64String(base64);
}
}
TokenStorage.cs-访问和刷新令牌存储
public class TokenStorage
{
private readonly ILocalStorage localStorage;
public TokenStorage(
ILocalStorage localStorage)
{
this.localStorage = localStorage;
}
public async Task SetTokensAsync(string accessToken, string refreshToken)
{
await localStorage.SetItem("accessToken", accessToken);
await localStorage.SetItem("refreshToken", refreshToken);
}
public async Task<string> GetAccessToken()
{
return await localStorage.GetItem<string>("accessToken");
}
public async Task<string> GetRefreshToken()
{
return await localStorage.GetItem<string>("refreshToken");
}
public async Task RemoveTokens()
{
await localStorage.RemoveItem("accessToken");
await localStorage.RemoveItem("refreshToken");
}
}
AccountService.cs-具有登录和注销方法的服务。我打电话给authState.StateChanged()
更新AuthenticationState
public class AccountService
{
private readonly TokenStorage tokenStorage;
private readonly HttpClient httpClient;
private readonly TokenAuthenticationStateProvider authState;
private readonly string authApiUrl = "/api/authentication";
public AccountService(
TokenStorage tokenStorage,
HttpClient httpClient,
TokenAuthenticationStateProvider authState)
{
this.tokenStorage = tokenStorage;
this.httpClient = httpClient;
this.authState = authState;
}
public async Task Login(LoginCredentialsDto credentials)
{
var response = await httpClient.PostJsonAsync<AuthenticationResponseDto>($"{authApiUrl}/login", credentials);
await tokenStorage.SetTokensAsync(response.AccessToken, response.RefreshToken);
authState.StateChanged();
}
public async Task Logout()
{
var refreshToken = await tokenStorage.GetRefreshToken();
await httpClient.GetJsonAsync<AuthenticationResponseDto>($"{authApiUrl}/logout/{refreshToken}");
await tokenStorage.RemoveTokens();
authState.StateChanged();
}
}
App.razor
<CascadingAuthenticationState>
<Router AppAssembly="@typeof(Program).Assembly" Context="routeData">
<Found>
<AuthorizeRouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)">
<NotAuthorized>
<h1>Not authorized!</h1>
</NotAuthorized>
</AuthorizeRouteView>
</Found>
<NotFound>
<LayoutView Layout="@typeof(MainLayout)">
<p>Sorry, there's nothing at this address.</p>
</LayoutView>
</NotFound>
</Router>
</CascadingAuthenticationState>
Profile.razor
@page "/profile/{UserName}"
@attribute [Authorize]
<h1>Profile</h1>
@code {
...
}
Startup.cs-客户端启动
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
services.AddValidatorsFromAssemblyContaining<LoginCredentialsDtoValidator>();
services.AddStorage();
services.AddScoped<TokenStorage>();
services.AddScoped<AccountService>();
services.AddScoped<TokenAuthenticationStateProvider>();
services.AddScoped<AuthenticationStateProvider, TokenAuthenticationStateProvider>();
services.AddAuthorizationCore();
}
public void Configure(IComponentsApplicationBuilder app)
{
app.AddComponent<App>("app");
}
}
答案 0 :(得分:1)
我发现了我的错误。问题出在客户端的Startup.cs文件中。
代替:
services.AddScoped<TokenAuthenticationStateProvider>();
services.AddScoped<AuthenticationStateProvider, TokenAuthenticationStateProvider>();
我需要这样注册我的服务:
services.AddScoped<TokenAuthenticationStateProvider>();
services.AddScoped<AuthenticationStateProvider>(provider => provider.GetRequiredService<TokenAuthenticationStateProvider>());
现在一切正常!
答案 1 :(得分:0)
它不起作用的原因是因为您依赖 DI 为您完成实例化工作,并且两个调用都创建了同一提供程序的单独实例。
如果你想做得对,试试这个:
var provider = new TokenAuthenticationStateProvider();
services.AddSingleton(c => provider);
services.AddSingleton<AuthenticationStateProvider>(c => provider);
这样,无论您如何解析服务,您都将获得相同的实例。如果这是一个客户端应用程序,则不需要 Scoped 实例,因为该应用程序在单个浏览器窗口中本地运行。
HTH!