Blazor Server 的认证/授权在运行时是“服务端驱动”的:浏览器通过 SignalR 与服务端保持 Circuit,每个连接对应一个 ClaimsPrincipal(用户身份)。组件通过 AuthenticationStateProvider 获取当前 ClaimsPrincipal 并响应变更。

角色授权(Role)是基于 ClaimTypes.Role 的简单方式;策略授权(Policy)更灵活,可基于任意 Claim、时间、外部调用或自定义验证器。

OIDC / IdentityServer:典型做法是把 Blazor Server 当作 “OAuth/OpenID Connect 客户端” — 使用 Cookie + OIDC challenge 流程完成登录/登出并把 token/claims 存入服务端会话。

模拟登录(开发/调试):常用做法是提供一个替代的 AuthenticationStateProvider(Fake),在开发环境注册并在页面上输入 EmpNo 即可切换用户;切勿在生产环境使用这种方式。

AuthenticationStateProvider(工作原理 + 常见用法):

  • AuthenticationStateProvider 是 Blazor 用来提供 AuthenticationState(包含 ClaimsPrincipal)的抽象类。

  • 组件通过注入 AuthenticationStateProvider 或使用 AuthorizeView / CascadingAuthenticationState 来访问认证信息。

  • 当用户身份变化时,AuthenticationStateProvider.NotifyAuthenticationStateChanged(Task<AuthenticationState>) 会触发 UI 更新。

代码示例:

App.razor:这是整个应用的入口,确保 认证状态 在所有组件中都能访问

<!-- App.razor -->

<!-- CascadingAuthenticationState 会把 AuthenticationState 作为级联参数提供给子组件 -->
<CascadingAuthenticationState>
    <!-- Router 控制路由匹配 -->
    <Router AppAssembly="@typeof(Program).Assembly">
        <Found Context="routeData">
            <!-- AuthorizeRouteView:和普通 RouteView 的区别是它能识别 [Authorize] 属性 -->
            <!-- 如果页面/组件需要授权,会自动跳转到 NotAuthorized 显示 -->
            <AuthorizeRouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)" />
        </Found>
        <NotFound>
            <LayoutView Layout="@typeof(MainLayout)">
                <p>页面未找到</p>
            </LayoutView>
        </NotFound>
    </Router>
</CascadingAuthenticationState>

在页面中获取当前用户:

@page "/userinfo"
@inject AuthenticationStateProvider AuthStateProvider

<h3>用户信息</h3>

@if (user != null && user.Identity?.IsAuthenticated == true)
{
    <p>用户名:@user.Identity.Name</p>
    <p>EmpNo:@empNo</p>
    <p>角色:@string.Join(",", user.FindAll(System.Security.Claims.ClaimTypes.Role).Select(r => r.Value))</p>
}
else
{
    <p>未登录</p>
}

@code {
    private ClaimsPrincipal? user;
    private string? empNo;

    protected override async Task OnInitializedAsync()
    {
        // 从 AuthenticationStateProvider 获取当前认证状态
        var authState = await AuthStateProvider.GetAuthenticationStateAsync();

        // 取出当前用户
        user = authState.User;

        // 查找名为 "empNo" 的 claim
        empNo = user.FindFirst("empNo")?.Value;
    }
}
  • authState.User 返回的是一个 ClaimsPrincipal,里面包含所有的用户声明(Claims)。

  • 你可以从 user.Identity.Name 拿到用户名。

  • 你也可以用 FindFirst("empNo") 拿到自定义的工号字段。

  • 如果要检查用户角色,用 user.IsInRole("Admin") 或枚举 ClaimsPrincipal 的 Role claims。

使用 AuthorizeView 控制 UI:

在 UI 层,很多时候只需要简单判断「已登录/未登录」「是否管理员」,用 AuthorizeView 最方便。

<h3>页面内容</h3>

<!-- 普通登录状态判断 -->
<AuthorizeView>
    <Authorized>
        <!-- 已登录用户能看到 -->
        <p>欢迎你,@context.User.Identity?.Name</p>
    </Authorized>
    <NotAuthorized>
        <!-- 未登录用户看到 -->
        <p>请先登录</p>
    </NotAuthorized>
</AuthorizeView>


<!-- 限定角色(例如 Admin) -->
<AuthorizeView Roles="Admin">
    <Authorized>
        <p>只有 Admin 角色能看到这段内容</p>
    </Authorized>
    <NotAuthorized>
        <p>你不是管理员,无法访问此功能</p>
    </NotAuthorized>
</AuthorizeView>
  • AuthorizeView 自动接收 AuthenticationState(因为我们在 App.razor 用了 CascadingAuthenticationState)。

  • 内部的 context.User 就是当前的 ClaimsPrincipal

  • Roles="Admin" 表示限制必须有角色 Admin,否则会显示 <NotAuthorized>

  • 这种方式一般用来控制按钮、菜单、部分 UI 的显示/隐藏。

基于角色 / 策略的授权

1)角色授权

  • 用法:[Authorize(Roles = "Admin")]<AuthorizeView Roles="Admin">

  • 依赖 ClaimsPrincipal 中的 role claim(默认 claim type: ClaimTypes.Rolerole,你可以配置 TokenValidationParameters.RoleClaimType)。

@attribute [Authorize(Roles = "Admin")]

2)策略授权

定义策略(在 Program.cs):

Program.cs 配置授权服务

using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Components.Authorization;

var builder = WebApplication.CreateBuilder(args);

// 添加认证(假设用 Cookie 或 OIDC,这里用示例的伪配置)
builder.Services.AddAuthentication("Cookies")
    .AddCookie("Cookies", options =>
    {
        options.LoginPath = "/login"; // 未登录时跳转的页面
    });

// 配置授权策略
builder.Services.AddAuthorization(options =>
{
    // 策略1:基于角色
    options.AddPolicy("RequireAdmin", policy => policy.RequireRole("Admin"));

    // 策略2:基于 claim(部门为 HR)
    options.AddPolicy("InHR", policy => policy.RequireClaim("department", "HR"));

    // 策略3:自定义要求(限定 EmpNo=1001)
    options.AddPolicy("Emp1001Only", policy =>
        policy.Requirements.Add(new EmpNoRequirement("1001")));
});

// 注册自定义处理器
builder.Services.AddSingleton<IAuthorizationHandler, EmpNoHandler>();

// Blazor 内置认证状态管理
builder.Services.AddScoped<AuthenticationStateProvider, 
    RevalidatingIdentityAuthenticationStateProvider<IdentityUser>>();

var app = builder.Build();

app.UseAuthentication();
app.UseAuthorization();

app.MapBlazorHub();
app.MapFallbackToPage("/_Host");

app.Run();

定义自定义策略 Requirement & Handler

using Microsoft.AspNetCore.Authorization;
using System.Security.Claims;

/// <summary>
/// 自定义要求:只允许特定的员工号(empNo)访问
/// </summary>
public class EmpNoRequirement : IAuthorizationRequirement
{
    public string EmpNo { get; }
    public EmpNoRequirement(string empNo) => EmpNo = empNo;
}

/// <summary>
/// 自定义处理器:判断当前用户的 empNo claim 是否满足要求
/// </summary>
public class EmpNoHandler : AuthorizationHandler<EmpNoRequirement>
{
    protected override Task HandleRequirementAsync(
        AuthorizationHandlerContext context, 
        EmpNoRequirement requirement)
    {
        var emp = context.User.FindFirst("empNo")?.Value;

        if (emp == requirement.EmpNo)
        {
            // ✅ 满足要求,标记为成功
            context.Succeed(requirement);
        }

        return Task.CompletedTask;
    }
}

Razor 组件中使用角色 / 策略授权

基于角色的授权

@page "/admin"
@attribute [Authorize(Roles = "Admin")]  <!-- 只有角色=Admin 才能访问 -->

<h3>Admin Page</h3>
<p>只有管理员可以看到此页面内容。</p>

基于策略的授权

@page "/hr"
@attribute [Authorize(Policy = "InHR")]  <!-- 必须 department=HR -->

<h3>HR Page</h3>
<p>只有HR部门的用户才能访问此页面。</p>

使用自定义策略(EmpNo=1001)

@page "/special"
@attribute [Authorize(Policy = "Emp1001Only")]

<h3>Special Employee Page</h3>
<p>只有员工号 = 1001 的用户可以访问。</p>

使用 <AuthorizeView> 控制 UI

<AuthorizeView Roles="Admin">
    <Authorized>
        <p>你是管理员,可以看到此段文字。</p>
    </Authorized>
    <NotAuthorized>
        <p>你不是管理员,无法看到内容。</p>
    </NotAuthorized>
</AuthorizeView>

在代码中动态检查授权

@page "/check"
@inject IAuthorizationService AuthorizationService
@inject AuthenticationStateProvider AuthStateProvider

<h3>Runtime Authorization Check</h3>

<p>@message</p>

@code {
    private string message = "Checking...";

    protected override async Task OnInitializedAsync()
    {
        var authState = await AuthStateProvider.GetAuthenticationStateAsync();
        var user = authState.User;

        // 检查 "RequireAdmin" 策略
        var result = await AuthorizationService.AuthorizeAsync(user, null, "RequireAdmin");

        if (result.Succeeded)
        {
            message = $"✅ 用户 {user.Identity?.Name} 具备 Admin 权限";
        }
        else
        {
            message = $"❌ 用户 {user.Identity?.Name} 没有 Admin 权限";
        }
    }
}

OIDC 认证(访问c#官网 可看完整代码)

配置 OIDC 登录 (Program.cs)

using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Authentication.OpenIdConnect;
using Microsoft.IdentityModel.Tokens;

var builder = WebApplication.CreateBuilder(args);

// 添加认证:OIDC + Cookie
builder.Services.AddAuthentication(options =>
{
    options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
    options.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
})
.AddCookie(CookieAuthenticationDefaults.AuthenticationScheme)
.AddOpenIdConnect(OpenIdConnectDefaults.AuthenticationScheme, options =>
{
    options.Authority = "https://oidc.your-company.com"; // 公司提供的OIDC地址
    options.ClientId = "your-client-id";
    options.ClientSecret = "your-client-secret";
    options.ResponseType = "code";   // 使用授权码流

    options.SaveTokens = true;       // 保存access/refresh token
    options.GetClaimsFromUserInfoEndpoint = true; // 从userinfo端点拿额外的claims

    // 映射Claim类型(根据IdP返回情况调整)
    options.TokenValidationParameters = new TokenValidationParameters
    {
        NameClaimType = "name",       // 默认显示用户名
        RoleClaimType = "role"        // 指定角色 claim 类型
    };

    // 如果公司返回了 empNo、department 等字段,手动映射
    options.ClaimActions.MapUniqueJsonKey("empNo", "empNo");
    options.ClaimActions.MapUniqueJsonKey("department", "department");
    options.ClaimActions.MapUniqueJsonKey("role", "role");
});

builder.Services.AddAuthorization(options =>
{
    // 示例策略:HR 部门
    options.AddPolicy("InHR", policy =>
        policy.RequireClaim("department", "HR"));

    // 示例策略:管理员角色
    options.AddPolicy("RequireAdmin", policy =>
        policy.RequireRole("Admin"));
});

var app = builder.Build();

app.UseAuthentication();
app.UseAuthorization();

app.MapBlazorHub();
app.MapFallbackToPage("/_Host");

app.Run();

App.razor 提供认证上下文

<CascadingAuthenticationState>
    <Router AppAssembly="@typeof(Program).Assembly">
        <Found Context="routeData">
            <AuthorizeRouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)" />
        </Found>
        <NotFound>
            <LayoutView Layout="@typeof(MainLayout)">
                <p>Page not found</p>
            </LayoutView>
        </NotFound>
    </Router>
</CascadingAuthenticationState>

在 Blazor 组件中读取 OIDC 用户信息

@page "/userinfo"
@inject AuthenticationStateProvider AuthStateProvider

<h3>用户信息</h3>

@if (user != null)
{
    <p><b>姓名:</b> @user.Identity?.Name</p>
    <p><b>工号:</b> @user.FindFirst("empNo")?.Value</p>
    <p><b>部门:</b> @user.FindFirst("department")?.Value</p>
    <p><b>角色:</b> @string.Join(", ", user.FindAll("role").Select(r => r.Value))</p>
}
else
{
    <p>未登录</p>
}

@code {
    private ClaimsPrincipal user;

    protected override async Task OnInitializedAsync()
    {
        var authState = await AuthStateProvider.GetAuthenticationStateAsync();
        user = authState.User;
    }
}

使用 <AuthorizeView> 控制 UI

<AuthorizeView Roles="Admin">
    <Authorized>
        <p>✅ 你是管理员,可以访问管理员内容</p>
    </Authorized>
    <NotAuthorized>
        <p>❌ 没有管理员权限</p>
    </NotAuthorized>
</AuthorizeView>

<AuthorizeView Policy="InHR">
    <Authorized>
        <p>✅ 你是 HR 部门,可以访问 HR 内容</p>
    </Authorized>
</AuthorizeView>
flowchart TD

A[用户在浏览器点击 "登录" 按钮] --> B[Blazor Server 检测未认证, Redirect 到 OIDC 认证端点]

B --> C[OIDC Provider (公司 SSO 登录页面)]
C -->|用户输入账号/密码, 身份验证| D[OIDC Provider 生成 Auth Code]

D --> E[OIDC Provider 回调到 Blazor Server /signin-oidc]
E --> F[Blazor Server 使用 Auth Code 向 OIDC 获取 Access Token & ID Token]

F --> G[Blazor Server 验证 Token, 解析 Claims (empNo, department, role...)]
G --> H[ASP.NET Core Authentication 中间件把用户写入 Cookie]

H --> I[浏览器携带 Cookie 回到 Blazor 应用]
I --> J[Blazor Server 恢复用户身份, 构建 ClaimsPrincipal]

J --> K[Blazor UI 中 AuthenticationStateProvider 提供 User 信息]
K --> L[页面通过 <AuthorizeView> / [Authorize] 控制访问内容]
  • 点击登录按钮

    • 如果未登录,Blazor Server 会触发 Challenge(),跳转到公司 OIDC 登录页。

  • 公司 OIDC 登录

    • 用户在公司提供的统一认证系统(SSO)输入账号密码。

  • 回调 Blazor

    • OIDC IdP 返回 Auth Code/signin-oidc

  • 交换 Token

    • Blazor Server 向 OIDC 请求 Access Token + ID Token,并保存到 Cookie。

  • ClaimsPrincipal 构建

    • empNodepartmentrole 等信息被存入用户 Claims。

  • 页面授权

    • 在 Blazor 页面中,AuthenticationStateProvider 提供用户上下文。

    • <AuthorizeView>[Authorize] 根据 Claims 判断是否显示内容。

模拟登录:

Fake AuthenticationStateProvider

  • 覆写 Blazor 的 AuthenticationStateProvider,内部维护一个 ClaimsPrincipal

  • 调用 NotifyAuthenticationStateChanged(...) 通知 UI 更新。

  • 仅影响 Blazor 组件内的 AuthenticationStateProviderAuthorizeViewAuthenticationStateProvider.GetAuthenticationStateAsync() 等接口。不会更改服务器端的 cookie / HttpContext.User

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

public class FakeAuthenticationStateProvider : AuthenticationStateProvider
{
    private readonly ClaimsPrincipal _anonymous = new(new ClaimsIdentity());
    private ClaimsPrincipal _current;

    public FakeAuthenticationStateProvider()
    {
        _current = _anonymous;
    }

    public override Task<AuthenticationState> GetAuthenticationStateAsync()
    {
        return Task.FromResult(new AuthenticationState(_current));
    }

    // 标记为已登录,roles 可以为 null 或数组
    public void MarkUserAsAuthenticated(string empNo, string[] roles = null)
    {
        var claims = new List<Claim>
        {
            new Claim(ClaimTypes.Name, $"Emp-{empNo}"),
            new Claim("empNo", empNo),
            new Claim(ClaimTypes.NameIdentifier, empNo)
        };
        if (roles != null)
            claims.AddRange(roles.Select(r => new Claim(ClaimTypes.Role, r)));

        var identity = new ClaimsIdentity(claims, "fake");
        _current = new ClaimsPrincipal(identity);

        // 通知所有订阅者(UI)认证状态已改变
        NotifyAuthenticationStateChanged(Task.FromResult(new AuthenticationState(_current)));
    }

    // 注销
    public void MarkUserAsLoggedOut()
    {
        _current = _anonymous;
        NotifyAuthenticationStateChanged(Task.FromResult(new AuthenticationState(_current)));
    }
}

Program.cs仅在 Development 注册

// ... 已有 AddAuthentication/AddOpenIdConnect 的配置(OIDC)
if (builder.Environment.IsDevelopment())
{
    // 将 Fake 注册为 AuthenticationStateProvider — 覆盖默认实现(仅 Blazor 组件层)
    builder.Services.AddScoped<FakeAuthenticationStateProvider>();
    builder.Services.AddScoped<AuthenticationStateProvider>(sp => sp.GetRequiredService<FakeAuthenticationStateProvider>());
}

在 UI 中(MainLayout.razor)放一个“开发模拟登录”输入框(仅开发环境显示)


@inject FakeAuthenticationStateProvider FakeAuth
@inject Microsoft.AspNetCore.Hosting.IWebHostEnvironment Env

@if (Env.IsDevelopment())
{
    <div style="position: absolute; right: 10px; top: 10px;">
        <input @bind="empNo" @onkeydown="OnKeyDown" placeholder="输入 EmpNo 模拟登录" />
        <button @onclick="DoLogin">模拟登录</button>
        <button @onclick="DoLogout">退出</button>
    </div>
}

@code {
    private string empNo = "";

    private async Task OnKeyDown(KeyboardEventArgs e)
    {
        if (e.Key == "Enter") await DoLogin();
    }

    private Task DoLogin()
    {
        if (!string.IsNullOrWhiteSpace(empNo))
        {
            // 给组件层注入一个 roles 示例
            FakeAuth.MarkUserAsAuthenticated(empNo, new[] { "User" });
        }
        return Task.CompletedTask;
    }

    private void DoLogout() => FakeAuth.MarkUserAsLoggedOut();
}

开发用 Cookie 签发(后端可识别)

  • 在开发环境中提供一个受限的后端接口(如 /devauth/sign-in),该接口调用 HttpContext.SignInAsync(cookieScheme, ClaimsPrincipal, AuthenticationProperties) 发出 cookie。

  • Cookie 的 scheme 要与 App 中的 Cookie auth scheme(AddCookie 所用)一致,这样后端控制器、Razor 页面以及 Blazor 服务端都会看到相同的 HttpContext.User

  • 控制器签发 cookie 之后,浏览器带着 cookie 与服务端建立/恢复 Circuit,整个应用都识别该模拟用户。

先在 Program.cs 开启 MVC 控制器并仅在 Development 映射接口:

// 在 Services 注册 Controllers
builder.Services.AddControllers(); // 或 AddControllersWithViews

// 下面是你原来的认证配置(Cookie + OIDC)示例(确保 Cookie scheme 名称一致)
builder.Services.AddAuthentication(options =>
{
    options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
    options.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
})
.AddCookie(CookieAuthenticationDefaults.AuthenticationScheme) // 默认 "Cookies"
.AddOpenIdConnect(OpenIdConnectDefaults.AuthenticationScheme, options =>
{
    // ... OIDC 配置(Authority, ClientId, ClientSecret 等)
});

DevAuthController.cs

using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.Cookies;
using System.Security.Claims;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Hosting;

[ApiController]
[Route("devauth")]
public class DevAuthController : Controller
{
    private readonly IConfiguration _config;
    private readonly IHostEnvironment _env;

    public DevAuthController(IConfiguration config, IHostEnvironment env)
    {
        _config = config;
        _env = env;
    }

    // GET /devauth/sign-in?empNo=1001&roles=Admin,User&returnUrl=/
    [HttpGet("sign-in")]
    public async Task<IActionResult> SignIn(string empNo, string roles = null, string returnUrl = "/",
                                            string devSecret = null)
    {
        // 只允许开发环境
        if (!_env.IsDevelopment()) return NotFound();

        // 可选:校验 devSecret(把真实的 secret 放在 UserSecrets 或 appsettings.Development.json)
        var expected = _config["DevAuth:Secret"];
        if (!string.IsNullOrEmpty(expected) && expected != devSecret)
            return Unauthorized();

        if (string.IsNullOrWhiteSpace(empNo)) return BadRequest("empNo required");

        var claims = new List<Claim>
        {
            new Claim(ClaimTypes.Name, $"Emp-{empNo}"),
            new Claim("empNo", empNo),
            new Claim(ClaimTypes.NameIdentifier, empNo)
        };

        if (!string.IsNullOrWhiteSpace(roles))
        {
            foreach (var r in roles.Split(',', StringSplitOptions.RemoveEmptyEntries))
            {
                claims.Add(new Claim(ClaimTypes.Role, r.Trim()));
            }
        }

        var identity = new ClaimsIdentity(claims, CookieAuthenticationDefaults.AuthenticationScheme);
        var principal = new ClaimsPrincipal(identity);

        var props = new AuthenticationProperties
        {
            IsPersistent = false,
            // ExpiresUtc = DateTimeOffset.UtcNow.AddHours(8) // 根据需要设置
        };

        // 关键:这里使用与你的 AddCookie(...) 相同的 scheme(通常 "Cookies")
        await HttpContext.SignInAsync(CookieAuthenticationDefaults.AuthenticationScheme, principal, props);

        // 重定向回 returnUrl(或前端当前页面),forceLoad=true 会强制浏览器重新加载页面
        return LocalRedirect(returnUrl);
    }

    // GET /devauth/sign-out
    [HttpGet("sign-out")]
    public async Task<IActionResult> SignOutDev()
    {
        if (!_env.IsDevelopment()) return NotFound();

        await HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
        return LocalRedirect("/");
    }
}

前端触发

@inject NavigationManager Nav

<button @onclick="DevSignIn">开发环境 模拟登录</button>

@code {
    private void DevSignIn()
    {
        // 直接把 empNo 和 roles 编入 query,这里示例 empNo=1001, Admin role
        var url = $"/devauth/sign-in?empNo=1001&roles=Admin&devSecret={Uri.EscapeDataString("your-secret-if-configured")}&returnUrl={Uri.EscapeDataString(Nav.Uri)}";
        // forceLoad=true 会让浏览器进行完整导航,后端写 cookie 后页面会重载并带 cookie
        Nav.NavigateTo(url, forceLoad: true);
    }
}

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐