.NET 10 Blazor Server JWT认证问题:AuthorizeView未更新AuthenticationState
问题描述
基于.NET 10的Blazor Server应用实现JWT认证时,已成功提取并解码JWT令牌,能查看role和personalId等声明,但AuthorizeView组件始终渲染NotAuthorized区块,即使用户已完成认证。
相关代码
CustomAuthenticationService 实现代码
public class CustomAuthenticationService : AuthenticationStateProvider { private readonly ILocalStorageService _localStorage; private readonly HttpClient _httpClient; private readonly NavigationManager _navigationManager; private string _cachedToken; private AuthenticationState _currentState; // Hold the current authentication state public CustomAuthenticationService(ILocalStorageService localStorage, HttpClient httpClient, NavigationManager navigationManager) { _localStorage = localStorage; _httpClient = httpClient; _navigationManager = navigationManager; _currentState = new AuthenticationState(new ClaimsPrincipal(new ClaimsIdentity())); } // Overriding GetAuthenticationStateAsync to return the current authentication state public override async Task<AuthenticationState> GetAuthenticationStateAsync() { if (_cachedToken != null) { // Return the cached token if it's already loaded return BuildAuthenticationStateFromToken(_cachedToken); } // Return a default, unauthenticated state while loading the token return new AuthenticationState(new ClaimsPrincipal(new ClaimsIdentity())); } // This method loads the token from localStorage and updates the authentication state public async Task LoadTokenFromLocalStorageAsync() { var token = await _localStorage.GetItemAsync<string>("authToken"); if (!string.IsNullOrEmpty(token)) { _cachedToken = token; // Notify that the authentication state has changed NotifyAuthenticationStateChanged(GetAuthenticationStateAsync()); } else { _cachedToken = null; } //var token = await _localStorage.GetItemAsync<string>("authToken"); //ClaimsPrincipal user = new ClaimsPrincipal(new ClaimsIdentity()); //if (!string.IsNullOrEmpty(token)) //{ // // Decode the JWT or extract claims (you can extend this as needed) // var claims = GetClaimsFromToken(token); // user = new ClaimsPrincipal(new ClaimsIdentity(claims, "jwt")); //} //// Update the authentication state //_currentState = new AuthenticationState(user); //// Notify any subscribers that the authentication state has changed //NotifyAuthenticationStateChanged(Task.FromResult(_currentState)); } // This method will be used to log out the user public async Task Logout() { _cachedToken = null; // Clear the cached token await _localStorage.RemoveItemAsync("authToken"); // Remove token from localStorage _httpClient.DefaultRequestHeaders.Authorization = null; // Remove the authorization header NotifyAuthenticationStateChanged(GetAuthenticationStateAsync()); // Notify state change _navigationManager.NavigateTo("/login", true); // Navigate to the login page } // This method builds the AuthenticationState from the JWT token private AuthenticationState BuildAuthenticationStateFromToken(string token) { // Parse claims from the JWT token var claims = ParseClaimsFromJwt(token); // Create identity using the claims var identity = new ClaimsIdentity(claims, "jwt"); // Create a ClaimsPrincipal from the identity var user = new ClaimsPrincipal(identity); // Return the authentication state with the ClaimsPrincipal return new AuthenticationState(user); } // Helper method to parse the JWT and extract claims private IEnumerable<Claim> ParseClaimsFromJwt(string jwt) { var payload = jwt.Split('.')[1]; // JWT payload is the second part of the token var jsonBytes = WebEncoders.Base64UrlDecode(payload); // Decode the payload from base64 URL var keyValuePairs = JsonSerializer.Deserialize<Dictionary<string, object>>(jsonBytes); // Deserialize the JSON payload return keyValuePairs.Select(kvp => new Claim(kvp.Key, kvp.Value.ToString())); // Return claims } // This method will be triggered after the component is rendered public async Task OnAfterRenderAsync(bool firstRender) { if (firstRender) { // Only load the token after the component is rendered on the client side await LoadTokenFromLocalStorageAsync(); } } // Helper method to extract claims from JWT (decoding the token payload) public IEnumerable<Claim> GetClaimsFromToken(string token) { // Split the JWT token into parts (header, payload, signature) var parts = token.Split('.'); if (parts.Length != 3) { return Enumerable.Empty<Claim>(); } // Decode the payload part of the JWT var payload = parts[1]; var jsonBytes = WebEncoders.Base64UrlDecode(payload); var json = Encoding.UTF8.GetString(jsonBytes); // Deserialize the JSON payload to get the claims var claimsDict = JsonSerializer.Deserialize<Dictionary<string, object>>(json); var claims = claimsDict?.Select(kvp => new Claim(kvp.Key, kvp.Value.ToString())) ?? Enumerable.Empty<Claim>(); return claims; } public async Task<string> GetProtectedDataAsync() { var token = await _localStorage.GetItemAsync<string>("authToken"); if (string.IsNullOrEmpty(token)) { throw new Exception("User is not authenticated"); } _httpClient.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", token); var response = await _httpClient.GetStringAsync("api/protected-data"); return response; } }
MainLayout.razor 部分代码
@using System.Security.Claims @inherits LayoutComponentBase @rendermode InteractiveServer @inject IStringLocalizer<SharedResource> L @inject CustomAuthenticationService _authService; @inject NavigationManager _navigationManager </AuthorizeView> <Authorized> ..... </Authorized> <NotAuthorized> @if (_navigationManager.Uri.Contains("/Auth/Login")) { <p>Please login to continue.</p> <Login/> } else { <p>You need to login first.</p> <a href="/Auth/Login">Login HERE</a> <p>@(string.IsNullOrEmpty(role) ? string.Empty : role) </p> <p>@(string.IsNullOrEmpty(personalId) ? string.Empty : personalId) </p> } </NotAuthorized> </AuthorizeView> @code { private bool isAuthenticated; private string? role; private string? personalId; private ClaimsPrincipal _user; // Called whenever the component parameters are set or changed. protected override async Task OnParametersSetAsync() { await UpdateAuthenticationState(); StateHasChanged(); // Trigger re-rendering } private bool hasRendered = false; protected override async Task OnAfterRenderAsync(bool firstRender) { if (firstRender || hasRendered == false) { await UpdateAuthenticationState(); hasRendered = true; } } protected override async Task OnInitializedAsync() { await UpdateAuthenticationState(); StateHasChanged(); // Trigger re-rendering } private async Task UpdateAuthenticationState() { AuthenticationState state = await _authService.GetAuthenticationStateAsync(); _user = state.User; isAuthenticated = _user.Identity.IsAuthenticated; if (isAuthenticated) { // Extract claims (role and PersonalId) role =_user.FindFirst("http://schemas.microsoft.com/ws/2008/06/identity/claims/role")?.Value; personalId = _user.FindFirst("PersonalId")?.Value; } else { role = null; personalId = null; } StateHasChanged(); // Trigger re-rendering } private void NavigateToLogin() { _navigationManager.NavigateTo("/Auth/Login", true); } }
问题排查与修复方案
1. 修正AuthorizeView标签语法错误
MainLayout代码中开头的</AuthorizeView>是闭合标签,缺少对应的起始标签,将其改为<AuthorizeView>,确保组件结构正确:
<AuthorizeView> <Authorized> ..... </Authorized> <NotAuthorized> <!-- 原有内容 --> </NotAuthorized> </AuthorizeView>
2. 修复GetAuthenticationStateAsync的异步加载逻辑
当前实现中,首次调用时未主动从LocalStorage加载token,直接返回未认证状态,导致AuthorizeView初始判断错误。修改该方法,确保首次调用时读取token:
public override async Task<AuthenticationState> GetAuthenticationStateAsync() { if (_cachedToken != null) { return BuildAuthenticationStateFromToken(_cachedToken); } // 首次调用时主动加载LocalStorage中的token var token = await _localStorage.GetItemAsync<string>("authToken"); if (!string.IsNullOrEmpty(token)) { _cachedToken = token; return BuildAuthenticationStateFromToken(token); } return new AuthenticationState(new ClaimsPrincipal(new ClaimsIdentity())); }
3. 移除CustomAuthenticationService中的OnAfterRenderAsync方法
该方法属于Blazor组件生命周期,不应该放在AuthenticationStateProvider实现类中,删除此方法,token加载逻辑已整合到GetAuthenticationStateAsync中。
4. 确保角色声明匹配
如果JWT payload中的角色键是role,而非微软标准声明键,需调整角色提取逻辑,避免无法找到声明:
// 在UpdateAuthenticationState方法中 role = _user.FindFirst("role")?.Value ?? _user.FindFirst(ClaimTypes.Role)?.Value;
5. 简化组件生命周期逻辑,依赖认证状态自动更新
AuthorizeView会自动监听AuthenticationStateProvider的状态变化,无需手动重复调用UpdateAuthenticationState。修改MainLayout的@code部分,利用级联参数获取认证状态并监听变化:
@code { private string? role; private string? personalId; [CascadingParameter] private Task<AuthenticationState> AuthenticationStateTask { get; set; } [Inject] private AuthenticationStateProvider AuthStateProvider { get; set; } protected override async Task OnInitializedAsync() { var state = await AuthenticationStateTask; UpdateUserClaims(state.User); // 监听认证状态变化,自动更新声明 AuthStateProvider.AuthenticationStateChanged += async authStateTask => { var newState = await authStateTask; UpdateUserClaims(newState.User); StateHasChanged(); }; } private void UpdateUserClaims(ClaimsPrincipal user) { if (user.Identity.IsAuthenticated) { role = user.FindFirst("role")?.Value ?? user.FindFirst(ClaimTypes.Role)?.Value; personalId = user.FindFirst("PersonalId")?.Value; } else { role = null; personalId = null; } } }
内容的提问来源于stack exchange,提问作者Maria
相关产品推荐
相关产品推荐

