mcpField
D
dotnet-blazor-auth

dotnet-blazor-auth

github.com/wshaddix安全8685 次安装35 周前更新
代码仓库

关于 dotnet-blazor-auth

面向 Blazor 应用开发者的认证与授权指南,覆盖不同托管模式下的认证流程、界面组件、策略授权和 Identity 页面脚手架。

主要功能

  1. 01按托管模式配置认证流程
  2. 02使用 AuthorizeView 和认证状态
  3. 03Identity UI 页面脚手架
  4. 04基于角色和策略授权
  5. 05外部身份提供商
  6. 06令牌处理指南

适用场景

  1. 01为 Blazor 应用添加登录和退出界面
  2. 02按角色或策略限制页面访问
  3. 03为 Server、WebAssembly 或 Hybrid 配置认证

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/wshaddix/dotnet-skills --skill dotnet-blazor-auth

安装后在 Claude Code 中输入/dotnet-blazor-auth确认已加载。

详细介绍

dotnet-blazor-auth

适用于所有 Blazor 托管模型的身份验证和授权。涵盖 AuthorizeView、CascadingAuthenticationState、Identity UI 基架、基于角色/策略的授权、各托管模型的身份验证流程差异(Cookie 与令牌),以及外部身份提供程序。

**范围边界:**此技能负责 Blazor 特有的身份验证 UI 模式——AuthorizeView、CascadingAuthenticationState、Identity UI 基架、客户端令牌处理,以及各托管模型的身份验证流程配置。API 级身份验证(JWT、OAuth/OIDC、通行密钥、CORS、速率限制)——请参阅 [skill:dotnet-api-security]。

**不在范围内:**JWT 令牌生成和验证——请参阅 [skill:dotnet-api-security]。OWASP 安全原则——请参阅 [skill:dotnet-security-owasp]。使用 bUnit 测试身份验证组件——请参阅 [skill:dotnet-blazor-testing]。E2E 身份验证测试——请参阅 [skill:dotnet-playwright]。UI 框架选择——请参阅 [skill:dotnet-ui-chooser]。

交叉引用:[skill:dotnet-api-security](API 级身份验证)、[skill:dotnet-security-owasp](OWASP 原则)、[skill:dotnet-blazor-patterns](托管模型)、[skill:dotnet-blazor-components](组件架构)、[skill:dotnet-blazor-testing](bUnit 测试)、[skill:dotnet-playwright](E2E 测试)、[skill:dotnet-ui-chooser](框架选择)。


各托管模型的身份验证流程

不同 Blazor 托管模型的身份验证模式差异显著:

关注点InteractiveServerInteractiveWebAssemblyInteractiveAutoStatic SSRHybrid
身份验证机制基于 Cookie(服务端)基于令牌(JWT/OIDC)Cookie(Server 阶段),令牌(WASM 阶段)基于 Cookie(标准 ASP.NET Core)平台原生或 Cookie
用户状态访问直接访问 HttpContextAuthenticationStateProvider因阶段而异HttpContext平台身份验证 API
令牌存储无需(Cookie)localStorage 或 sessionStorage从 Cookie 切换到令牌无需(Cookie)安全存储(Keychain 等)
刷新处理Circuit 重新连接通过拦截器刷新令牌自动标准 Cookie 续期依平台而定
InteractiveServer 身份验证

服务端 Blazor 使用 Cookie 身份验证。用户通过标准 ASP.NET Core 登录流程完成身份验证,建立 SignalR circuit 的初始 HTTP 请求会携带 Cookie。

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

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

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

InteractiveWebAssembly 身份验证

WASM 在浏览器中运行。对于同源 API(以及 Backend-for-Frontend / BFF 模式),Cookie 身份验证可正常工作;但对于跨源 API 和委托访问场景,基于令牌的身份验证(OIDC/JWT)是标准做法:

// 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");
});
// Attach tokens to API calls using BaseAddressAuthorizationMessageHandler
// (auto-attaches tokens for requests to the app's base address)
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(令牌身份验证)。两种情况都要处理:

// Server Program.cs
builder.Services.AddAuthentication()
    .AddCookie()
    .AddJwtBearer(); // For WASM API calls after transition

builder.Services.AddCascadingAuthenticationState();
Hybrid(MAUI)身份验证
// Register platform-specific auth
builder.Services.AddAuthorizationCore();
builder.Services.AddScoped<AuthenticationStateProvider, MauiAuthStateProvider>();

// Custom provider using secure storage
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>Welcome, @context.User.Identity?.Name!</p>
        <a href="/Account/Logout">Log out</a>
    </Authorized>
    <NotAuthorized>
        <a href="/Account/Login">Log in</a>
    </NotAuthorized>
    <Authorizing>
        <p>Checking authentication...</p>
    </Authorizing>
</AuthorizeView>
基于角色
<AuthorizeView Roles="Admin,Manager">
    <Authorized>
        <AdminDashboard />
    </Authorized>
    <NotAuthorized>
        <p>You do not have access to the admin dashboard.</p>
    </NotAuthorized>
</AuthorizeView>
基于策略
<AuthorizeView Policy="CanEditProducts">
    <Authorized>
        <button @onclick="EditProduct">Edit</button>
    </Authorized>
</AuthorizeView>
// Register policy in Program.cs
builder.Services.AddAuthorizationBuilder()
    .AddPolicy("CanEditProducts", policy =>
        policy.RequireClaim("permission", "products.edit"));

CascadingAuthenticationState

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

设置
// Program.cs -- register cascading auth state
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;

// Check authentication
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 提供完整的身份验证系统,支持注册、登录、电子邮件确认、密码重置和双重身份验证。

向 Blazor Web App 添加 Identity
# Add Identity scaffolding
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 页面
# Scaffold individual Identity pages for customization
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="Email" />
    </div>
    <div>
        <InputText @bind-Value="loginModel.Password" type="password" placeholder="Password" />
    </div>
    <div>
        <InputCheckbox @bind-Value="loginModel.RememberMe" /> Remember me
    </div>

    <button type="submit">Log in</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 = "Account is locked. Try again later.";
        }
        else
        {
            errorMessage = "Invalid login attempt.";
        }
    }
}

注意事项: SignInManager 使用 HttpContext 设置 Cookie。在交互式呈现模式下,建立 circuit 后将无法使用 HttpContext。登录/注销页面必须使用 Static SSR(不设置 @rendermode),以便访问 HttpContext 执行 Cookie 操作。


基于角色和策略的授权

页面级授权
@page "/admin"
@attribute [Authorize(Roles = "Admin")]

<h1>Admin Panel</h1>
@page "/products/manage"
@attribute [Authorize(Policy = "ProductManager")]

<h1>Manage Products</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;
    }
}

// Register
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;

Agent 注意事项

  1. 不要在交互式组件中访问 HttpContext。 HttpContext 仅在初始 HTTP 请求期间可用。SignalR circuit 建立后(InteractiveServer),或 WASM 运行时加载后,它将为 null。请改用 AuthenticationStateProvider 或 CascadingAuthenticationState。
  2. 不要在 WASM 中依赖 Cookie 进行跨源或委托式 API 访问。 对于跨源 API,请使用 OIDC/JWT 和 AuthorizationMessageHandler。对于 WASM 应用,同源 Cookie 身份验证和 Backend-for-Frontend(BFF)Cookie 身份验证仍然有效。
  3. 不要以 Interactive 模式呈现登录/注销页面。 SignInManager 需要 HttpContext 来设置/清除 Cookie。登录和注销页面必须使用 Static SSR 呈现模式。
  4. 如果未考虑 XSS 风险,不要将令牌存储在 localStorage 中。 如果应用存在 XSS 漏洞,localStorage 中的令牌可能会被盗取。请使用 sessionStorage(关闭标签页时清除),或使用内置存储机制并启用 PKCE 的 OIDC 库。
  5. 不要忘记 AddCascadingAuthenticationState()。 如果未添加此服务,组件中的 [CascadingParameter] Task<AuthenticationState> 始终为 null,导致身份验证检查在无提示的情况下失效。
  6. 不要同时使用 AddIdentity 和 AddDefaultIdentity。 AddDefaultIdentity 包含 UI 搭建,而 AddIdentity 不包含。请根据是否需要默认的 Identity UI 页面选择其中之一。

前置条件

  • .NET 8.0+(支持呈现模式的 Blazor Web App,以及 AddCascadingAuthenticationState 服务注册)
  • 用于通过 EF Core 实现 Identity 的 Microsoft.AspNetCore.Identity.EntityFrameworkCore
  • 用于搭建默认 Identity UI 的 Microsoft.AspNetCore.Identity.UI
  • 用于外部提供程序的 Microsoft.AspNetCore.Authentication.MicrosoftAccount / .Google
  • 用于通过 Microsoft Identity(Azure AD/Entra)实现 WASM 身份验证的 Microsoft.Authentication.WebAssembly.Msal

参考资料

评论

支持 Markdown