使用 Jwt 令牌身份验证在 Blazor 服务器应用程序中自定义 AuthenticationStateProvider

Customizing the AuthenticationStateProvider in Blazor Server App with Jwt Token Authentication

我注意到许多开发人员都将 AuthenticationStateProvider 子类化为 Blazor Server App 和 Blazor WebAssembly App 错误,更重要的是错误 原因。

如何正确地做到这一点?

首先,您不会将 AuthenticationStateProvider 子class 仅用于 向 ClaimPrincipal 对象添加声明。一般来说,声明是在a之后添加的 用户已通过身份验证,如果您需要检查这些声明并进行转换,它 应该在其他地方完成,而不是在 AuthenticationStateProvider 对象中。顺便说一句,在 Asp.Net 核心有两种方法可以做到这一点,但这本身就是一个问题。

我猜 this code sample 让很多人相信这是向 ClaimsPrincipal 对象添加声明的地方。

在当前上下文中,实现 Jwt 令牌身份验证,应添加声明 在服务器上创建 Jwt 令牌,并在需要时在客户端提取, 例如,您需要当前用户的名称。我注意到开发人员保存 本地存储中的用户名称,并在需要时检索它。这是错误的。 您应该从 Jwt 令牌中提取用户名。

以下代码示例描述了如何创建自定义 AuthenticationStateProvider 对象 其中objective是从本地存储中获取一个新添加的Jwt Token字符串, 解析其内容,并创建一个提供给感兴趣的 ClaimsPrincipal 对象 派对(AuthenticationStateProvider.AuthenticationStateChanged 活动的订阅者) ,例如 CascadingAuthenticationState 对象。

以下代码示例演示了如何实现自定义 authenticationstateprovider 正确,并且有充分的理由。

public class TokenServerAuthenticationStateProvider : 
                                AuthenticationStateProvider
    {
        private readonly IJSRuntime _jsRuntime;
       
        public TokenServerAuthenticationStateProvider(IJSRuntime jsRuntime)
        {
            _jsRuntime = jsRuntime;
           
           
        }

       public async Task<string> GetTokenAsync()
            => await _jsRuntime.InvokeAsync<string>("localStorage.getItem", "authToken");

        public async Task SetTokenAsync(string token)
        {
            if (token == null)
            {
                await _jsRuntime.InvokeAsync<object>("localStorage.removeItem", "authToken");
            }
            else
            {
                await _jsRuntime.InvokeAsync<object>("localStorage.setItem", "authToken", token);
            }
            
            NotifyAuthenticationStateChanged(GetAuthenticationStateAsync());
        }

        public override async Task<AuthenticationState> GetAuthenticationStateAsync()
        {
            var token = await GetTokenAsync();
            var identity = string.IsNullOrEmpty(token)
                ? new ClaimsIdentity()
                : new ClaimsIdentity(ServiceExtensions.ParseClaimsFromJwt(token), "jwt");
            return new AuthenticationState(new ClaimsPrincipal(identity));
        }
    }

这是位于登录页面提交按钮中的代码示例 调用验证用户凭据的 Web Api 端点,之后 创建 Jwt 令牌并将其传递回调用代码:

async Task SubmitCredentials()
{

    bool lastLoginFailed;

    var httpClient = clientFactory.CreateClient();
    httpClient.BaseAddress = new Uri("https://localhost:44371/");

    var requestJson = JsonSerializer.Serialize(credentials, new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase });


    var response = await httpClient.SendAsync(new HttpRequestMessage(HttpMethod.Post, "api/user/login")
    {
        Content = new StringContent(requestJson, Encoding.UTF8, "application/json")
    });

    var stringContent = await response.Content.ReadAsStringAsync();

    var result = JsonSerializer.Deserialize<LoginResult>(stringContent, new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase });

    lastLoginFailed = result.Token == null;
    if (!lastLoginFailed)
    {
        // Success! Store token in underlying auth state service
        await TokenProvider.SetTokenAsync(result.Token);
        NavigationManager.NavigateTo(ReturnUrl);
        
    }
}

Point to note: TokenProvider is an instance of TokenServerAuthenticationStateProvider. 
Its name reflects its functionality: handling the recieved Jwt Token, and providing 
the Access Token when requested.

This line of code: TokenProvider.SetTokenAsync(result.Token); passes the Jwt Token 
to TokenServerAuthenticationStateProvider.SetTokenAsync in which the token is sored 
in the local storage, and then raises AuthenticationStateProvider.AuthenticationStateChanged
event by calling NotifyAuthenticationStateChanged, passing an AuthenticationState object
built from the data contained in the stored Jwt Token.


Note that the GetAuthenticationStateAsync method creates a new ClaimsIdentity object from 
the parsed Jwt Token. All the claims added to the newly created ClaimsIdentity object 
are retrieved from the Jwt Token. I cannot think of a use case where you have to create
a new claim object and add it to the ClaimsPrincipal object.

The following code is executed when an authenticated user is attempting to access
the FecthData page

@code 
{
   private WeatherForecast[] forecasts;


protected override async Task OnInitializedAsync()
{
    var token = await TokenProvider.GetTokenAsync();
   
    var httpClient = clientFactory.CreateClient();
    httpClient.BaseAddress = new Uri("https://localhost:44371/");
    httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token);

    var response = await httpClient.SendAsync(new HttpRequestMessage(HttpMethod.Get, $"api/WeatherForecast?startDate={DateTime.Now}"));
    
    var stringContent = await response.Content.ReadAsStringAsync();

    forecasts = JsonSerializer.Deserialize<WeatherForecast[]>(stringContent, new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase });
   
}

}

注意第一行代码:var token = await TokenProvider.GetTokenAsync();检索 本地存储的Jwt Token,添加到请求的Authorization header中。

希望这对您有所帮助...

编辑

注意:ServiceExtensions.ParseClaimsFromJwt是获取本地存储中提取的Jwt token,解析成claims集合的方法。

你的 Startup class 应该是这样的:

public void ConfigureServices(IServiceCollection services)
   {
      // Code omitted...

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

  }