- VisualStudio2022插件的安装及使用-编程手把手系列文章
- pprof-在现网场景怎么用
- C#实现的下拉多选框,下拉多选树,多级节点
- 【学习笔记】基础数据结构:猫树
在现代Web应用程序中,身份认证与授权是确保应用安全性和用户数据保护的关键环节。Blazor作为基于C#和.NET的前端框架,提供了丰富的身份认证与授权机制。本文将深入解析Blazor的身份认证框架的构成,比较不同渲染模式下鉴权逻辑的异同,并通过具体案例演示如何在Blazor Server和Blazor WebAssembly中实现身份认证。通过本文的学习,读者将能够更好地理解并应用Blazor中的Identity,以构建安全可靠的Web应用程序.
Blazor的身份认证框架主要由以下三个核心部分组成:
AuthenticationMiddleware是ASP.NET Core中用于处理身份认证的中间件组件。它位于请求处理管道中,负责验证用户的身份并构建ClaimsPrincipal对象,将其附加到HttpContext.User属性中。所有后续的中间件和请求处理程序都可以访问该用户对象,从而了解当前请求的身份信息.
在Blazor应用程序中,AuthenticationMiddleware的作用是拦截HTTP请求,检查请求中是否包含有效的认证凭据(例如Cookie、JWT等)。如果凭据有效,它将解析并构建用户的身份信息;如果无效,则将用户视为未认证状态.
否 。
是 。
Cookie认证 。
JWT认证 。
其他方案 。
成功 。
失败 。
是 。
否 。
通过 。
不通过 。
HTTP请求 。
是否需要认证 。
直接访问资源 。
Authentication中间件 。
检查认证方案 。
解析Cookie 。
解析Authorization Header 。
... 。
验证Cookie有效性 。
验证Token签名 。
验证相应凭证 。
验证结果 。
创建ClaimsPrincipal 。
返回401/403 。
设置HttpContext.User 。
是否需要授权 。
Authorization中间件 。
访问资源 。
检查授权策略 。
访问资源 。
返回403 。
引用:
IdentityCore是ASP.NET Core提供的完整的身份管理框架。它为开发者提供了处理用户注册、登录、角色管理、密码重置等功能的 APIs 和服务。IdentityCore高度可定制,可以使用不同的数据存储方式(如Entity Framework Core、MongoDB等)和密码哈希算法.
在Blazor Server模式下,IdentityCore通常与AuthenticationMiddleware结合使用。后端服务器负责处理所有与身份相关的逻辑,包括验证用户凭据、管理用户数据和生成身份认证令牌等.
IdentityUser 。
+string Id 。
+string UserName 。
+string Email 。
+string PasswordHash 。
+string SecurityStamp 。
+bool EmailConfirmed 。
+bool TwoFactorEnabled 。
IdentityRole 。
+string Id 。
+string Name 。
+string NormalizedName 。
UserManager 。
+CreateAsync() 。
+FindByIdAsync() 。
+AddToRoleAsync() 。
+CheckPasswordAsync() 。
+GenerateEmailConfirmationTokenAsync() 。
SignInManager 。
+PasswordSignInAsync() 。
+SignInAsync() 。
+SignOutAsync() 。
+TwoFactorAuthenticatorSignInAsync() 。
RoleManager 。
+CreateAsync() 。
+FindByIdAsync() 。
+AddClaimAsync() 。
IdentityDbContext 。
+DbSet<IdentityUser> Users 。
+DbSet<IdentityRole> Roles 。
+DbSet<IdentityUserClaim> UserClaims 。
+DbSet<IdentityRoleClaim> RoleClaims 。
UserStore 。
+CreateAsync() 。
+UpdateAsync() 。
+FindByIdAsync() 。
+AddToRoleAsync() 。
RoleStore 。
+CreateAsync() 。
+UpdateAsync() 。
+FindByIdAsync() 。
IdentityOptions 。
+PasswordOptions 。
+LockoutOptions 。
+UserOptions 。
+SignInOptions 。
IdentityBuilder 。
+AddEntityFrameworkStores() 。
+AddDefaultTokenProviders() 。
+AddDefaultUI() 。
Options EF Identity Services Startup Options EF Identity Services Startup 服务配置阶段 配置密码规则 锁定策略 用户选项等 中间件配置 ConfigureServices() AddIdentity<TUser, TRole>() 注册核心服务 AddEntityFrameworkStores() Configure<IdentityOptions>() UseAuthentication() UseAuthorization() 配置认证中间件 配置授权中间件 。
Email DB Store SignInManager UserManager Controller Client Email DB Store SignInManager UserManager Controller Client 注册流程 alt [需要邮箱验证] 登录流程 alt [启用2FA] 注册请求 CreateAsync(user, password) 验证密码规则 生成SecurityStamp Hash密码 CreateAsync(user) 保存用户 GenerateEmailConfirmationTokenAsync 发送确认邮件 登录请求 PasswordSignInAsync FindByNameAsync FindByNameAsync 查询用户 CheckPasswordAsync 要求2FA验证 提供2FA码 TwoFactorSignInAsync 创建身份票据 设置认证Cookie 。
DB Store RoleManager UserManager Controller Client DB Store RoleManager UserManager Controller Client 角色管理 授权验证 alt [基于角色] [基于Claims] 创建角色请求 CreateAsync(role) CreateAsync(role) 保存角色 分配角色请求 AddToRoleAsync(user, role) AddToRoleAsync 更新用户角色关系 请求受保护资源 [Authorize(Roles = "Admin")] IsInRoleAsync IsInRoleAsync 查询角色关系 GetClaimsAsync GetClaimsAsync 查询Claims 。
«interface» 。
IUserStore 。
+CreateAsync() 。
+UpdateAsync() 。
+DeleteAsync() 。
+FindByIdAsync() 。
«interface» 。
IUserPasswordStore 。
+SetPasswordHashAsync() 。
+GetPasswordHashAsync() 。
«interface» 。
IUserRoleStore 。
+AddToRoleAsync() 。
+RemoveFromRoleAsync() 。
+GetRolesAsync() 。
«interface» 。
IUserClaimStore 。
+AddClaimsAsync() 。
+RemoveClaimsAsync() 。
+GetClaimsAsync() 。
CustomUserStore 。
-IRepository repository 。
+CreateAsync() 。
+UpdateAsync() 。
+FindByIdAsync() 。
«interface» 。
IPasswordHasher 。
+HashPassword() 。
+VerifyHashedPassword() 。
«interface» 。
IPasswordValidator 。
+ValidateAsync() 。
«interface» 。
IUserValidator 。
+ValidateAsync() 。
主要特点:
模块化设计:核心身份模型、用户管理服务、存储抽象层、验证器接口 。
灵活的扩展性:自定义用户模型、自定义存储实现、可配置的选项、验证器扩展 。
完整的认证流程:用户注册、密码验证、双因素认证、外部登录 。
丰富的授权机制:基于角色、基于Claims、基于策略、动态授权 。
安全特性:密码哈希、账户锁定、安全戳验证、令牌管理 。
引用:
AuthenticationStateProvider是Blazor中用于提供当前用户身份状态的抽象类。它的主要作用是向Blazor组件提供身份认证状态(AuthenticationState),以便组件能够根据用户的身份进行相应的显示和操作.
在Blazor应用程序中,AuthenticationStateProvider的具体实现方式取决于应用的渲染模式和身份认证方案。对于Blazor Server,这个提供程序可以直接从服务器的HttpContext.User获取身份信息;对于Blazor WebAssembly,由于代码在客户端运行,需要通过其他方式(如调用后端API或解析令牌)获取用户身份.
状态存储 Identity服务 自定义AuthProvider AuthenticationStateProvider CascadingAuthenticationState Blazor组件 状态存储 Identity服务 自定义AuthProvider AuthenticationStateProvider CascadingAuthenticationState Blazor组件 初始化阶段 alt [自定义认证逻辑] [默认实现] 状态访问 alt [通过注入访问] [通过级联参数访问] 状态变更 Server模式授权验证 alt [未授权] [已授权] 组件初始化 GetAuthenticationStateAsync() 委托处理 获取用户信息 返回用户数据 构建AuthenticationState 缓存状态 获取当前用户 返回ClaimsPrincipal new AuthenticationState() 返回AuthenticationState 通过级联参数提供状态 [CascadingParameter] Task<AuthenticationState> @inject AuthenticationStateProvider 返回Provider实例 GetAuthenticationStateAsync() await AuthenticationState 获取User信息 处理授权逻辑 用户状态变更 更新AuthenticationState 更新缓存状态 NotifyAuthenticationStateChanged 通知状态变更 触发重新渲染 @attribute [Authorize] GetAuthenticationStateAsync() 获取缓存状态 返回状态 重定向到登录页 允许访问 渲染受保护内容 。
主要特点:
状态管理:缓存认证状态、状态变更通知、状态同步更新 。
组件集成:CascadingAuthenticationState提供状态共享、AuthorizeView用于条件渲染、Authorize特性支持 。
自定义能力:自定义认证逻辑、自定义授权策略、状态持久化 。
安全特性:状态验证、角色授权、Claims基础授权 。
性能优化:状态缓存、按需刷新、组件重渲染控制 。
引用:
无论是Blazor Server还是Blazor WebAssembly,组件在需要访问用户身份信息时,都通过CascadingAuthenticationState提供的AuthenticationState。这使得组件能够以一致的方式获取用户的身份认证状态,无需关注底层的实现细节.
使用级联参数获取用户信息 。
@code {
[CascadingParameter] private Task<AuthenticationState> stateTask { get; set; }
private ClaimsPrincipal user;
protected override async Task OnInitializedAsync()
{
var authState = await stateTask;
user = authState.User;
}
}
使用服务获取用户信息 。
@inject AuthenticationStateProvider AuthenticationStateProvider
@code {
private ClaimsPrincipal user;
protected override async Task OnInitializedAsync()
{
var authState = await AuthenticationStateProvider.GetAuthenticationStateAsync();
user = authState.User;
}
}
引用:
在Blazor Server模式下,应用程序在服务器上运行,客户端通过SignalR持久连接与服务器通信。默认情况下,Blazor Server使用ServerAuthenticationStateProvider,它直接从HttpContext.User获取用户的身份信息.
为了增强安全性,Blazor Server提供了RevalidatingServerAuthenticationStateProvider。它继承自ServerAuthenticationStateProvider,能够在指定的时间间隔内重新验证用户的身份状态。这主要通过检查ClaimsPrincipal中的安全戳(Security Stamp)来实现。当用户的安全戳发生变化(如密码更改、账户被禁用等),该提供程序会检测到并更新用户的身份状态,要求用户重新登录.
HttpContext 认证状态存储 Blazor组件 Revalidating AuthProvider AuthenticationStateProvider Blazor Circuit SignalR Hub Cookie中间件 数据库 User Store UserManager SignInManager Identity服务 认证中间件 服务器 浏览器 用户 HttpContext 认证状态存储 Blazor组件 Revalidating AuthProvider AuthenticationStateProvider Blazor Circuit SignalR Hub Cookie中间件 数据库 User Store UserManager SignInManager Identity服务 认证中间件 服务器 浏览器 用户 SecurityStamp生成阶段 Guid.NewGuid().ToString() 如密码修改/角色变更等 alt [用户注册] [安全相关操作] 登录阶段 包含用户Claims和SecurityStamp HttpContext可用 new AuthenticationState(Principal) 认证状态持久化 验证当前用户状态 opt [需要重新验证] alt [首次请求或状态过期] 后台定期验证 par [获取Claims中的SecurityStamp] [获取数据库中的SecurityStamp] alt [SecurityStamp不匹配] [SecurityStamp匹配] loop [每30分钟] 注册请求 CreateAsync 生成新SecurityStamp 保存用户信息 存储SecurityStamp UpdateSecurityStampAsync 生成新SecurityStamp 更新用户信息 更新SecurityStamp 登录请求 SignInAsync 验证凭据 获取用户信息 返回用户数据 验证成功 验证成功 创建ClaimsPrincipal 创建认证Cookie 设置Cookie 首次HTTP请求(带Cookie) 返回初始HTML(App.razor) WebSocket连接请求(带Cookie) 创建Circuit 验证请求Cookie 解析认证Cookie 还原ClaimsPrincipal 返回Principal 创建AuthenticationState 初始化认证状态 存储认证状态 GetAuthenticationStateAsync() 检查状态是否需要重新验证 返回验证时间 ValidateAuthenticationStateAsync 返回AuthenticationState 用户交互 处理请求 ❌无法访问HttpContext 获取认证状态 返回认证状态 ValidateAuthenticationStateAsync 从Claims获取SecurityStamp GetSecurityStampAsync 查询用户 获取SecurityStamp 返回当前SecurityStamp 返回结果 返回SecurityStamp 更新认证状态 NotifyAuthenticationStateChanged 更新UI状态 继续使用当前认证状态 。
引用:
在Blazor WebAssembly模式下,应用程序在客户端浏览器中运行,没有直接访问服务器HttpContext的能力。因此,获取用户身份信息需要通过其他方式。例如,实现自定义的AuthenticationStateProvider,通过调用后端API(如用户信息终结点)获取用户信息,或者解析存储在客户端的JWT令牌来构建ClaimsPrincipal.
此外,为了在客户端向受保护的API发送请求时自动附加身份认证令牌,或是在令牌过期后自动刷新令牌,可以配置HttpClient使用自定义的HttpMessageHandler。这样,可以在请求头中添加必要的身份认证信息(如Bearer Token)或拦截401响应并刷新令牌重试请求.
Token服务 后端API HttpClientHandler AuthenticationProvider Blazor组件 Blazor WebAssembly应用 浏览器 用户 Token服务 后端API HttpClientHandler AuthenticationProvider Blazor组件 Blazor WebAssembly应用 浏览器 用户 alt [Token存在且未过- 期] [Token不存在或已- 过期] alt [验证成功] [验证失败] alt [刷新成功] [刷新失败] alt [Token有效] [Token无效或过期(返回401)] alt [刷新成功] [刷新失败] alt [Token即将过期] [Token仍然有效] loop [每固定时间间隔] 打开应用URL 加载Blazor WebAssembly应用 初始化认证状态 检查本地存储的Token 使用现有Token 设置为未认证状态 返回认证状态 渲染组件 点击登录按钮 调用Login方法 发送登录请求(带用户凭据) 验证用户凭据 返回访问Token和刷新Token 存储Token(如localStorage) 更新认证状态为已认证 通知认证状态已改变 重新渲染受限页面 返回错误信息 通知登录失败 GetAuthenticationStateAsync() 使用Token请求用户信息(/userinfo) 添加Authorization头 返回用户信息 返回AuthenticationState(包含用户信息) 发起HTTP请求(携带API地址) 拦截请求,添加Authorization头 发送请求(带Token) 返回请求数据 返回数据 返回401 Unauthorized 通知Token无效 尝试刷新Token 发送刷新Token请求 验证刷新Token 返回新的访问Token和刷新Token 返回新的Token 更新存储的Token 通知重试原请求 重新发送原请求(带新Token) 返回请求数据 返回数据 返回刷新失败信息 通知刷新失败 清除Token,更新为未认证状态 通知认证状态已改变(未认证) 重定向到登录页或显示登录按钮 检查Token有效期 刷新Token 验证刷新Token 返回新的Token 更新Token 更新存储的Token 返回刷新失败信息 通知刷新失败 清除Token,更新为未认证状态 通知认证状态已改变(未认证) 重定向到登录页或显示登录按钮 无需操作 。
引用:
下面,我们将通过具体的案例,演示如何在Blazor Server和Blazor WebAssembly应用程序中实现身份认证.
注册服务 。
builder.Services.AddCascadingAuthenticationState(); // 添加级联参数获取认证信息
builder.Services.AddScoped<AuthenticationStateProvider, IdentityRevalidatingAuthenticationStateProvider>(); // 注册自实现的StateProvider
builder.Services.AddAuthentication(options =>
{
//在这里设定默认方案
options.DefaultScheme = IdentityConstants.ApplicationScheme;
options.DefaultSignInScheme = IdentityConstants.ExternalScheme;
options.DefaultSignOutScheme = IdentityConstants.ExternalScheme;
}).AddIdentityCookies(options =>
{
});
builder.Services.AddIdentityCore<ApplicationUser>(options => //ApplicationUser继承自IdentityUser
{
//在这里设定鉴权配置,比如验证邮箱(这里不验证)、密码规则(这里设置最简规则)
options.SignIn.RequireConfirmedAccount = false;
options.Password.RequiredLength = 6;
options.Password.RequireDigit = false;
options.Password.RequireLowercase = false;
options.Password.RequireNonAlphanumeric = false;
options.Password.RequireUppercase = false;
})
.AddRoles<IdentityRole>() //需要自定义角色时,继承IdentityRole
.AddEntityFrameworkStores<ApplicationDbContext>() //ApplicationDbContext需要继承IdentityDbContext,其他ORM请自行搜索Store实现
.AddSignInManager() //使用Cookie时,推荐注册
.AddDefaultTokenProviders();
builder.Services.AddSingleton<IEmailSender<ApplicationUser>, IdentityNoOpEmailSender>();
var app = builder.Build();
app.UseAuthentication();
app.UseAuthorization();
其中,AddIdentityCookies方法的源代码如下:
/// <summary>
/// Adds the cookie authentication needed for sign in manager.
/// </summary>
/// <param name="builder">The current <see cref="AuthenticationBuilder"/> instance.</param>
/// <param name="configureCookies">Action used to configure the cookies.</param>
/// <returns>The <see cref="IdentityCookiesBuilder"/> which can be used to configure the identity cookies.</returns>
public static IdentityCookiesBuilder AddIdentityCookies(this AuthenticationBuilder builder, Action<IdentityCookiesBuilder> configureCookies)
{
var cookieBuilder = new IdentityCookiesBuilder();
cookieBuilder.ApplicationCookie = builder.AddApplicationCookie();
cookieBuilder.ExternalCookie = builder.AddExternalCookie();
cookieBuilder.TwoFactorRememberMeCookie = builder.AddTwoFactorRememberMeCookie();
cookieBuilder.TwoFactorUserIdCookie = builder.AddTwoFactorUserIdCookie();
configureCookies?.Invoke(cookieBuilder);
return cookieBuilder;
}
IdentityNoOpEmailSender 。
public sealed class IdentityNoOpEmailSender : IEmailSender<ApplicationUser>
{
private readonly IEmailSender emailSender = new NoOpEmailSender(); //案例不实现真正的邮件发送
public Task SendConfirmationLinkAsync(ApplicationUser user, string email, string confirmationLink) =>
emailSender.SendEmailAsync(email, "Confirm your email", $"Please confirm your account by <a href='{confirmationLink}'>clicking here</a>.");
public Task SendPasswordResetLinkAsync(ApplicationUser user, string email, string resetLink) =>
emailSender.SendEmailAsync(email, "Reset your password", $"Please reset your password by <a href='{resetLink}'>clicking here</a>.");
public Task SendPasswordResetCodeAsync(ApplicationUser user, string email, string resetCode) =>
emailSender.SendEmailAsync(email, "Reset your password", $"Please reset your password using the following code: {resetCode}");
}
IdentityRevalidatingAuthenticationStateProvider 。
public sealed class IdentityRevalidatingAuthenticationStateProvider(
ILoggerFactory loggerFactory,
IServiceScopeFactory scopeFactory,
IOptions<IdentityOptions> options)
: RevalidatingServerAuthenticationStateProvider(loggerFactory)
{
protected override TimeSpan RevalidationInterval => TimeSpan.FromMinutes(30);
protected override async Task<bool> ValidateAuthenticationStateAsync(
AuthenticationState authenticationState, CancellationToken cancellationToken)
{
// Get the user manager from a new scope to ensure it fetches fresh data
await using var scope = scopeFactory.CreateAsyncScope();
var userManager = scope.ServiceProvider.GetRequiredService<UserManager<ApplicationUser>>();
return await ValidateSecurityStampAsync(userManager, authenticationState.User);
}
private async Task<bool> ValidateSecurityStampAsync(UserManager<ApplicationUser> userManager, ClaimsPrincipal principal)
{
var user = await userManager.GetUserAsync(principal);
if (user is null)
{
return false;
}
else if (!userManager.SupportsUserSecurityStamp)
{
return true;
}
else
{
var principalStamp = principal.FindFirstValue(options.Value.ClaimsIdentity.SecurityStampClaimType);
var userStamp = await userManager.GetSecurityStampAsync(user);
return principalStamp == userStamp;
}
}
}
登录时,可以使用表单提交,也可以使用Ajax POST(制作动态网页时的首选),后端处理的逻辑代码相同,即都通过SignInManager实现发送Set-Cookie请求 。
表单处理:
public async Task LoginUser()
{
// This doesn't count login failures towards account lockout
// To enable password failures to trigger account lockout, set lockoutOnFailure: true
var result = await SignInManager.PasswordSignInAsync(Input.Email, Input.Password, Input.RememberMe, lockoutOnFailure: false);
if (result.Succeeded)
{
RedirectManager.RedirectTo("/");
}
else if (result.RequiresTwoFactor)
{
RedirectManager.RedirectTo(
"Account/LoginWith2fa",
new() { ["returnUrl"] = ReturnUrl, ["rememberMe"] = Input.RememberMe });
}
else if (result.IsLockedOut)
{
RedirectManager.RedirectTo("Account/Lockout");
}
else
{
errorMessage = $@"账号或密码错误";
}
}
WebAPI:
app.MapPost("Login", async (CheckDto Input,
UserManager<ApplicationUser> userManager,
SignInManager<ApplicationUser> signInManager) =>
{
var emailResult = await userManager.FindByEmailAsync(dto.username);
if (emailResult is null) return Results.BadRequest("账号未注册");
if (await userManager.CheckPasswordAsync(emailResult, dto.password))
{
await signInManager.SignInAsync(emailResult, false);
return Results.Ok();
}
return Results.BadRequest("密码错误"); //401不能返回错误信息,故使用400
});
此方式是微软推荐的鉴权方式,相比于JWT安全性较高 。
注册服务(使用.NET 8以后新增的方法) 。
builder.Services.AddAuthorization();
builder.Services.AddIdentityApiEndpoints<ApplicationUser>()
.AddEntityFrameworkStores<ApplicationDbContext>();
var app = builder.Build();
app.UseAuthentication();
app.UseAuthorization();
app.MapIdentityApi<ApplicationUser>();
源码解读:
/// <summary>
/// Adds a set of common identity services to the application to support <see cref="IdentityApiEndpointRouteBuilderExtensions.MapIdentityApi{TUser}(IEndpointRouteBuilder)"/>
/// and configures authentication to support identity bearer tokens and cookies.
/// </summary>
/// <param name="services">The <see cref="IServiceCollection"/>.</param>
/// <param name="configure">Configures the <see cref="IdentityOptions"/>.</param>
/// <returns>The <see cref="IdentityBuilder"/>.</returns>
public static IdentityBuilder AddIdentityApiEndpoints<TUser>(this IServiceCollection services, Action<IdentityOptions> configure)
where TUser : class, new()
{
ArgumentNullException.ThrowIfNull(services);
ArgumentNullException.ThrowIfNull(configure);
services
.AddAuthentication(IdentityConstants.BearerAndApplicationScheme)
.AddScheme<AuthenticationSchemeOptions, CompositeIdentityHandler>(IdentityConstants.BearerAndApplicationScheme, null, compositeOptions =>
{
compositeOptions.ForwardDefault = IdentityConstants.BearerScheme;
compositeOptions.ForwardAuthenticate = IdentityConstants.BearerAndApplicationScheme;
})
.AddBearerToken(IdentityConstants.BearerScheme)
.AddIdentityCookies();
return services.AddIdentityCore<TUser>(configure)
.AddApiEndpoints();
}
/// <summary>
/// Add endpoints for registering, logging in, and logging out using ASP.NET Core Identity.
/// </summary>
/// <typeparam name="TUser">The type describing the user. This should match the generic parameter in <see cref="UserManager{TUser}"/>.</typeparam>
/// <param name="endpoints">
/// The <see cref="IEndpointRouteBuilder"/> to add the identity endpoints to.
/// Call <see cref="EndpointRouteBuilderExtensions.MapGroup(IEndpointRouteBuilder, string)"/> to add a prefix to all the endpoints.
/// </param>
/// <returns>An <see cref="IEndpointConventionBuilder"/> to further customize the added endpoints.</returns>
public static IEndpointConventionBuilder MapIdentityApi<TUser>(this IEndpointRouteBuilder endpoints)
where TUser : class, new()
{
ArgumentNullException.ThrowIfNull(endpoints);
var timeProvider = endpoints.ServiceProvider.GetRequiredService<TimeProvider>();
var bearerTokenOptions = endpoints.ServiceProvider.GetRequiredService<IOptionsMonitor<BearerTokenOptions>>();
var emailSender = endpoints.ServiceProvider.GetRequiredService<IEmailSender<TUser>>();
var linkGenerator = endpoints.ServiceProvider.GetRequiredService<LinkGenerator>();
// We'll figure out a unique endpoint name based on the final route pattern during endpoint generation.
string? confirmEmailEndpointName = null;
var routeGroup = endpoints.MapGroup("");
// NOTE: We cannot inject UserManager<TUser> directly because the TUser generic parameter is currently unsupported by RDG.
// https://github.com/dotnet/aspnetcore/issues/47338
routeGroup.MapPost("/register", async Task<Results<Ok, ValidationProblem>>
([FromBody] RegisterRequest registration, HttpContext context, [FromServices] IServiceProvider sp) =>
{
var userManager = sp.GetRequiredService<UserManager<TUser>>();
if (!userManager.SupportsUserEmail)
{
throw new NotSupportedException($"{nameof(MapIdentityApi)} requires a user store with email support.");
}
var userStore = sp.GetRequiredService<IUserStore<TUser>>();
var emailStore = (IUserEmailStore<TUser>)userStore;
var email = registration.Email;
if (string.IsNullOrEmpty(email) || !_emailAddressAttribute.IsValid(email))
{
return CreateValidationProblem(IdentityResult.Failed(userManager.ErrorDescriber.InvalidEmail(email)));
}
var user = new TUser();
await userStore.SetUserNameAsync(user, email, CancellationToken.None);
await emailStore.SetEmailAsync(user, email, CancellationToken.None);
var result = await userManager.CreateAsync(user, registration.Password);
if (!result.Succeeded)
{
return CreateValidationProblem(result);
}
await SendConfirmationEmailAsync(user, userManager, context, email);
return TypedResults.Ok();
});
routeGroup.MapPost("/login", async Task<Results<Ok<AccessTokenResponse>, EmptyHttpResult, ProblemHttpResult>>
([FromBody] LoginRequest login, [FromQuery] bool? useCookies, [FromQuery] bool? useSessionCookies, [FromServices] IServiceProvider sp) =>
{
var signInManager = sp.GetRequiredService<SignInManager<TUser>>();
var useCookieScheme = (useCookies == true) || (useSessionCookies == true);
var isPersistent = (useCookies == true) && (useSessionCookies != true);
signInManager.AuthenticationScheme = useCookieScheme ? IdentityConstants.ApplicationScheme : IdentityConstants.BearerScheme;
var result = await signInManager.PasswordSignInAsync(login.Email, login.Password, isPersistent, lockoutOnFailure: true);
if (result.RequiresTwoFactor)
{
if (!string.IsNullOrEmpty(login.TwoFactorCode))
{
result = await signInManager.TwoFactorAuthenticatorSignInAsync(login.TwoFactorCode, isPersistent, rememberClient: isPersistent);
}
else if (!string.IsNullOrEmpty(login.TwoFactorRecoveryCode))
{
result = await signInManager.TwoFactorRecoveryCodeSignInAsync(login.TwoFactorRecoveryCode);
}
}
if (!result.Succeeded)
{
return TypedResults.Problem(result.ToString(), statusCode: StatusCodes.Status401Unauthorized);
}
// The signInManager already produced the needed response in the form of a cookie or bearer token.
return TypedResults.Empty;
});
routeGroup.MapPost("/refresh", async Task<Results<Ok<AccessTokenResponse>, UnauthorizedHttpResult, SignInHttpResult, ChallengeHttpResult>>
([FromBody] RefreshRequest refreshRequest, [FromServices] IServiceProvider sp) =>
{
var signInManager = sp.GetRequiredService<SignInManager<TUser>>();
var refreshTokenProtector = bearerTokenOptions.Get(IdentityConstants.BearerScheme).RefreshTokenProtector;
var refreshTicket = refreshTokenProtector.Unprotect(refreshRequest.RefreshToken);
// Reject the /refresh attempt with a 401 if the token expired or the security stamp validation fails
if (refreshTicket?.Properties?.ExpiresUtc is not { } expiresUtc ||
timeProvider.GetUtcNow() >= expiresUtc ||
await signInManager.ValidateSecurityStampAsync(refreshTicket.Principal) is not TUser user)
{
return TypedResults.Challenge();
}
var newPrincipal = await signInManager.CreateUserPrincipalAsync(user);
return TypedResults.SignIn(newPrincipal, authenticationScheme: IdentityConstants.BearerScheme);
});
routeGroup.MapGet("/confirmEmail", async Task<Results<ContentHttpResult, UnauthorizedHttpResult>>
([FromQuery] string userId, [FromQuery] string code, [FromQuery] string? changedEmail, [FromServices] IServiceProvider sp) =>
{
var userManager = sp.GetRequiredService<UserManager<TUser>>();
if (await userManager.FindByIdAsync(userId) is not { } user)
{
// We could respond with a 404 instead of a 401 like Identity UI, but that feels like unnecessary information.
return TypedResults.Unauthorized();
}
try
{
code = Encoding.UTF8.GetString(WebEncoders.Base64UrlDecode(code));
}
catch (FormatException)
{
return TypedResults.Unauthorized();
}
IdentityResult result;
if (string.IsNullOrEmpty(changedEmail))
{
result = await userManager.ConfirmEmailAsync(user, code);
}
else
{
// As with Identity UI, email and user name are one and the same. So when we update the email,
// we need to update the user name.
result = await userManager.ChangeEmailAsync(user, changedEmail, code);
if (result.Succeeded)
{
result = await userManager.SetUserNameAsync(user, changedEmail);
}
}
if (!result.Succeeded)
{
return TypedResults.Unauthorized();
}
return TypedResults.Text("Thank you for confirming your email.");
})
.Add(endpointBuilder =>
{
var finalPattern = ((RouteEndpointBuilder)endpointBuilder).RoutePattern.RawText;
confirmEmailEndpointName = $"{nameof(MapIdentityApi)}-{finalPattern}";
endpointBuilder.Metadata.Add(new EndpointNameMetadata(confirmEmailEndpointName));
});
routeGroup.MapPost("/resendConfirmationEmail", async Task<Ok>
([FromBody] ResendConfirmationEmailRequest resendRequest, HttpContext context, [FromServices] IServiceProvider sp) =>
{
var userManager = sp.GetRequiredService<UserManager<TUser>>();
if (await userManager.FindByEmailAsync(resendRequest.Email) is not { } user)
{
return TypedResults.Ok();
}
await SendConfirmationEmailAsync(user, userManager, context, resendRequest.Email);
return TypedResults.Ok();
});
routeGroup.MapPost("/forgotPassword", async Task<Results<Ok, ValidationProblem>>
([FromBody] ForgotPasswordRequest resetRequest, [FromServices] IServiceProvider sp) =>
{
var userManager = sp.GetRequiredService<UserManager<TUser>>();
var user = await userManager.FindByEmailAsync(resetRequest.Email);
if (user is not null && await userManager.IsEmailConfirmedAsync(user))
{
var code = await userManager.GeneratePasswordResetTokenAsync(user);
code = WebEncoders.Base64UrlEncode(Encoding.UTF8.GetBytes(code));
await emailSender.SendPasswordResetCodeAsync(user, resetRequest.Email, HtmlEncoder.Default.Encode(code));
}
// Don't reveal that the user does not exist or is not confirmed, so don't return a 200 if we would have
// returned a 400 for an invalid code given a valid user email.
return TypedResults.Ok();
});
routeGroup.MapPost("/resetPassword", async Task<Results<Ok, ValidationProblem>>
([FromBody] ResetPasswordRequest resetRequest, [FromServices] IServiceProvider sp) =>
{
var userManager = sp.GetRequiredService<UserManager<TUser>>();
var user = await userManager.FindByEmailAsync(resetRequest.Email);
if (user is null || !(await userManager.IsEmailConfirmedAsync(user)))
{
// Don't reveal that the user does not exist or is not confirmed, so don't return a 200 if we would have
// returned a 400 for an invalid code given a valid user email.
return CreateValidationProblem(IdentityResult.Failed(userManager.ErrorDescriber.InvalidToken()));
}
IdentityResult result;
try
{
var code = Encoding.UTF8.GetString(WebEncoders.Base64UrlDecode(resetRequest.ResetCode));
result = await userManager.ResetPasswordAsync(user, code, resetRequest.NewPassword);
}
catch (FormatException)
{
result = IdentityResult.Failed(userManager.ErrorDescriber.InvalidToken());
}
if (!result.Succeeded)
{
return CreateValidationProblem(result);
}
return TypedResults.Ok();
});
var accountGroup = routeGroup.MapGroup("/manage").RequireAuthorization();
accountGroup.MapPost("/2fa", async Task<Results<Ok<TwoFactorResponse>, ValidationProblem, NotFound>>
(ClaimsPrincipal claimsPrincipal, [FromBody] TwoFactorRequest tfaRequest, [FromServices] IServiceProvider sp) =>
{
var signInManager = sp.GetRequiredService<SignInManager<TUser>>();
var userManager = signInManager.UserManager;
if (await userManager.GetUserAsync(claimsPrincipal) is not { } user)
{
return TypedResults.NotFound();
}
if (tfaRequest.Enable == true)
{
if (tfaRequest.ResetSharedKey)
{
return CreateValidationProblem("CannotResetSharedKeyAndEnable",
"Resetting the 2fa shared key must disable 2fa until a 2fa token based on the new shared key is validated.");
}
if (string.IsNullOrEmpty(tfaRequest.TwoFactorCode))
{
return CreateValidationProblem("RequiresTwoFactor",
"No 2fa token was provided by the request. A valid 2fa token is required to enable 2fa.");
}
if (!await userManager.VerifyTwoFactorTokenAsync(user, userManager.Options.Tokens.AuthenticatorTokenProvider, tfaRequest.TwoFactorCode))
{
return CreateValidationProblem("InvalidTwoFactorCode",
"The 2fa token provided by the request was invalid. A valid 2fa token is required to enable 2fa.");
}
await userManager.SetTwoFactorEnabledAsync(user, true);
}
else if (tfaRequest.Enable == false || tfaRequest.ResetSharedKey)
{
await userManager.SetTwoFactorEnabledAsync(user, false);
}
if (tfaRequest.ResetSharedKey)
{
await userManager.ResetAuthenticatorKeyAsync(user);
}
string[]? recoveryCodes = null;
if (tfaRequest.ResetRecoveryCodes || (tfaRequest.Enable == true && await userManager.CountRecoveryCodesAsync(user) == 0))
{
var recoveryCodesEnumerable = await userManager.GenerateNewTwoFactorRecoveryCodesAsync(user, 10);
recoveryCodes = recoveryCodesEnumerable?.ToArray();
}
if (tfaRequest.ForgetMachine)
{
await signInManager.ForgetTwoFactorClientAsync();
}
var key = await userManager.GetAuthenticatorKeyAsync(user);
if (string.IsNullOrEmpty(key))
{
await userManager.ResetAuthenticatorKeyAsync(user);
key = await userManager.GetAuthenticatorKeyAsync(user);
if (string.IsNullOrEmpty(key))
{
throw new NotSupportedException("The user manager must produce an authenticator key after reset.");
}
}
return TypedResults.Ok(new TwoFactorResponse
{
SharedKey = key,
RecoveryCodes = recoveryCodes,
RecoveryCodesLeft = recoveryCodes?.Length ?? await userManager.CountRecoveryCodesAsync(user),
IsTwoFactorEnabled = await userManager.GetTwoFactorEnabledAsync(user),
IsMachineRemembered = await signInManager.IsTwoFactorClientRememberedAsync(user),
});
});
accountGroup.MapGet("/info", async Task<Results<Ok<InfoResponse>, ValidationProblem, NotFound>>
(ClaimsPrincipal claimsPrincipal, [FromServices] IServiceProvider sp) =>
{
var userManager = sp.GetRequiredService<UserManager<TUser>>();
if (await userManager.GetUserAsync(claimsPrincipal) is not { } user)
{
return TypedResults.NotFound();
}
return TypedResults.Ok(await CreateInfoResponseAsync(user, userManager));
});
accountGroup.MapPost("/info", async Task<Results<Ok<InfoResponse>, ValidationProblem, NotFound>>
(ClaimsPrincipal claimsPrincipal, [FromBody] InfoRequest infoRequest, HttpContext context, [FromServices] IServiceProvider sp) =>
{
var userManager = sp.GetRequiredService<UserManager<TUser>>();
if (await userManager.GetUserAsync(claimsPrincipal) is not { } user)
{
return TypedResults.NotFound();
}
if (!string.IsNullOrEmpty(infoRequest.NewEmail) && !_emailAddressAttribute.IsValid(infoRequest.NewEmail))
{
return CreateValidationProblem(IdentityResult.Failed(userManager.ErrorDescriber.InvalidEmail(infoRequest.NewEmail)));
}
if (!string.IsNullOrEmpty(infoRequest.NewPassword))
{
if (string.IsNullOrEmpty(infoRequest.OldPassword))
{
return CreateValidationProblem("OldPasswordRequired",
"The old password is required to set a new password. If the old password is forgotten, use /resetPassword.");
}
var changePasswordResult = await userManager.ChangePasswordAsync(user, infoRequest.OldPassword, infoRequest.NewPassword);
if (!changePasswordResult.Succeeded)
{
return CreateValidationProblem(changePasswordResult);
}
}
if (!string.IsNullOrEmpty(infoRequest.NewEmail))
{
var email = await userManager.GetEmailAsync(user);
if (email != infoRequest.NewEmail)
{
await SendConfirmationEmailAsync(user, userManager, context, infoRequest.NewEmail, isChange: true);
}
}
return TypedResults.Ok(await CreateInfoResponseAsync(user, userManager));
});
}
可以看到,基架生成的info接口返回的用户信息非常有限,实际业务中需要自行写一个返回角色/Claims信息的userinfo接口(注入UserManager和RoleManager查询) 根据基架生成的API,编写前端代码:
注册服务:
// 注册LocalStorage服务
builder.Services.AddBlazoredLocalStorageAsSingleton();
// 注册鉴权服务
builder.Services.AddSingleton<AuthenticationStateProvider, CookieAuthenticationStateProvider>();
// 注册通用客户端的鉴权拦截器(令牌过期重试)
builder.Services.AddSingleton<AuthenticationStateHandler>();
// 注册鉴权专用客户端
builder.Services.AddHttpClient("auth", client =>
{
client.BaseAddress = new Uri("API_URL");
});
// 注册业务通用客户端
builder.Services.AddHttpClient("backend", client =>
{
client.BaseAddress = new Uri("API_URL");
}).AddHttpMessageHandler<AuthenticationStateHandler>();
// 设为默认客户端
builder.Services.AddSingleton(sp => sp.GetRequiredService<IHttpClientFactory>().CreateClient("backend"));
// 注册鉴权基架
builder.Services.AddAuthorizationCore();
builder.Services.AddCascadingAuthenticationState();
AuthenticationStateHandler
public class AuthenticationStateHandler(AuthenticationStateProvider stateProvider, NavigationManager navigationManager) : DelegatingHandler
{
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
var response = await base.SendAsync(request, cancellationToken);
//如果令牌过期,刷新令牌并重试请求
if (response.StatusCode == System.Net.HttpStatusCode.Unauthorized)
{
var authState = await stateProvider.GetAuthenticationStateAsync();
if (authState.User.Identity?.IsAuthenticated ?? false)
{
if (await (stateProvider as CookieAuthenticationStateProvider).RefreshTokenAsync())
{
return await SendAsync(request, cancellationToken);
}
}
navigationManager.NavigateTo("/login");
}
return response;
}
}
CookieAuthenticationStateProvider
public sealed class CookieAuthenticationStateProvider(IServiceProvider serviceProvider, IHttpClientFactory httpClientFactory, ILocalStorageService localStorage, ISyncLocalStorageService syncLocalStorage) : AuthenticationStateProvider
{
//token过期时间
private static TimeSpan UserCacheRefreshInterval = TimeSpan.FromHours(1);
//上次获取token时间
private static DateTimeOffset UserLastCheckTime = DateTimeOffset.FromUnixTimeSeconds(0);
//缓存用户状态
private ClaimsPrincipal CachedUser = new(new ClaimsIdentity());
//默认用户状态(未登录)
private static readonly Task<AuthenticationState> defaultUnanthenticatedTask = Task.FromResult(new AuthenticationState(new ClaimsPrincipal(new ClaimsIdentity())));
//刷新令牌
private string? refresh_token { get; set; } = syncLocalStorage.GetItem<string>("refresh_token");
//访问令牌
private string? access_token { get; set; } = syncLocalStorage.GetItem<string>("access_token");
//鉴权专用客户端示例
private HttpClient client = httpClientFactory.CreateClient("auth");
//解析令牌获取身份信息
private async Task ParseTokenAsync()
{
var response = await client.GetAsync("/info"); //推荐自己写userinfo接口代替
if (response.IsSuccessStatusCode)
{
var infoResponse = await response.Content.ReadFromJsonAsync<InfoResponse>();
if (infoResponse != null)
{
var claims = new List<Claim>
{
new Claim(ClaimTypes.Email, infoResponse.Email),
new Claim("IsEmailConfirmed", infoResponse.IsEmailConfirmed.ToString())
};
CachedUser = new ClaimsPrincipal(new ClaimsIdentity(claims, "Bearer"));
}
}
else
{
CachedUser = new ClaimsPrincipal(new ClaimsIdentity());
}
}
//设置客户端携带令牌
private void SetClientToken()
{
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", access_token);
serviceProvider.GetRequiredService<HttpClient>().DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", access_token);
}
//处理令牌接口返回值
private async Task<bool> ParseResponseAsync(AccessTokenResponse token)
{
if (token != null)
{
refresh_token = token.RefreshToken;
access_token = token.AccessToken;
await localStorage.SetItemAsync("access_token", access_token);
await localStorage.SetItemAsync("refresh_token", refresh_token);
UserCacheRefreshInterval = TimeSpan.FromSeconds(token.ExpiresIn);
UserLastCheckTime = DateTimeOffset.UtcNow;
SetClientToken();
await ParseTokenAsync();
NotifyAuthenticationStateChanged(GetAuthenticationStateAsync());
return true;
}
return false;
}
//注册
public async Task<RegisterResponse> RegisterAsync(RegisterModel model)
{
var response = await client.PostAsJsonAsync(@"register", model);
var reg = await response.Content.ReadFromJsonAsync<RegisterResponse>();
if (reg.Succeeded) await ParseResponseAsync(reg.TokenResponse);
return reg;
}
//登录
public async Task<bool> LoginAsync(string username, string password)
{
var response = await client.PostAsJsonAsync(@"login", new LoginRequest { Username = username, Password = password } );
if (response.StatusCode == HttpStatusCode.Unauthorized)
{
return false;
}
var token = await response.Content.ReadFromJsonAsync<AccessTokenResponse>();
return await ParseResponseAsync(token);
}
//登出
public async Task LogoutAsync()
{
await client.PostAsync(@"logout",new StringContent(string.Empty));
refresh_token = null;
access_token = null;
await localStorage.RemoveItemAsync("access_token");
await localStorage.RemoveItemAsync("refresh_token");
UserLastCheckTime = DateTimeOffset.FromUnixTimeSeconds(0);
CachedUser = new ClaimsPrincipal(new ClaimsIdentity());
NotifyAuthenticationStateChanged(defaultUnanthenticatedTask);
}
//刷新令牌
public async Task<bool> RefreshTokenAsync()
{
if (refresh_token is null) return false;
SetClientToken();
var response = await client.PostAsJsonAsync(@"refresh", new { RefreshToken = refresh_token });
if (response.StatusCode == HttpStatusCode.Unauthorized)
{
return false;
}
var token = await response.Content.ReadFromJsonAsync<AccessTokenResponse>();
return await ParseResponseAsync(token);
}
//获取用户鉴权信息
public override async Task<AuthenticationState> GetAuthenticationStateAsync()
{
if (DateTimeOffset.UtcNow - UserLastCheckTime < UserCacheRefreshInterval || await RefreshTokenAsync())
{
return new AuthenticationState(CachedUser);
}
return await defaultUnanthenticatedTask;
}
}
此方法安全性较低,不推荐 。
注册服务 。
// JWT 配置
var jwtSettings = builder.Configuration.GetSection("JwtSettings");
var secretKey = Encoding.UTF8.GetBytes(jwtSettings["SecretKey"]!);
// 添加认证服务
builder.Services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(options =>
{
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidateAudience = true,
ValidateLifetime = true,
ValidateIssuerSigningKey = true,
ValidIssuer = jwtSettings["Issuer"],
ValidAudience = jwtSettings["Audience"],
IssuerSigningKey = new SymmetricSecurityKey(secretKey)
};
});
// 添加授权服务
builder.Services.AddAuthorization();
// 添加 JWT 服务
builder.Services.AddScoped<JwtService>();
// 添加 Identity 服务(如果需要)
builder.Services.AddIdentityCore<ApplicationUser>(options =>
{
options.Password.RequireDigit = false;
options.Password.RequireLowercase = false;
options.Password.RequireNonAlphanumeric = false;
options.Password.RequireUppercase = false;
options.Password.RequiredLength = 6;
options.SignIn.RequireConfirmedEmail = false;
}).AddRoles<IdentityRole<Guid>>()
.AddEntityFrameworkStores<ApplicationDbContext>()
.AddDefaultTokenProviders()
.AddRoleManager<RoleManager<IdentityRole<Guid>>>()
.AddUserManager<UserManager<ApplicationUser>>();
var app = builder.Build();
app.UseAuthentication();
app.UseAuthorization();
appsettings.json 。
"JwtSettings": {
"SecretKey": "your-very-looong-secret-key-here",
"Issuer": "your-issuer",
"Audience": "your-audience"
}
JwtService 。
public class JwtService(IConfiguration configuration)
{
public AccessTokenResponse GenerateTokens(ApplicationUser user, IList<string> roles)
{
// 生成访问令牌
var accessToken = GenerateAccessToken(user, roles);
// 生成刷新令牌
var refreshToken = GenerateRefreshToken();
// 计算过期时间(以秒为单位)
var expiresIn = Convert.ToInt32(TimeSpan.FromHours(1).TotalSeconds);
return new AccessTokenResponse(accessToken, expiresIn, refreshToken);
}
private string GenerateAccessToken(ApplicationUser user, IList<string> roles)
{
var secretKey = Encoding.UTF8.GetBytes(configuration["JwtSettings:SecretKey"]!);
var signingCredentials = new SigningCredentials(
new SymmetricSecurityKey(secretKey),
SecurityAlgorithms.HmacSha256Signature
);
var claims = new List<Claim>
{
new(ClaimTypes.NameIdentifier, user.Id.ToString()),
new(ClaimTypes.Name, user.UserName!)
};
claims.AddRange(roles.Select(role => new Claim(ClaimTypes.Role, role)));
var token = new JwtSecurityToken(
issuer: configuration["JwtSettings:Issuer"],
audience: configuration["JwtSettings:Audience"],
claims: claims,
expires: DateTime.UtcNow.AddHours(1),
signingCredentials: signingCredentials
);
return new JwtSecurityTokenHandler().WriteToken(token);
}
private string GenerateRefreshToken()
{
var randomNumber = new byte[64];
using var rng = RandomNumberGenerator.Create();
rng.GetBytes(randomNumber);
return Convert.ToBase64String(randomNumber);
}
public ClaimsPrincipal? GetPrincipalFromExpiredToken(string token)
{
var secretKey = configuration["JwtSettings:SecretKey"]!;
var tokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidateAudience = true,
ValidateLifetime = false, // 不验证过期时间
ValidateIssuerSigningKey = true,
ValidIssuer = configuration["JwtSettings:Issuer"],
ValidAudience = configuration["JwtSettings:Audience"],
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(secretKey))
};
var tokenHandler = new JwtSecurityTokenHandler();
var principal = tokenHandler.ValidateToken(token, tokenValidationParameters, out SecurityToken securityToken);
if (securityToken is not JwtSecurityToken jwtSecurityToken ||
!jwtSecurityToken.Header.Alg.Equals(SecurityAlgorithms.HmacSha256Signature,
StringComparison.InvariantCultureIgnoreCase))
{
return null;
}
return principal;
}
}
IdentityEndpoints 。
public static void MapIdentityEndpoints(this IEndpointRouteBuilder routes)
{
var group = routes.MapGroup("/Account").WithTags("Account");
group.MapPost("/Login", async (
LoginRequest model,
UserManager<ApplicationUser> userManager,
JwtService jwtService) =>
{
var user = await userManager.FindByNameAsync(model.Username);
if (user == null)
{
return Results.Unauthorized();
}
var isPasswordValid = await userManager.CheckPasswordAsync(user, model.Password);
if (!isPasswordValid)
{
return Results.Unauthorized();
}
var roles = await userManager.GetRolesAsync(user);
var tokenResponse = jwtService.GenerateTokens(user, roles);
// 保存刷新令牌到用户记录
user.RefreshToken = tokenResponse.RefreshToken;
user.RefreshTokenExpiryTime = DateTime.UtcNow.AddDays(7); // 刷新令牌7天有效
await userManager.UpdateAsync(user);
return Results.Ok(tokenResponse);
})
.AllowAnonymous()
.WithName("Login")
.WithOpenApi();
group.MapGet("/User", (HttpContext context) =>
{
var user = context.User;
return Results.Ok(new
{
Username = user.Identity?.Name,
UserId = user.FindFirst(ClaimTypes.NameIdentifier)?.Value,
Roles = user.Claims
.Where(c => c.Type == ClaimTypes.Role)
.Select(c => c.Value)
.ToList()
});
})
.RequireAuthorization()
.WithName("User")
.WithOpenApi();
group.MapPost("/Refresh", async (
RefreshTokenModel model,
UserManager<ApplicationUser> userManager,
JwtService jwtService,
HttpContext context) =>
{
// 从请求头获取过期的访问令牌
string? accessToken = context.Request.Headers["Authorization"]
.FirstOrDefault()?.Split(" ").Last();
if (string.IsNullOrEmpty(accessToken))
{
return Results.BadRequest("Access token is required");
}
// 从过期的访问令牌中获取用户信息
var principal = jwtService.GetPrincipalFromExpiredToken(accessToken);
if (principal == null)
{
return Results.BadRequest("Invalid access token");
}
var username = principal.Identity?.Name;
var user = await userManager.FindByNameAsync(username!);
if (user == null ||
user.RefreshToken != model.RefreshToken ||
user.RefreshTokenExpiryTime <= DateTime.UtcNow)
{
return Results.BadRequest("Invalid refresh token");
}
// 生成新的令牌
var roles = await userManager.GetRolesAsync(user);
var newTokenResponse = jwtService.GenerateTokens(user, roles);
// 更新数据库中的刷新令牌
user.RefreshToken = newTokenResponse.RefreshToken;
user.RefreshTokenExpiryTime = DateTime.UtcNow.AddDays(7);
await userManager.UpdateAsync(user);
return Results.Ok(newTokenResponse);
})
.AllowAnonymous()
.WithName("Refresh")
.WithOpenApi(); ;
group.MapPost("/Logout", async (
UserManager<ApplicationUser> userManager,
HttpContext context) =>
{
var user = context.User;
var appUser = await userManager.FindByNameAsync(user.Identity?.Name!);
if (appUser == null)
{
return Results.NotFound();
}
// 清除刷新令牌
appUser.RefreshToken = null;
appUser.RefreshTokenExpiryTime = null;
await userManager.UpdateAsync(appUser);
return Results.Ok();
})
.RequireAuthorization()
.WithName("Logout")
.WithOpenApi();
group.MapPost("/Register", async (
RegisterModel model,
UserManager<ApplicationUser> userManager,
JwtService jwtService) =>
{
// 验证模型
if (model.Password != model.ConfirmPassword)
{
return Results.BadRequest(new RegisterResponse(
false,
["密码和确认密码不匹配"], null, null
));
}
// 检查用户名是否已存在
var existingUser = await userManager.FindByNameAsync(model.Username);
if (existingUser != null)
{
return Results.BadRequest(new RegisterResponse(
false,
["用户名已存在"], null, null
));
}
// 检查邮箱是否已存在
var existingEmail = await userManager.FindByEmailAsync(model.Email);
if (existingEmail != null)
{
return Results.BadRequest(new RegisterResponse(
false,
["邮箱已被使用"], null, null
));
}
// 创建新用户
var user = new ApplicationUser
{
UserName = model.Username,
Email = model.Email,
EmailConfirmed = true // 如果需要邮箱验证,设置为 false
};
// 添加用户
var result = await userManager.CreateAsync(user, model.Password);
if (!result.Succeeded)
{
return Results.BadRequest(new RegisterResponse(
false,
result.Errors.Select(e => e.Description), null, null
));
}
// 添加默认角色
// await userManager.AddToRoleAsync(user, "User");
// 生成令牌
var roles = await userManager.GetRolesAsync(user);
var tokenResponse = jwtService.GenerateTokens(user, roles);
// 保存刷新令牌
user.RefreshToken = tokenResponse.RefreshToken;
user.RefreshTokenExpiryTime = DateTime.UtcNow.AddDays(7);
await userManager.UpdateAsync(user);
// 返回成功响应和令牌
return Results.Ok(new RegisterResponse(true, [], tokenResponse, "注册成功"));
})
.AllowAnonymous()
.WithName("Register")
.WithOpenApi();
}
前端只需要修改上一案例中的ParseTokenAsync方法 。
private async Task ParseTokenAsync()
{
var handler = new JwtSecurityTokenHandler();
var token = handler.ReadJwtToken(access_token);
CachedUser = new ClaimsPrincipal(new ClaimsIdentity(token.Claims, "Bearer"));
}
详细案例文章、视频、源码请等待后续发布 。
最后此篇关于[Blazor]一文理清BlazorIdentity鉴权验证的文章就讲到这里了,如果你想了解更多关于[Blazor]一文理清BlazorIdentity鉴权验证的内容请搜索CFSDN的文章或继续浏览相关文章,希望大家以后支持我的博客! 。
我有两列布局 Left pane item 1 it
我是一名优秀的程序员,十分优秀!