blazor学习笔记---认证、授权、OIDC以及模拟登录
的认证/授权在运行时是“服务端驱动”的:浏览器通过 SignalR 与服务端保持 Circuit,每个连接对应一个(用户身份)。组件通过获取当前并响应变更。(Role)是基于的简单方式;(Policy)更灵活,可基于任意 Claim、时间、外部调用或自定义验证器。:典型做法是把 Blazor Server 当作 “OAuth/OpenID Connect 客户端” — 使用 Cookie + OI
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.Role
或role
,你可以配置 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 构建
-
empNo
、department
、role
等信息被存入用户 Claims。
-
-
页面授权
-
在 Blazor 页面中,
AuthenticationStateProvider
提供用户上下文。 -
<AuthorizeView>
或[Authorize]
根据 Claims 判断是否显示内容。
-
模拟登录:
Fake AuthenticationStateProvider
-
覆写 Blazor 的
AuthenticationStateProvider
,内部维护一个ClaimsPrincipal
。 -
调用
NotifyAuthenticationStateChanged(...)
通知 UI 更新。 -
仅影响 Blazor 组件内的
AuthenticationStateProvider
、AuthorizeView
、AuthenticationStateProvider.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);
}
}
更多推荐
所有评论(0)