Blazor认证与授权技能 dotnet-blazor-auth

本技能专注于在.NET Blazor应用中实现完整的认证和授权解决方案,覆盖登录/注销流程、AuthorizeView组件、ASP.NET Core Identity UI集成、OIDC外部身份提供商,支持多种Blazor托管模型(Server、WASM、Auto、SSR、Hybrid),并包括角色和策略授权机制,适用于前端开发者和全栈工程师构建安全、高效的Web应用。关键词:Blazor认证、Blazor授权、.NET安全、ASP.NET Core Identity、OIDC、前端开发。

前端开发 0 次安装 0 次浏览 更新于 3/6/2026

名称: dotnet-blazor-auth 描述: “实现Blazor认证流程:登录/注销、AuthorizeView、Identity UI、OIDC。” 用户可调用: false

dotnet-blazor-auth

在所有Blazor托管模型中实现认证和授权。涵盖AuthorizeView、CascadingAuthenticationState、Identity UI脚手架、基于角色/策略的授权、每个托管模型的认证流程差异(cookie vs token)以及外部身份提供商。

范围

  • 每个Blazor托管模型的认证流程(Server、WASM、Auto、SSR、Hybrid)
  • AuthorizeView和CascadingAuthenticationState模式
  • Identity UI脚手架和自定义
  • Blazor中基于角色/策略的授权
  • 客户端令牌处理和外部身份提供商
  • Blazor应用的显式登录/注销/认证UI实现任务

超出范围

  • JWT令牌生成和验证 – 参见[技能:dotnet-api-security]
  • OWASP安全原则 – 参见[技能:dotnet-security-owasp]
  • 无认证流程工作的CSRF/XSS/CSP/速率限制加固 – 参见[技能:dotnet-security-owasp]
  • 无认证流程实现更改的现有登录页面仅加固审查 – 参见[技能:dotnet-security-owasp]
  • bUnit测试认证组件 – 参见[技能:dotnet-blazor-testing]
  • E2E认证测试 – 参见[技能:dotnet-playwright]
  • UI框架选择 – 参见[技能:dotnet-ui-chooser]

交叉引用:[技能:dotnet-api-security]用于API级认证,[技能:dotnet-security-owasp]用于OWASP原则,[技能:dotnet-blazor-patterns]用于托管模型,[技能:dotnet-blazor-components]用于组件架构,[技能:dotnet-blazor-testing]用于bUnit测试,[技能:dotnet-playwright]用于E2E测试,[技能:dotnet-ui-chooser]用于框架选择。

路由说明:除非任务明确包括Blazor认证流程/UI实现,否则不要为OWASP加固审查加载此技能。


每个托管模型的认证流程

Blazor托管模型中的认证模式有显著差异:

关注点 InteractiveServer InteractiveWebAssembly InteractiveAuto Static SSR Hybrid
认证机制 基于cookie(服务器端) 基于令牌(JWT/OIDC) Cookie(Server阶段),Token(WASM阶段) 基于cookie(标准ASP.NET Core) 平台原生或cookie
用户状态访问 直接HttpContext访问 AuthenticationStateProvider 按阶段变化 HttpContext 平台认证API
令牌存储 不需要(cookie) localStoragesessionStorage 从cookie过渡到token 不需要(cookie) 安全存储(Keychain等)
刷新处理 电路重新连接 通过拦截器刷新令牌 自动 标准cookie续订 平台特定

InteractiveServer认证

服务器端Blazor使用cookie认证。用户通过标准ASP.NET Core登录流程认证,cookie随初始HTTP请求发送以建立SignalR电路。

// Program.cs
builder.Services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
    .AddCookie(options =>
    {
        options.LoginPath = "/Account/Login";
        options.AccessDeniedPath = "/Account/AccessDenied";
    });

builder.Services.AddCascadingAuthenticationState();
builder.Services.AddAuthorization();

注意: HttpContext在初始HTTP请求期间可用,但在SignalR电路建立后,在交互式组件的生命周期方法中为null。不要在交互式组件生命周期方法中访问HttpContext。使用AuthenticationStateProvider代替。

InteractiveWebAssembly认证

WASM在浏览器中运行。Cookie认证适用于同源API(和Backend-for-Frontend / BFF模式),但基于令牌的认证(OIDC/JWT)是跨源API和委派访问场景的标准方法:

// Client Program.cs (WASM)
builder.Services.AddOidcAuthentication(options =>
{
    options.ProviderOptions.Authority = "https://login.example.com";
    options.ProviderOptions.ClientId = "blazor-wasm-client";
    options.ProviderOptions.ResponseType = "code";
    options.ProviderOptions.DefaultScopes.Add("api");
});
// 使用BaseAddressAuthorizationMessageHandler将令牌附加到API调用
// (自动为应用基地址的请求附加令牌)
builder.Services.AddHttpClient("API", client =>
    client.BaseAddress = new Uri("https://api.example.com"))
    .AddHttpMessageHandler(sp =>
        sp.GetRequiredService<AuthorizationMessageHandler>()
            .ConfigureHandler(
                authorizedUrls: ["https://api.example.com"],
                scopes: ["api"]));

builder.Services.AddScoped(sp =>
    sp.GetRequiredService<IHttpClientFactory>().CreateClient("API"));

InteractiveAuto认证

Auto模式以InteractiveServer(cookie认证)开始,然后过渡到WASM(token认证)。处理两者:

// Server Program.cs
builder.Services.AddAuthentication()
    .AddCookie()
    .AddJwtBearer(); // 用于过渡后的WASM API调用

builder.Services.AddCascadingAuthenticationState();

Hybrid (MAUI)认证

// 注册平台特定认证
builder.Services.AddAuthorizationCore();
builder.Services.AddScoped<AuthenticationStateProvider, MauiAuthStateProvider>();

// 使用安全存储的自定义提供程序
public class MauiAuthStateProvider : AuthenticationStateProvider
{
    public override async Task<AuthenticationState> GetAuthenticationStateAsync()
    {
        var token = await SecureStorage.Default.GetAsync("auth_token");
        if (string.IsNullOrEmpty(token))
        {
            return new AuthenticationState(new ClaimsPrincipal(new ClaimsIdentity()));
        }

        var claims = ParseClaimsFromJwt(token);
        var identity = new ClaimsIdentity(claims, "jwt");
        return new AuthenticationState(new ClaimsPrincipal(identity));
    }
}

AuthorizeView

AuthorizeView根据用户的认证和授权状态有条件地渲染内容。

基本用法

<AuthorizeView>
    <Authorized>
        <p>欢迎,@context.User.Identity?.Name!</p>
        <a href="/Account/Logout">注销</a>
    </Authorized>
    <NotAuthorized>
        <a href="/Account/Login">登录</a>
    </NotAuthorized>
    <Authorizing>
        <p>检查认证中...</p>
    </Authorizing>
</AuthorizeView>

基于角色

<AuthorizeView Roles="Admin,Manager">
    <Authorized>
        <AdminDashboard />
    </Authorized>
    <NotAuthorized>
        <p>您没有访问管理员仪表板的权限。</p>
    </NotAuthorized>
</AuthorizeView>

基于策略

<AuthorizeView Policy="CanEditProducts">
    <Authorized>
        <button @onclick="EditProduct">编辑</button>
    </Authorized>
</AuthorizeView>
// 在Program.cs中注册策略
builder.Services.AddAuthorizationBuilder()
    .AddPolicy("CanEditProducts", policy =>
        policy.RequireClaim("permission", "products.edit"));

CascadingAuthenticationState

CascadingAuthenticationState将当前AuthenticationState作为级联参数提供给所有后代组件。

设置

// Program.cs -- 注册级联认证状态
builder.Services.AddCascadingAuthenticationState();

这替换了将整个应用包装在<CascadingAuthenticationState>中的旧模式。服务基础注册(.NET 8+)是首选。

在组件中消费认证状态

@code {
    [CascadingParameter]
    private Task<AuthenticationState>? AuthState { get; set; }

    private string? userName;

    protected override async Task OnInitializedAsync()
    {
        if (AuthState is not null)
        {
            var state = await AuthState;
            userName = state.User.Identity?.Name;
        }
    }
}

访问声明

var state = await AuthState;
var user = state.User;

// 检查认证
if (user.Identity?.IsAuthenticated == true)
{
    var email = user.FindFirst(ClaimTypes.Email)?.Value;
    var roles = user.FindAll(ClaimTypes.Role).Select(c => c.Value);
    var isAdmin = user.IsInRole("Admin");
}

Identity UI脚手架

ASP.NET Core Identity提供了一个完整的认证系统,包括注册、登录、电子邮件确认、密码重置和双因素认证。

将Identity添加到Blazor Web应用

# 添加Identity脚手架
dotnet add package Microsoft.AspNetCore.Identity.EntityFrameworkCore
dotnet add package Microsoft.AspNetCore.Identity.UI
// Program.cs
builder.Services.AddDbContext<ApplicationDbContext>(options =>
    options.UseSqlServer(builder.Configuration.GetConnectionString("Default")));

builder.Services.AddIdentity<ApplicationUser, IdentityRole>(options =>
{
    options.Password.RequireDigit = true;
    options.Password.RequiredLength = 8;
    options.Password.RequireNonAlphanumeric = true;
    options.SignIn.RequireConfirmedAccount = true;
})
.AddEntityFrameworkStores<ApplicationDbContext>()
.AddDefaultTokenProviders();

脚手架Identity页面

# 为自定义脚手架个别Identity页面
dotnet aspnet-codegenerator identity -dc ApplicationDbContext --files "Account.Login;Account.Register;Account.Logout"

使用Blazor组件自定义Identity UI

为了完全Blazor原生的认证体验,创建调用Identity API的Blazor组件:

@page "/Account/Login"
@inject SignInManager<ApplicationUser> SignInManager
@inject NavigationManager Navigation

<EditForm Model="loginModel" OnValidSubmit="HandleLogin" FormName="login" Enhance>
    <DataAnnotationsValidator />
    <ValidationSummary />

    <div>
        <InputText @bind-Value="loginModel.Email" placeholder="邮箱" />
    </div>
    <div>
        <InputText @bind-Value="loginModel.Password" type="password" placeholder="密码" />
    </div>
    <div>
        <InputCheckbox @bind-Value="loginModel.RememberMe" /> 记住我
    </div>

    <button type="submit">登录</button>
</EditForm>

@if (!string.IsNullOrEmpty(errorMessage))
{
    <p class="text-danger">@errorMessage</p>
}

@code {
    [SupplyParameterFromForm]
    private LoginModel loginModel { get; set; } = new();

    private string? errorMessage;

    private async Task HandleLogin()
    {
        var result = await SignInManager.PasswordSignInAsync(
            loginModel.Email, loginModel.Password,
            loginModel.RememberMe, lockoutOnFailure: true);

        if (result.Succeeded)
        {
            Navigation.NavigateTo("/", forceLoad: true);
        }
        else if (result.RequiresTwoFactor)
        {
            Navigation.NavigateTo("/Account/LoginWith2fa");
        }
        else if (result.IsLockedOut)
        {
            errorMessage = "账户已锁定。请稍后重试。";
        }
        else
        {
            errorMessage = "无效登录尝试。";
        }
    }
}

注意: SignInManager使用HttpContext设置cookie。在交互式渲染模式中,电路建立后HttpContext不可用。登录/注销页面必须使用Static SSR(无@rendermode)以便访问HttpContext进行cookie操作。


基于角色和策略的授权

页面级授权

@page "/admin"
@attribute [Authorize(Roles = "Admin")]

<h1>管理员面板</h1>
@page "/products/manage"
@attribute [Authorize(Policy = "ProductManager")]

<h1>管理产品</h1>

定义策略

builder.Services.AddAuthorizationBuilder()
    .AddPolicy("ProductManager", policy =>
        policy.RequireRole("Admin", "ProductManager"))
    .AddPolicy("CanDeleteOrders", policy =>
        policy.RequireClaim("permission", "orders.delete")
              .RequireAuthenticatedUser())
    .AddPolicy("MinimumAge", policy =>
        policy.AddRequirements(new MinimumAgeRequirement(18)));

自定义授权处理程序

public sealed class MinimumAgeRequirement(int minimumAge) : IAuthorizationRequirement
{
    public int MinimumAge { get; } = minimumAge;
}

public sealed class MinimumAgeHandler : AuthorizationHandler<MinimumAgeRequirement>
{
    protected override Task HandleRequirementAsync(
        AuthorizationHandlerContext context,
        MinimumAgeRequirement requirement)
    {
        var dateOfBirthClaim = context.User.FindFirst("date_of_birth");
        if (dateOfBirthClaim is not null
            && DateOnly.TryParse(dateOfBirthClaim.Value, out var dob))
        {
            var age = DateOnly.FromDateTime(DateTime.UtcNow).Year - dob.Year;
            if (age >= requirement.MinimumAge)
            {
                context.Succeed(requirement);
            }
        }
        return Task.CompletedTask;
    }
}

// 注册
builder.Services.AddSingleton<IAuthorizationHandler, MinimumAgeHandler>();

组件中的程序化授权

@inject IAuthorizationService AuthorizationService

@code {
    [CascadingParameter]
    private Task<AuthenticationState>? AuthState { get; set; }

    private bool canEdit;

    protected override async Task OnInitializedAsync()
    {
        if (AuthState is not null)
        {
            var state = await AuthState;
            var result = await AuthorizationService.AuthorizeAsync(
                state.User, "CanEditProducts");
            canEdit = result.Succeeded;
        }
    }
}

外部身份提供商

添加外部提供商

builder.Services.AddAuthentication()
    .AddMicrosoftAccount(options =>
    {
        options.ClientId = builder.Configuration["Auth:Microsoft:ClientId"]!;
        options.ClientSecret = builder.Configuration["Auth:Microsoft:ClientSecret"]!;
    })
    .AddGoogle(options =>
    {
        options.ClientId = builder.Configuration["Auth:Google:ClientId"]!;
        options.ClientSecret = builder.Configuration["Auth:Google:ClientSecret"]!;
    });

每个托管模型的外部登录流程

托管模型 流程 备注
InteractiveServer / Static SSR 标准OAuth重定向(服务器端) 回调后存储cookie
InteractiveWebAssembly 带PKCE的OIDC(客户端) 令牌存储在浏览器中
Hybrid (MAUI) WebAuthenticator或MSAL 平台特定安全存储

对于WASM,在客户端项目中配置OIDC提供商:

// Client Program.cs
builder.Services.AddOidcAuthentication(options =>
{
    options.ProviderOptions.Authority = "https://login.microsoftonline.com/{tenant}";
    options.ProviderOptions.ClientId = "{client-id}";
    options.ProviderOptions.ResponseType = "code";
});

对于MAUI Hybrid:

var result = await WebAuthenticator.Default.AuthenticateAsync(
    new Uri("https://login.example.com/authorize"),
    new Uri("myapp://callback"));
var token = result.AccessToken;

代理注意事项

  1. 不要在交互式组件中访问HttpContext HttpContext仅在初始HTTP请求期间可用。SignalR电路建立后(InteractiveServer)或WASM运行时加载后,其为null。使用AuthenticationStateProviderCascadingAuthenticationState代替。
  2. 在WASM中不要依赖cookie进行跨源或委派API访问。 对于跨源API,使用带AuthorizationMessageHandler的OIDC/JWT。对于WASM应用,同源和Backend-for-Frontend(BFF)cookie认证仍然有效。
  3. 不要在交互式模式中渲染登录/注销页面。 SignInManager需要HttpContext设置/清除cookie。登录和注销页面必须使用Static SSR渲染模式。
  4. 在不考虑XSS的情况下不要将令牌存储在localStorage中。 如果应用易受XSS攻击,localStorage中的令牌可能被盗。使用sessionStorage(标签关闭时清除)或带PKCE的OIDC库内置存储机制。
  5. 不要忘记AddCascadingAuthenticationState() 没有它,组件中[CascadingParameter] Task<AuthenticationState>始终为null,无声地破坏认证检查。
  6. 不要同时使用AddIdentityAddDefaultIdentity AddDefaultIdentity包括UI脚手架;AddIdentity不包括。根据是否想要默认Identity UI页面选择其一。

先决条件

  • .NET 8.0+(带渲染模式的Blazor Web应用,AddCascadingAuthenticationState服务注册)
  • Microsoft.AspNetCore.Identity.EntityFrameworkCore用于Identity与EF Core
  • Microsoft.AspNetCore.Identity.UI用于默认Identity UI脚手架
  • Microsoft.AspNetCore.Authentication.MicrosoftAccount / .Google用于外部提供商
  • Microsoft.Authentication.WebAssembly.Msal用于WASM与Microsoft Identity(Azure AD/Entra)

参考