前言
在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> : DbContext
where 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 1
builder.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标识框架的功能不止于此,后续我会大家总结各种特殊的用法。

488

被折叠的 条评论
为什么被折叠?



