You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

.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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.11 21:15:54