【ASP.NET Core】身份认证——Identity标识框架指南
文章目录
- 前言
- * 身份认证与授权
- 一、ASP.NET Core中的Identity
- 二、Identity框架架构
- 2.1 Identity上下文
- 2.1.1 IdentityUserContext
- 2.1.1.1 IdentityUserContext泛型类
- 2.1.1.2 核心属性
- 2.1.1.3 模型创建
- 2.1.2 IdentityDbContext
- 2.1.2.1 IdentityDbContext泛型类
- 2.1.2.2 核心属性
- 2.1.2.3 模型创建
- 2.2 Identity Manager
- 三、Identity的使用
- 3.1 Nuget引入
- 3.2 创建用户类和角色类
- 3.3 dbContext的引用
- 3.4 Program.cs注册服务
- 3.5 Controller通过identity manager调用identity实体数据
- 总结
前言
在ASP.NET Core 中,有两道非常重要的安全访问机制,分别是认证(Authentication)和授权(Authorization)。本篇文章主要介绍在ASP.NET Core中通过官方的Identity身份认证框架来实现访问用户的认证,授权的部分后面会通过另一篇关于JWT的实现来介绍使用。
* 身份认证与授权
在ASP.NET Core 中,身份认证(Authentication)和授权(Authorization)是我们绕不开的用于保障应用的两道安全机制。无论是我们传统开发中基于session或者是现代开发中流行的JWT,本质上还是通过先认证,后授权的方式。
身份认证(Authentication):比方说在登录流程中或使用客户端验证信息验证身份,这都属于身份认证(Authentication)的范畴:
- 首次登录:用户提交用户名和密码,服务器端验证登录凭据
- 使用session的情况:服务器生成一个唯一的SessionID,并将用户信息(如身份、权限等)存储在服务器端,将SessionID返回给客户端保存;
- 使用JWT的情况:服务器生成包含用户身份信息的 JWT 令牌返回给客户端。
- 完成登录后请求:客户端携带保存的cookie或者JWT
- 使用session的情况:客户端将唯一SessionID通过cookie传给服务器用于验证身份;
- 使用JWT的情况:客户端将JWT放在header发送到服务器,服务器验证是否被串改验证身份,解析payload。
授权(Authorization):而在身份已确认的基础上,判断该用户是否有权限访问请求的资源。这些都是属于授权(Authorization)的范畴
- 访问后台管理员页面:通过上一步身份认证来验证当前用户后,鉴别此用户是否具有管理员权限访问。
换句话说,Authentication就是指系统用于验证当前访问的用户身份,通过用户名密码、cookie、令牌等确认是否是合法用户,也就是解析当前用户是谁。而Authorization是用来确定这个用户是否有权限去访问请求的资源,也就是判断用户能做什么。
一、ASP.NET Core中的Identity
ASP.NET Core Identity是一个管理用户、密码、配置文件数据、角色、声明、令牌、电子邮件确认等的身份验证系统,是微软官方提供的身份认证框架。
比起我们日常工作里自己去设计用户表、密码加密,ASP.NET Core Identity提供了一套契合实际工作生产的身份认证机制,比如封装了登录、注册、重置等常用方法。在一些安全性上也包含了密码哈希加密,账号锁定等策略。并且我们也可以自定义属性用于扩展登录上的一些操作。
- 标识(ldentity)框架:采用基于角色的访问控制(Role-Based Access Control,简称RBAC)策略,内置了对用户、角色等表的管理以及相关的接口,支持外部登录、2FA等,
- 标识框架使用EF Core对数据库进行操作,因此标识框架支持几乎所有数据库。
ASP.NET Core Identity框架提供了一组开箱即用的api,帮助我们去管理身份验证。这里通过webapi的方式搭建一个包含登录和注册和重置密码的操作,下面让我们开始了解并且使用这个官方的身份认证框架。
二、Identity框架架构
2.1 Identity上下文
Identity框架本质上是还是一个基于EF Core的框架,通过两个核心的上下文(IdentityDbContext ,IdentityUserContext )对用户、角色、权限等核心数据结构进行操作。属于数据访问的底层支撑,负责将实体模型映射到数据库,并提供基础的数据操作能力。
其中IdentityUserContext继承自DbContext,IdentityDbContext继承自IdentityUserContext。这两个上下文的区别在于:
- IdentityUserContext: 作为一个基础上下文,处理用户及用户关联数据,比如用户TUser、用户声明TUserClaim、用户登录TUserLogin、用户令牌TUserToken。简而言之,它适用于仅需用户认证的简单系统,IdentityUserContext只包含对用户相关数据操作。
- IdentityDbContext:是一个完整的Identity上下文,它继承了IdentityUserContext,并且包含了角色及角色关联的数据。比如角色,角色声明、用户和角色关联关系。它使用于基于角色的访问控制(Role-Based Access Control,简称RBAC)策略的复杂系统。
2.1.1 IdentityUserContext
2.1.1.1 IdentityUserContext泛型类
IdentityUserContext的核心是一个名为IdentityUserContext的抽象泛型类,它接收User相关的类作为构造函数。也包含各种默认的构造方法。
IdentityUserContext< TUser>和 public class IdentityUserContext< TUser, TKey>最后都是通过内部的base关键字,调用抽象类IdentityUserContext< TUser, TKey, TUserClaim, TUserLogin, TUserToken>。
抽象类IdentityUserContext< TUser, TKey, TUserClaim, TUserLogin, TUserToken>继承自DbContext,通过内部base关键字调用DbContext构造函数。
这些泛型类的不同版本,都默认有一个空构造函数。主要是为了兼容EFCore的DbContext的空构造函数,实现约定大于配置。并且在通过工具迁移的时候能够至少保证有无参构造函数供调用。
IdentityUserContext源码
public class IdentityUserContext<TUser> : IdentityUserContext<TUser, string> where TUser : IdentityUser{/// <summary>/// Initializes a new instance of <see cref="IdentityUserContext{TUser}"/>./// </summary>/// <param name="options">The options to be used by a <see cref="DbContext"/>.</param>public IdentityUserContext(DbContextOptions options) : base(options) { }/// <summary>/// Initializes a new instance of the <see cref="IdentityUserContext{TUser}" /> class./// </summary>protected IdentityUserContext() { }}/// <summary>/// Base class for the Entity Framework database context used for identity./// </summary>/// <typeparam name="TUser">The type of user objects.</typeparam>/// <typeparam name="TKey">The type of the primary key for users and roles.</typeparam>public class IdentityUserContext<TUser, TKey> : IdentityUserContext<TUser, TKey, IdentityUserClaim<TKey>, IdentityUserLogin<TKey>, IdentityUserToken<TKey>>where TUser : IdentityUser<TKey>where TKey : IEquatable<TKey>{/// <summary>/// Initializes a new instance of the db context./// </summary>/// <param name="options">The options to be used by a <see cref="DbContext"/>.</param>public IdentityUserContext(DbContextOptions options) : base(options) { }/// <summary>/// Initializes a new instance of the class./// </summary>protected IdentityUserContext() { }}/// <summary>/// Base class for the Entity Framework database context used for identity./// </summary>/// <typeparam name="TUser">The type of user objects.</typeparam>/// <typeparam name="TKey">The type of the primary key for users and roles.</typeparam>/// <typeparam name="TUserClaim">The type of the user claim object.</typeparam>/// <typeparam name="TUserLogin">The type of the user login object.</typeparam>
/// <typeparam name="TUserToken">The type of the user token object.</typeparam>
public abstract class IdentityUserContext<TUser, TKey, TUserClaim, TUserLogin, TUserToken> : DbContextwhere TUser : IdentityUser<TKey>where TKey : IEquatable<TKey>where TUserClaim : IdentityUserClaim<TKey>where TUserLogin : IdentityUserLogin<TKey>where TUserToken : IdentityUserToken<TKey>
{/// <summary>/// Initializes a new instance of the class./// </summary>/// <param name="options">The options to be used by a <see cref="DbContext"/>.</param>public IdentityUserContext(DbContextOptions options) : base(options) { }/// <summary>/// Initializes a new instance of the class./// </summary>protected IdentityUserContext() { }
}
2.1.1.2 核心属性
IdentityUserContext包含四个核心属性,对应 Identity 框架中存储用户相关数据的数据库表。主要用于管理用户身份信息。
属性被vritual修饰,允许子类重写,实现自定义逻辑。比如添加一些操作审计日志之类的。
IdentityUserContext方法
/// <summary>/// Gets or sets the <see cref="DbSet{TEntity}"/> of Users./// </summary>public virtual DbSet<TUser> Users { get; set; } = default!;/// <summary>/// Gets or sets the <see cref="DbSet{TEntity}"/> of User claims./// </summary>public virtual DbSet<TUserClaim> UserClaims { get; set; } = default!;/// <summary>/// Gets or sets the <see cref="DbSet{TEntity}"/> of User logins./// </summary>public virtual DbSet<TUserLogin> UserLogins { get; set; } = default!;/// <summary>/// Gets or sets the <see cref="DbSet{TEntity}"/> of User tokens./// </summary>public virtual DbSet<TUserToken> UserTokens { get; set; } = default!;
2.1.1.3 模型创建
IdentityUserContext通过一个OnModelCreating重载,实现了TUser,TUserClaim,TUserLogin,TUserToken模型的配置。具体的初始化方法在OnModelCreating调用的OnModelCreatingVersion里。值得注意的是OnModelCreatingVersion1和OnModelCreatingVersion2都是虚方法,方便子类重写。
protected override void OnModelCreating(ModelBuilder builder)
{var version = GetStoreOptions()?.SchemaVersion ?? IdentitySchemaVersions.Version1;OnModelCreatingVersion(builder, version);
}internal virtual void OnModelCreatingVersion(ModelBuilder builder, Version schemaVersion)
{if (schemaVersion >= IdentitySchemaVersions.Version2){OnModelCreatingVersion2(builder);}else{OnModelCreatingVersion1(builder);}
}internal virtual void OnModelCreatingVersion2(ModelBuilder builder)
{
}internal virtual void OnModelCreatingVersion1(ModelBuilder builder)
{}
2.1.2 IdentityDbContext
2.1.2.1 IdentityDbContext泛型类
和IdentityUserContext类似,IdentityDbContext的核心也是一个名为IdentityDbContext的抽象泛型类,它接收User和Role相关的类(TUser, TRole, TKey, TUserClaim, TUserRole, TUserLogin, TRoleClaim, TUserToken)作为构造函数。也包含各种默认的构造方法。
IdentityDbContext, IdentityDbContext< TUser>和 IdentityDbContext< TUser, TRole, TKey>最后都是通过内部的base关键字,调用抽象类IdentityDbContext< TUser, TKey, TUserClaim, TUserLogin, TUserToken>。
IdentityDbContext< TUser, TKey, TUserClaim, TUserLogin, TUserToken>继承自IdentityUserContext,使用IdentityUserContext<TUser, TKey, TUserClaim, TUserLogin, TUserToken>的构造函数。在内部通过base关键字调用IdentityUserContext的构造函数。
IdentityUserContext源码
public class IdentityDbContext : IdentityDbContext<IdentityUser, IdentityRole, string>
{public IdentityDbContext(DbContextOptions options) : base(options) { }protected IdentityDbContext() { }
}public class IdentityDbContext<TUser> : IdentityDbContext<TUser, IdentityRole, string> where TUser : IdentityUser
{public IdentityDbContext(DbContextOptions options) : base(options) { }protected IdentityDbContext() { }
}public class IdentityDbContext<TUser, TRole, TKey> : IdentityDbContext<TUser, TRole, TKey, IdentityUserClaim<TKey>, IdentityUserRole<TKey>, IdentityUserLogin<TKey>, IdentityRoleClaim<TKey>, IdentityUserToken<TKey>>where TUser : IdentityUser<TKey>where TRole : IdentityRole<TKey>where TKey : IEquatable<TKey>
{public IdentityDbContext(DbContextOptions options) : base(options) { }protected IdentityDbContext() { }
}public abstract class IdentityDbContext<TUser, TRole, TKey, TUserClaim, TUserRole, TUserLogin, TRoleClaim, TUserToken> : IdentityUserContext<TUser, TKey, TUserClaim, TUserLogin, TUserToken>where TUser : IdentityUser<TKey>where TRole : IdentityRole<TKey>where TKey : IEquatable<TKey>where TUserClaim : IdentityUserClaim<TKey>where TUserRole : IdentityUserRole<TKey>where TUserLogin : IdentityUserLogin<TKey>where TRoleClaim : IdentityRoleClaim<TKey>where TUserToken : IdentityUserToken<TKey>
{public IdentityDbContext(DbContextOptions options) : base(options) { }protected IdentityDbContext() { }
}
2.1.2.2 核心属性
比起IdentityUserContext已经包含的用户属性,IdentityDbContext内部有三个角色相关的属性——UserRoles,Roles和RoleClaims。对应 Identity 框架中存储角色相关数据的数据库表。主要用于管理角色,角色声明和用户角色绑定信息。
属性同样被vritual修饰,允许子类重写,实现自定义逻辑。
IdentityDbContext方法
/// <summary>
/// Gets or sets the <see cref="DbSet{TEntity}"/> of User roles.
/// </summary>
public virtual DbSet<TUserRole> UserRoles { get; set; } = default!;/// <summary>
/// Gets or sets the <see cref="DbSet{TEntity}"/> of roles.
/// </summary>
public virtual DbSet<TRole> Roles { get; set; } = default!;/// <summary>
/// Gets or sets the <see cref="DbSet{TEntity}"/> of role claims.
/// </summary>
public virtual DbSet<TRoleClaim> RoleClaims { get; set; } = default!;
2.1.2.3 模型创建
IdentityUserContext通过一个OnModelCreating重载,实现了TUser(指定外键关系),TRole,TRoleClaim,TUserRole模型的配置。(具体方法在OnModelCreating调用的OnModelCreatingVersion里)
protected override void OnModelCreating(ModelBuilder builder)
{base.OnModelCreating(builder);
}
前面我们提到,IdentityUserContext里的OnModelCreatingVersion虚方法。这里IdentityDbContext继承自IdentityUserContext,重写了OnModelCreatingVersion虚方法。通过base关键字调用父类的OnModelCreatingVersion2方法,初始化User相关的配置,然后在下面继续编写Role相关的配置。
internal override void OnModelCreatingVersion2(ModelBuilder builder)
{base.OnModelCreatingVersion2(builder);// Current no differences between Version 2 and Version 1builder.Entity<TUser>(b =>{b.HasMany<TUserRole>().WithOne().HasForeignKey(ur => ur.UserId).IsRequired();});builder.Entity<TRole>(b =>{/.../ });builder.Entity<TRoleClaim>(b =>{/.../ });builder.Entity<TUserRole>(b =>{/.../ });
}
2.2 Identity Manager
Identity框架中,Managers可以理解为一个业务逻辑的封装,封装身份认证的核心逻辑。Manager 组件有UserManager、SignInManager,RoleManager。
- UserManager< TUser>:提供用于在持久性存储中管理用户的 API,负责用户的创建、查询、更新、删除、密码管理、角色分配等操作
- 创建用户 CreateAsync()
- 删除用户 DeleteAsync()
- 验证密码 CheckPasswordAsync()
- 修改密码 ChangePasswordAsync()
- 分配角色 AddToRoleAsync()
- 移除角色 RemoveFromRoleAsync()
- 添加用户声明 AddClaimAsync()
- 获取用户声明 GetClaimsAsync()
- 锁定用户 SetLockoutEndDateAsync() 【直到指定的结束日期过去。 设置过去结束日期会立即解锁用户 】
- 完整API: 微软官方文档
- SignInManager< TUser>:提供用于用户登录的 API,负责用户登录、注销、身份验证等会话管理操作
- 密码登录 PasswordSignInAsync()
- 外部登录用户 ExternalLoginSignInAsync()
- 注销 SignOutAsync()
- 验证用户是否已登录 IsSignedIn()
- 双因素认证 TwoFactorRecoveryCodeSignInAsync()
- 完整API: 微软官方文档
- RoleManager< TRole>:提供用于管理持久性存储区中角色的 API,用于角色的创建、查询、更新、删除等管理操作
- 创建角色 CreateAsync()
- 删除角色 DeleteAsync()
- 检查角色是否存在 RoleExistsAsync()
- 为角色添加声明 AddClaimAsync()
- 为角色移除声明 RemoveClaimAsync()
- 完整API: 微软官方文档
这类Manager不直接与上文提到的Identity上下文的直接依赖,而是通过IUserStore或者IRoleStore间接访问上下文。UserManager通过IUserStore访问IdentityUserContext上下文,RoleManager通过IRoleStore访问IdentityDbContext上下文,SignInManager依赖UserManager。
这样一来,我们就能在Controller里,通过依赖注入各类identity Manager,调用identity实体数据。
3.5小结有关于UserManager,RoleManager的使用示例
三、Identity的使用
3.1 Nuget引入
dotnet add package Microsoft.AspNetCore.Identity.EntityFrameworkCore --version 9.0.8
3.2 创建用户类和角色类
ldentity框架采用的基于角色的访问控制策略,我们需要建立两个基础类来作为实际数据的映射,分别是用户类和角色类。它们需要各种继承自一个IdentityUser和IdentityRole的泛型
IdentityUser和IdentityRole的泛型类型必须一致,也就是User类和Role类的主键类型必须一致,不然后续对IdentityDbContext的操作会无法编译通过
public class User: IdentityUser<long>
{public string? NickName { get; set; }public long? RowVersion { get; set; }
}public class Role : IdentityRole<long>
{
}
3.3 dbContext的引用
这里我们引用完整的Identity上下文——IdentityDbContext。
IdentityDbContext支持传入自定义User和Role写法的构造方法,第三个参数是全部Identity相关实体的主键。
public class CoreDbContext : IdentityDbContext<User, Role, long>
{public CoreDbContext(DbContextOptions<CoreDbContext> options) : base(options) { }protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder){//base.OnConfiguring(optionsBuilder);}protected override void OnModelCreating(ModelBuilder modelBuilder){base.OnModelCreating(modelBuilder);modelBuilder.ApplyConfigurationsFromAssembly(this.GetType().Assembly);}
}
3.4 Program.cs注册服务
builder.Services里注册Identity相关的配置。
// 配置数据保护服务,用于加密和解密数据
builder.Services.AddDataProtection();
// 配置Identity身份验证服务,设置用户和角色管理选项
builder.Services.AddIdentity<User,Role>(options =>
{// 设置密码策略选项options.Password.RequireDigit = false; // 密码不要求包含数字options.Password.RequiredLength = 6; // 密码最小长度为6位options.Password.RequireLowercase = false; // 密码不要求包含小写字母options.Password.RequireNonAlphanumeric = false; // 密码不要求包含非字母数字字符options.Password.RequireUppercase = false; // 密码不要求包含大写字母// 设置令牌提供程序选项options.Tokens.PasswordResetTokenProvider = TokenOptions.DefaultEmailProvider; // 密码重置令牌使用默认邮件提供程序options.Tokens.EmailConfirmationTokenProvider = TokenOptions.DefaultEmailProvider; // 邮箱确认令牌使用默认邮件提供程序
})
// 配置EntityFramework存储提供程序,使用CoreDbContext作为数据上下文
.AddEntityFrameworkStores<CoreDbContext>()
// 添加默认令牌提供程序用于生成安全令牌
.AddDefaultTokenProviders();
3.5 Controller通过identity manager调用identity实体数据
完整的登录,注册,重置和登出方法。
邮箱服务可以参考我的另一篇博客
链接: 【ASP.NET Core】基于MailKit(SMTP 协议)实现邮件发送
[Route("api/[controller]/[action]")][ApiController]public class AuthController : ControllerBase{private readonly ILogger<AuthController> _logger;private readonly IWebHostEnvironment _webHostEnvironment;private readonly IJWTService _jwtService;private readonly UserManager<User> _userManager;private readonly RoleManager<Role> _roleManager;private readonly SignInManager<User> _signInManager;private readonly EmailService _emailService;public AuthController(ILogger<AuthController> logger, IJWTService jwtService, UserManager<User> userManager, RoleManager<Role> roleManager, IWebHostEnvironment webHostEnvironment = null, EmailService emailService = null){_logger = logger;_jwtService = jwtService;_userManager = userManager;_roleManager = roleManager;_webHostEnvironment = webHostEnvironment;_emailService = emailService;}/// <summary>/// 登录/// </summary>/// <param name="loginUser"></param>/// <returns></returns>[HttpPost]public async Task<ActionResult<LoginResponse>> Login([FromBody] LoginUserReq loginUser){if (loginUser.UserName == null && loginUser.Email == null){return Ok("请输入用户名或邮箱");}User? user = await _userManager.FindByNameAsync(loginUser.UserName);if (user == null){user = await _userManager.FindByEmailAsync(loginUser.Email);if (user == null){if (_webHostEnvironment.IsDevelopment()){return Ok("用户不存在");}else{return Ok("账号或密码错误");}}}if (await _userManager.IsLockedOutAsync(user)){return Ok($"用户被锁,{user.LockoutEnd}");}if (!await _userManager.CheckPasswordAsync(user, loginUser.Password)){if (_webHostEnvironment.IsDevelopment()){return Ok("登录密码错误");}else{return BadRequest("账号或密码错误");}}HttpUser httpUser = new HttpUser(){UserId = user.Id,Name = user.UserName,NickName = user.NickName,RoleList = (await _userManager.GetRolesAsync(user)).ToList(),};var token = _jwtService.GenerateToken(httpUser);return new LoginResponse(){Token = token};}/// <summary>/// 发送重置密码验证码/// </summary>/// <param name="userName"></param>/// <returns></returns>[HttpGet]public async Task<ActionResult> SendRestPasswordCode(string userName){User? user = await _userManager.FindByNameAsync(userName);if (user == null){if (_webHostEnvironment.IsDevelopment()){return Ok("用户不存在");}return Ok("执行有误");}if (user.Email == null || user.Email == ""){return Ok("用户没有邮箱");}var token = await _userManager.GeneratePasswordResetTokenAsync(user);try{await _emailService.SendEmailAsync(user.Email,"重置密码",$"<h1>验证码如下</h1><p>{token}</p>");return Content("邮件发送成功");}catch (Exception ex){return Content($"邮件发送失败: {ex.Message}");}}/// <summary>/// 重置密码/// </summary>/// <param name="resetPasswordReq"></param>/// <returns></returns>[HttpPost]public async Task<ActionResult> ResetPassword(ResetPasswordReq resetPasswordReq){var user = await _userManager.FindByNameAsync(resetPasswordReq.UserName);if (user == null){if (_webHostEnvironment.IsDevelopment()){return Ok("用户不存在");}return Ok("执行有误");}var result = await _userManager.ResetPasswordAsync(user, resetPasswordReq.Token, resetPasswordReq.Password);if (result.Succeeded){return Ok("密码重置成功");}return Ok("密码重置失败");}/// <summary>/// 注册/// </summary>/// <param name="registerUser"></param>/// <returns></returns>[HttpPost]public async Task<ActionResult> Register([FromBody] RegisterUserReq registerUser){if (!ModelState.IsValid){return BadRequest(ModelState);}var user = new User(){UserName = registerUser.UserName,Email = registerUser.Email,EmailConfirmed = false, // 初始设置为未验证};var result = await _userManager.CreateAsync(user, registerUser.Password);if (result.Succeeded){//生产邮箱验证码var code = await _userManager.GenerateEmailConfirmationTokenAsync(user);code = WebEncoders.Base64UrlEncode(Encoding.UTF8.GetBytes(code));try{await _emailService.SendEmailAsync(user.Email,"账号注册",$"<h1>验证链接如下</h1><p>https://localhost:7154/api/auth/ConfirmRegister/{user.Id}/{code}</p>");return Content("邮件发送成功");}catch (Exception ex){return Content($"邮件发送失败: {ex.Message}");}}foreach (var error in result.Errors){ModelState.AddModelError(string.Empty, error.Description);}return BadRequest(ModelState);}/// <summary>/// 确认注册/// </summary>/// <param name="userId"></param>/// <param name="code"></param>/// <returns></returns>[HttpGet("{userId}/{code}")]public async Task<ActionResult> ConfirmRegister(string userId, string code){if (userId == null || code == null){return BadRequest("请输入合适的参数");}var user = await _userManager.FindByIdAsync(userId);if (user == null){return NotFound($"用户名无效。");}// 解码令牌var decodedCode = Encoding.UTF8.GetString(WebEncoders.Base64UrlDecode(code));// 确认邮箱var result = await _userManager.ConfirmEmailAsync(user, decodedCode);if (result.Succeeded){var created = await _userManager.AddToRoleAsync(user, "admin");if (!created.Succeeded){return Ok("用户添加角色失败");}return Ok("验证成功");}else{return BadRequest("验证失败");}}/// <summary>/// 登出/// </summary>/// <returns></returns>public async Task<ActionResult> LoginOut(string userId){await _signInManager.SignOutAsync();return Ok("登出成功");}}
总结
以上就是如何使用Identity 框架快速实现用户管理功能,当然Identity标识框架的功能不止于此,后续我会大家总结各种特殊的用法。