Asp.net-Core

使用 Jwt 令牌身份驗證在 Blazor 伺服器應用程序中自定義 AuthenticationStateProvider

  • October 11, 2021

我注意到許多開發人員在 Blazor Server App 和 Blazor WebAssembly App 中都錯誤地將 AuthenticationStateProvider 子類化,更重要的是由於錯誤的原因。

如何正確以及何時執行?

首先,您不會僅為將聲明添加到 ClaimPrincipal 對象而對 AuthenticationStateProvider 進行子類化。一般來說,聲明是在使用者通過身份驗證後添加的,如果您需要檢查這些聲明並對其進行轉換,則應該在其他地方完成,而不是在 AuthenticationStateProvider 對像中。順便說一句,在 Asp.Net Core 中,有兩種​​方法可以做到這一點,但這本身就是一個問題。

我猜這個程式碼範例讓很多人相信這是向 ClaimsPrincipal 對象添加聲明的地方。

在目前上下文中,實現 Jwt Token Authentication,應該在伺服器上創建 Jwt Token 時將聲明添加到 Jwt Token 中,並在需要時在客戶端提取,例如,您需要目前使用者的名稱。我注意到開發人員將使用者名保存在本地儲存中,並在需要時檢索它。這是錯誤的。您應該從 Jwt 令牌中提取使用者名。

以下程式碼範例描述瞭如何創建自定義 AuthenticationStateProvider 對象,其目標是從本地儲存中檢索新添加的 Jwt Token 字元串,解析其內容,並創建提供給相關方(AuthenticationStateProvider 的訂閱者)的 ClaimsPrincipal 對象.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 令牌並將其解析為聲明集合的方法。

你的 Startup 類應該是這樣的:

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

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

 }

引用自:https://stackoverflow.com/questions/62529029