Blazor客户端(Web Assembly)的AuthenticationState状态只有在重新加载页面后才会更新

6
我在使用Blazor身份验证时遇到了问题。我已经实现了AuthenticationStateProvider, 一切都运行正常,但是在登入或登出之后,我需要手动刷新页面以更新AuthenticationState
例如,我有一个Profile.razor页面组件,其包含@attribute [Authorize]。我无法在登录后打开此页面,就像我没有得到授权一样,但在页面重新加载后一切都很好。退出也是同样的情况。
我怀疑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");
    }
}

1
请问您能否发布您的启动类,包括客户端和服务器端的启动类... - enet
@enet 我更新了我的帖子,并添加了来自客户端的Startup.cs。我也可以提供来自服务器的Startup.cs,但我使用与Angular前端完全相同的服务器,一切都正常工作。登录后我收到有效的访问令牌,但似乎Blazor没有接收到更新的AuthenticationState。 - Gleb Skripnikov
@enet 谢谢,我刚刚找到了我的错误,它在客户端的 Startup.cs 文件中。 - Gleb Skripnikov
我有完全相反的问题:D - Piou
2个回答

9
我发现了我的错误。问题出在客户端的 Startup.cs 文件中。
而不是:
services.AddScoped<TokenAuthenticationStateProvider>();
services.AddScoped<AuthenticationStateProvider, TokenAuthenticationStateProvider>();

我需要以这种方式注册我的服务:

services.AddScoped<TokenAuthenticationStateProvider>();
services.AddScoped<AuthenticationStateProvider>(provider => provider.GetRequiredService<TokenAuthenticationStateProvider>());

现在一切正常!

1
请问您能否解释一下为什么以及如何修改代码解决了您的问题... - enet
1
@enet:为什么以及如何:错误的代码会添加两个TokenAuthenticationStateProvider的实现。AccountService将使用第一个,因为它正在寻找TokenAuthenticationStateProvider。Razor代码将使用第二个,因为它正在寻找AuthenticationStateProvider。通过更正代码,只创建并且重复使用一个实现来供AuthenticationStateProvider使用。 - Marcel Wolterbeek
@ Marcel W,感谢您的回复...但是这段代码不足以吗:services.AddScoped<AuthenticationStateProvider, TokenAuthenticationStateProvider>(); - enet
1
据我所知,不行,因为他们需要在登录时注入TokenAuthenticationStateProvider。在这种情况下,注入AuthenticationStateProvider并将其强制转换为正确的类型可能会起作用,但这会在一定程度上破坏DI原则。因此,他们必须单独添加TokenAuthenticationStateProvider。 - PuerNoctis

0

它没起作用的原因是因为你依赖 DI 来为你执行实例化工作,并且两个调用会创建同一个提供程序的不同实例。

如果你想做对的事情,试试这个:

var provider = new TokenAuthenticationStateProvider();
services.AddSingleton(c => provider);
services.AddSingleton<AuthenticationStateProvider>(c => provider);

这样,无论如何解析服务,您都将得到相同的实例。如果这是一个客户端应用程序,则不需要Scoped实例,因为应用程序在单个浏览器窗口内本地运行。

希望有所帮助!


1
这是否也适用于 Blazor 服务器端? - Zubair Khakwani

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接