使用 Jwt 令牌身份驗證在 Blazor 伺服器應用程序中自定義 AuthenticationStateProvider
我注意到許多開發人員在 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>()); }