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

.NET 8.0 Blazor Server对接独立Identity API认证方案咨询

实现方案

一、自定义AuthenticationStateProvider

不需要启用Blazor Server内置的Cookie认证,完全依赖独立API的认证结果维护Blazor端的认证状态,让AuthorizedView能感知登录状态变化。

1. 创建自定义认证状态提供者

using Microsoft.AspNetCore.Components.Authorization;
using System.Security.Claims;
using Microsoft.AspNetCore.Components.Server.ProtectedBrowserStorage;

public class ApiAuthStateProvider : AuthenticationStateProvider
{
    private readonly ProtectedLocalStorage _protectedLocalStorage;
    private readonly ClaimsPrincipal _anonymous = new(new ClaimsIdentity());

    public ApiAuthStateProvider(ProtectedLocalStorage protectedLocalStorage)
    {
        _protectedLocalStorage = protectedLocalStorage;
    }

    public override async Task<AuthenticationState> GetAuthenticationStateAsync()
    {
        try
        {
            // 从加密的客户端存储读取认证信息
            var storedUser = await _protectedLocalStorage.GetAsync<AuthUser>("auth-user");
            if (storedUser.Success && storedUser.Value != null && !string.IsNullOrEmpty(storedUser.Value.AccessToken))
            {
                // 用API返回的Claims构造认证用户
                var identity = new ClaimsIdentity(storedUser.Value.Claims, "ApiAuth");
                var user = new ClaimsPrincipal(identity);
                return new AuthenticationState(user);
            }
        }
        catch
        {
            // 读取失败时返回匿名身份
        }
        return new AuthenticationState(_anonymous);
    }

    // 登录/登出后更新认证状态,通知Blazor组件刷新
    public async Task UpdateAuthenticationState(AuthUser authUser)
    {
        ClaimsPrincipal user;
        if (authUser != null && !string.IsNullOrEmpty(authUser.AccessToken))
        {
            var identity = new ClaimsIdentity(authUser.Claims, "ApiAuth");
            user = new ClaimsPrincipal(identity);
            await _protectedLocalStorage.SetAsync("auth-user", authUser);
        }
        else
        {
            user = _anonymous;
            await _protectedLocalStorage.DeleteAsync("auth-user");
        }

        NotifyAuthenticationStateChanged(Task.FromResult(new AuthenticationState(user)));
    }

    // 快捷登出方法
    public async Task Logout() => await UpdateAuthenticationState(null);
}

// 存储认证信息的模型,与API返回的令牌、Claims对应
public class AuthUser
{
    public string AccessToken { get; set; }
    public string RefreshToken { get; set; }
    public List<Claim> Claims { get; set; } = new();
}

2. 注册服务

在Program.cs中注册自定义提供者和依赖服务:

builder.Services.AddScoped<AuthenticationStateProvider, ApiAuthStateProvider>();
builder.Services.AddAuthorizationCore();
builder.Services.AddProtectedBrowserStorage();
// 注册你的ApiClient
builder.Services.AddScoped<ApiClient>();

二、登录UI与认证流程

1. 登录组件实现(含2FA处理)

创建Login.razor组件,处理账号密码登录和2FA验证,避免存储明文账号密码:

@inject ApiClient ApiClient
@inject ApiAuthStateProvider AuthStateProvider
@inject NavigationManager NavigationManager

<div class="login-form">
    <input @bind="_username" placeholder="用户名" />
    <input @bind="_password" type="password" placeholder="密码" />
    <button @onclick="HandleLogin">登录</button>

    @if (_show2FaForm)
    {
        <div class="2fa-form">
            <input @bind="_twoFactorCode" placeholder="2FA验证码" />
            <button @onclick="HandleTwoFaSubmit">验证</button>
        </div>
    }
</div>

@code {
    private string _username;
    private string _password;
    private string _twoFactorCode;
    private bool _show2FaForm;
    // 存储2FA挑战的临时标识,而非明文账号密码
    private string _twoFactorUserId;
    private string _twoFactorProvider;

    private async Task HandleLogin()
    {
        try
        {
            var loginResult = await ApiClient.LoginAsync(_username, _password);
            // 登录成功,更新认证状态
            var authUser = new AuthUser
            {
                AccessToken = loginResult.AccessToken,
                RefreshToken = loginResult.RefreshToken,
                Claims = loginResult.Claims.ToList()
            };
            await AuthStateProvider.UpdateAuthenticationState(authUser);
            NavigationManager.NavigateTo("/");
        }
        catch (HttpRequestException ex)
        {
            if (ex.StatusCode == System.Net.HttpStatusCode.Unauthorized)
            {
                var responseContent = await ex.GetResponseContentAsync();
                if (responseContent.Contains("TwoFactorRequired"))
                {
                    // 解析API返回的2FA挑战数据(默认MapIdentityApi会返回这些信息)
                    var challengeData = System.Text.Json.JsonSerializer.Deserialize<TwoFactorChallenge>(responseContent);
                    _twoFactorUserId = challengeData.UserId;
                    _twoFactorProvider = challengeData.Provider;
                    _show2FaForm = true;
                }
                else
                {
                    // 处理账号密码错误等其他未授权情况
                }
            }
        }
    }

    private async Task HandleTwoFaSubmit()
    {
        try
        {
            // 调用带2FA码的登录接口
            var loginResult = await ApiClient.LoginWithTwoFactorAsync(_twoFactorUserId, _twoFactorProvider, _twoFactorCode);
            var authUser = new AuthUser
            {
                AccessToken = loginResult.AccessToken,
                RefreshToken = loginResult.RefreshToken,
                Claims = loginResult.Claims.ToList()
            };
            await AuthStateProvider.UpdateAuthenticationState(authUser);
            NavigationManager.NavigateTo("/");
        }
        catch (Exception ex)
        {
            // 处理2FA验证码错误
        }
    }

    // 对应API返回的2FA挑战响应模型
    private class TwoFactorChallenge
    {
        public string UserId { get; set; }
        public string Provider { get; set; }
        public string Message { get; set; }
    }
}

2. 扩展ApiClient支持2FA登录

在ApiClient中添加2FA登录方法,对接默认Identity API的登录端点:

public async Task<LoginResult> LoginWithTwoFactorAsync(string userId, string provider, string twoFactorCode)
{
    var request = new
    {
        UserId = userId,
        Provider = provider,
        Code = twoFactorCode,
        RememberMe = false
    };
    var response = await _httpClient.PostAsJsonAsync("/account/login", request);
    response.EnsureSuccessStatusCode();
    return await response.Content.ReadFromJsonAsync<LoginResult>();
}

三、API调用自动携带令牌

修改ApiClient,每次调用前从加密存储读取令牌并添加到请求头:

public class ApiClient
{
    private readonly HttpClient _httpClient;
    private readonly ProtectedLocalStorage _protectedLocalStorage;

    public ApiClient(HttpClient httpClient, ProtectedLocalStorage protectedLocalStorage)
    {
        _httpClient = httpClient;
        _protectedLocalStorage = protectedLocalStorage;
    }

    // 通用方法:添加认证令牌到请求头
    private async Task AddAuthHeader()
    {
        var storedUser = await _protectedLocalStorage.GetAsync<AuthUser>("auth-user");
        if (storedUser.Success && storedUser.Value != null)
        {
            _httpClient.DefaultRequestHeaders.Authorization = 
                new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", storedUser.Value.AccessToken);
        }
    }

    // 示例:调用受保护的API接口
    public async Task<UserProfile> GetCurrentUserProfile()
    {
        await AddAuthHeader();
        return await _httpClient.GetFromJsonAsync<UserProfile>("/api/user/profile");
    }
}

四、2FA安全优化

默认MapIdentityApi的登录端点在要求2FA时,会返回包含UserId和Provider的响应体,无需存储明文账号密码:

  1. 第一次登录返回401时,解析响应中的挑战标识
  2. 第二次调用登录端点时,携带UserId、Provider和2FA码即可完成认证
  3. 若API未返回这些信息,可在WebApi中扩展登录端点的响应内容,补充必要的挑战数据

五、刷新令牌维持登录状态

为避免令牌过期导致用户频繁登录,在ApiClient中实现刷新令牌逻辑:

public async Task RefreshTokenAsync()
{
    var storedUser = await _protectedLocalStorage.GetAsync<AuthUser>("auth-user");
    if (storedUser.Success && storedUser.Value != null && !string.IsNullOrEmpty(storedUser.Value.RefreshToken))
    {
        var refreshRequest = new { RefreshToken = storedUser.Value.RefreshToken };
        var response = await _httpClient.PostAsJsonAsync("/account/refresh", refreshRequest);
        if (response.IsSuccessStatusCode)
        {
            var newTokenData = await response.Content.ReadFromJsonAsync<LoginResult>();
            var updatedUser = storedUser.Value;
            updatedUser.AccessToken = newTokenData.AccessToken;
            updatedUser.RefreshToken = newTokenData.RefreshToken;
            
            await _protectedLocalStorage.SetAsync("auth-user", updatedUser);
            // 更新认证状态
            var authStateProvider = _serviceProvider.GetRequiredService<ApiAuthStateProvider>();
            await authStateProvider.UpdateAuthenticationState(updatedUser);
        }
    }
}

内容的提问来源于stack exchange,提问作者bckspc

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.05 07:28:13