《ASP.NET Core 与 RESTful API 开发实战》-- (第8章)-- 读书笔记(中)
第 8 章 认证和安全
8.2 ASP.NET Core Identity
Identity 是 ASP.NET Core 中提供的对用户和角色等信息进行存储与管理的系统
Identity 由3层构成,最底层为 Store 层,即存储层,包含 IUserStore 接口与 IRoleStore 接口
IUserStore 接口定义如下:
namespace Microsoft.AspNetCore.Identity
{
public interface IUserStore<TUser> : IDisposable where TUser : class
{
Task<string> GetUserIdAsync(TUser user, CancellationToken cancellationToken);
Task<string> GetUserNameAsync(TUser user, CancellationToken cancellationToken);
Task SetUserNameAsync(TUser user, string userName, CancellationToken cancellationToken);
Task<string> GetNormalizedUserNameAsync(TUser user, CancellationToken cancellationToken);
Task SetNormalizedUserNameAsync(
TUser user,
string normalizedName,
CancellationToken cancellationToken);
Task<IdentityResult> CreateAsync(
TUser user,
CancellationToken cancellationToken);
Task<IdentityResult> UpdateAsync(
TUser user,
CancellationToken cancellationToken);
Task<IdentityResult> DeleteAsync(
TUser user,
CancellationToken cancellationToken);
Task<TUser> FindByIdAsync(string userId, CancellationToken cancellationToken);
Task<TUser> FindByNameAsync(string normalizedUserName, CancellationToken cancellationToken);
}
}
两个接口定义极为类似,分别用来管理用户与角色,在它们的定义中均包含了对各自的泛型参数 TUser 和 TRole 的查找、创建、更新、删除等数据读取与存储操作
对于这两个接口的实现将决定用户与角色数据是如何存储的,比如存储在数据库中或者文件中,甚至存储在内存中
在 Microsoft.AspNetCore.Identity 中定义了两种形式的 UserStoreBase 抽象类,它们均实现了 IUserStore
public abstract class UserStoreBase<TUser, TKey, TUserClaim, TUserLogin, TUserToken> : IUserLoginStore<TUser>, IUserStore<TUser>, IDisposable, IUserClaimStore<TUser>, IUserPasswordStore<TUser>, IUserSecurityStampStore<TUser>, IUserEmailStore<TUser>, IUserLockoutStore<TUser>, IUserPhoneNumberStore<TUser>, IQueryableUserStore<TUser>, IUserTwoFactorStore<TUser>, IUserAuthenticationTokenStore<TUser>, IUserAuthenticatorKeyStore<TUser>, IUserTwoFactorRecoveryCodeStore<TUser>
where TUser : IdentityUser<TKey>
where TKey : IEquatable<TKey>
where TUserClaim : IdentityUserClaim<TKey>, new()
where TUserLogin : IdentityUserLogin<TKey>, new()
where TUserToken : IdentityUserToken<TKey>, new()
{
。。。
}
public abstract class UserStoreBase<TUser, TRole, TKey, TUserClaim, TUserRole, TUserLogin, TUserToken, TRoleClaim> :
UserStoreBase<TUser, TKey, TUserClaim, TUserLogin, TUserToken>,
IUserRoleStore<TUser>
where TUser : IdentityUser<TKey>
where TRole : IdentityRole<TKey>
where TKey : IEquatable<TKey>
where TUserClaim : IdentityUserClaim<TKey>, new()
where TUserRole : IdentityUserRole<TKey>, new()
where TUserLogin : IdentityUserLogin<TKey>, new()
where TUserToken : IdentityUserToken<TKey>, new()
where TRoleClaim : IdentityRoleClaim<TKey>, new()
{
。。。
}
第一种仅处理对用户的操作,第二种处理对用户与角色的操作
Identity 的第二层为 Managers 层,它包括 UserManager 与 RoleManager 两个类,分别用于处理与用户和角色相关的业务操作
UserManager 的构造函数如下:
public class UserManager<TUser> : IDisposable where TUser : class
{
public UserManager(
IUserStore<TUser> store,// 实现对用户的存储与读取操作
IOptions<IdentityOptions> optionsAccessor,// 访问在程序中添加Identity服务时的IdentityOptions配置
IPasswordHasher<TUser> passwordHasher,// 用于创建密码散列值以及验证密码
IEnumerable<IUserValidator<TUser>> userValidators,// 验证用户的规则集合
IEnumerable<IPasswordValidator<TUser>> passwordValidators,// 验证密码的规则集合
ILookupNormalizer keyNormalizer,// 用于对用户名进行规范化,从而便于查询
IdentityErrorDescriber errors,// 用于提供错误信息
IServiceProvider services,// 用于获取需要的依赖
ILogger<UserManager<TUser>> logger)// 用于记录日志
{
。。。
}
}
Identity 的最上层,即 Extensions 层,提供了一些辅助类(如 SignInManager 类),它包含了一系列与登录相关的方法
使用 Identity
由于用户和角色等数据均存储在数据表中,因此需要创建一个 EF Core 迁移,并通过该迁移在数据库中创建与 Identity 相关的数据表
namespace Library.API.Entities
{
public class User : IdentityUser
{
public DateTimeOffset BirthDate { get; set; }
}
}
namespace Library.API.Entities
{
public class Role : IdentityRole
{
}
}
接下来,修改 LibraryDbContext,使其派生自 IdentityDbContext<TUser, TRole, TKey> 类,TKey 类型参数是用户表与角色表主键字段的类型
public class LibraryDbContext : IdentityDbContext<User, Role, string>
{
。。。
}
需要添加 nuget 包:Microsoft.AspNetCore.Identity.EntityFrameworkCore
接下来,在 startup 中添加 Identity 服务
services.AddIdentity<User, Role>()
.AddEntityFrameworkStores<LibraryDbContext>();
AddIdentity 方法会向容器添加 UserManager、RoleManager,以及它们所依赖的服务,并且会添加 Identity 用到的 Cookie 认证
AddEntityFrameworkStores 方法会将 EF Core 中对 IUserStore 接口和 IroleStore 接口的实现添加到容器中
添加 Identity 服务后,还应修改添加 DbContext 服务的代码为
services.AddDbContext<LibraryDbContext>(
config => config.UseSqlServer(Configuration.GetConnectionString("DefaultConnection"),
optionBuilder => optionBuilder.MigrationsAssembly(typeof(Startup).Assembly.GetName().Name)));
MigrationsAssembly 方法为当前 DbContext 设置其迁移所在的程序集名称,这是由于 DbContext 与为其创建的迁移并不在同一个程序集中
接着,运行以下命令
Add-Migration AddIdentity
Update-Database
上述命令会创建一个名为 AddIdentity 的 EF Core 迁移,该迁移包含了创建与 Identity 相关的数据表操作,并将其修改应用到数据库中
接下来,在 AuthenticateController 中添加创建用户的方法,并修改原来对用户信息验证的逻辑
首先创建 RegisterUser 类,在创建用户时,请求中的信息将会反序列化为此类型
namespace Library.API.Models
{
public class RegisterUser
{
[Required, MinLength(4)]
public string UserName { get; set; }
[EmailAddress]
public string Email { get; set; }
[MinLength(6)]
public string Password { get; set; }
public DateTimeOffset BirthDate { get; set; }
}
}
然后,在 AuthenticateController 中添加 AddUserAsync 方法,用于创建用户
public class AuthenticateController : ControllerBase
{
public IConfiguration Configuration { get; set; }
public RoleManager<Role> RoleManager { get; set; }
public UserManager<User> UserManager { get; set; }
public AuthenticateController(IConfiguration configuration, RoleManager<Role> roleManager, UserManager<User> userManager)
{
Configuration = configuration;
RoleManager = roleManager;
UserManager = userManager;
}
[HttpPost("register", Name = nameof(AddUserAsync))]
public async Task<ActionResult> AddUserAsync(RegisterUser registerUser)
{
var user = new User
{
UserName = registerUser.UserName,
Email = registerUser.Email,
BirthDate = registerUser.BirthDate
};
IdentityResult result = await UserManager.CreateAsync(user, registerUser.Password);
if (result.Succeeded)
{
return Ok();
}
else
{
ModelState.AddModelError("Error", result.Errors.FirstOrDefault()?.Description);
return BadRequest(ModelState);
}
}
。。。
}
接着添加一个根据用户信息生成 Bearer Token 的方法
[HttpPost("token2", Name = nameof(GenerateTokenAsync))]
public async Task<IActionResult> GenerateTokenAsync(LoginUser loginUser)
{
var user = await UserManager.FindByEmailAsync(loginUser.UserName);
if (user == null)
{
return Unauthorized();
}
var result = UserManager.PasswordHasher.VerifyHashedPassword(user, user.PasswordHash, loginUser.Password);
if (result != PasswordVerificationResult.Success)
{
return Unauthorized();
}
var userClaims = await UserManager.GetClaimsAsync(user);
var userRoles = await UserManager.GetRolesAsync(user);
foreach (var roleItem in userRoles)
{
userClaims.Add(new Claim(ClaimTypes.Role, roleItem));
}
var claims = new List<Claim>
{
new Claim(JwtRegisteredClaimNames.Sub, user.UserName),
new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()),
new Claim(JwtRegisteredClaimNames.Email, user.Email)
};
claims.AddRange(userClaims);
// 此处为生成token代码,与GenerateToken方法中的内容相同
if (loginUser.UserName != "demouser" || loginUser.Password != "demopassword")
{
return Unauthorized();
}
//var claims = new List<Claim>
//{
// new Claim(JwtRegisteredClaimNames.Sub,loginUser.UserName)
//};
var tokenConfigSection = Configuration.GetSection("Security:Token");
var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(tokenConfigSection["Key"]));
var signCredential = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);
var jwtToken = new JwtSecurityToken(
issuer: tokenConfigSection["Issuer"],
audience: tokenConfigSection["Audience"],
claims: claims,
expires: DateTime.Now.AddMinutes(3),// 由于 JWT 不支持销毁以及撤回功能,因此在设置它的有效时间时,应该设置一个较短的时间
signingCredentials: signCredential);
return Ok(new
{
token = new JwtSecurityTokenHandler().WriteToken(jwtToken),
expiration = TimeZoneInfo.ConvertTimeFromUtc(jwtToken.ValidTo, TimeZoneInfo.Local)
});
}
在上述方法中,首先验证用户信息是否存在以及用户信息是否正确,如果通过验证,则获取该用户相关的 Claim 以及角色,这些信息最终都会包含在生成的 Token 中
运行程序,注册用户,获取用户信息后请求 token2
接下来介绍授权及其实现
通过 UserManager 类提供的方法可以将用户添加到角色中,然而在这之前,需要先使用 RoleManager 创建相应的角色
private async Task AddUserToRoleAsync(User user, string roleName)
{
if (user == null || string.IsNullOrWhiteSpace(roleName))
{
return;
}
bool isRoleExist = await RoleManager.RoleExistsAsync(roleName);
if (!isRoleExist)
{
await RoleManager.CreateAsync(new Role {Name = roleName});
}
else
{
if (await UserManager.IsInRoleAsync(user, roleName))
{
return;
}
}
await UserManager.AddToRoleAsync(user, roleName);
}
当创建用户或管理用户信息时,调用上述方法即可将用户添加到指定的角色中
await AddUserToRoleAsync(user, "Administrator");
当把用户添加到某一角色中时,如果要使某一个接口仅被指定的角色访问,那么只要在为其添加 [Authorize] 特性时指定 Roles 属性即可
[Authorize(Roles = "Administrator")]
public class BookController : ControllerBase
{
。。。
}
允许多个角色访问,可通过逗号分隔角色名
[Authorize(Roles = "Administrator,Manager")]
同时需要具有多个角色才能访问
[Authorize(Roles = "Administrator")]
[Authorize(Roles = "Manager")]
基于 Claim 的授权则要求用户必须具有某一个指定类型的 Claim,要实现基于 Claim 的授权,需要创建授权策略并为其命名,然后在 [Authorize] 特性中指定 Policy 属性
要创建授权策略,只需在 startup 中添加并配置认证服务
services.AddMvc();
services.AddAuthorization(options =>
{
options.AddPolicy("ManagerOnly", builder => builder.RequireClaim("ManagerId"));
options.AddPolicy("LimitedUsers",
builder => builder.RequireClaim("UserId", new string[] {"1", "2", "3"}));
});
上述方法添加了两个授权策略,ManagerOnly 要求用户必须具有类型为 ManagerId 的 Claim,而 LimitedUsers 则要求用户必须具有类型为 UserId 的 Claim,且它的值必须为指定的值
创建之后,只要在添加 [Authorize] 特性的时候指定 Policy 属性即可
[Authorize(Policy = "ManagerOnly")]
复杂的授权策略需要通过 IAuthorizationRequirement 接口和 AuthorizationHandler 类实现
实现只有注册日期超过3天后才有权限访问
namespace Library.API.Policy
{
public class RegisteredMoreThen3DaysRequirement : AuthorizationHandler<RegisteredMoreThen3DaysRequirement>, IAuthorizationRequirement
{
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, RegisteredMoreThen3DaysRequirement requirement)
{
if (!context.User.HasClaim(cliam => cliam.Type == "RegisterDate"))
{
return Task.CompletedTask;
}
var regDate = Convert.ToDateTime(context.User.FindFirst(c => c.Type == "RegisterDate").Value);
var timeSpan = DateTime.Now - regDate;
if (timeSpan.TotalDays > 3)
{
context.Succeed(requirement);
}
return Task.CompletedTask;
}
}
}
要使用自定义策略,只要将它添加到 AuthorizationPolicyBuilder 类的集合属性 Requirements 中即可
services.AddAuthorization(options =>
{
options.AddPolicy("RegisteredMoreThen3DaysRequirement",
builder => builder.Requirements.Add(new RegisteredMoreThen3DaysRequirement()));
});
之后通过特性指定策略名称即可
本作品采用知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议进行许可。
欢迎转载、使用、重新发布,但务必保留文章署名 郑子铭 (包含链接: http://www.cnblogs.com/MingsonZheng/ ),不得用于商业目的,基于本文修改后的作品务必以相同的许可发布。
如有任何疑问,请与我联系 (MingsonZheng@outlook.com) 。
《ASP.NET Core 与 RESTful API 开发实战》-- (第8章)-- 读书笔记(中)的更多相关文章
- 使用ASP.NET Core构建RESTful API的技术指南
译者荐语:利用周末的时间,本人拜读了长沙.NET技术社区翻译的技术标准<微软RESTFul API指南>,打算按照步骤写一个完整的教程,后来无意中看到了这篇文章,与我要写的主题有不少相似之 ...
- 4类Storage方案(AS开发实战第四章学习笔记)
4.1 共享参数SharedPreferences SharedPreferences按照key-value对的方式把数据保存在配置文件中,该配置文件符合XML规范,文件路径是/data/data/应 ...
- 菜单Menu(AS开发实战第四章学习笔记)
4.5 菜单Menu Android的菜单主要分两种,一种是选项菜单OptionMenu,通过按菜单键或点击事件触发,另一种是上下文菜单ContextMenu,通过长按事件触发.页面的布局文件放在re ...
- [Android]《Android艺术开发探索》第一章读书笔记
1. 典型情况下生命周期分析 (1)一般情况下,当当前Activity从不可见重新变为可见状态时,onRestart方法就会被调用. (2)当用户打开新的Activity或者切换到桌面的时候,回调如下 ...
- 温故知新,使用ASP.NET Core创建Web API,永远第一次
ASP.NET Core简介 ASP.NET Core是一个跨平台的高性能开源框架,用于生成启用云且连接Internet的新式应用. 使用ASP.NET Core,您可以: 生成Web应用和服务.物联 ...
- 快读《ASP.NET Core技术内幕与项目实战》WebApi3.1:WebApi最佳实践
本节内容,涉及到6.1-6.6(P155-182),以WebApi说明为主.主要NuGet包:无 一.创建WebApi的最佳实践,综合了RPC和Restful两种风格的特点 1 //定义Person类 ...
- 零基础ASP.NET Core WebAPI团队协作开发
零基础ASP.NET Core WebAPI团队协作开发 相信大家对“前后端分离”和“微服务”这两个词应该是耳熟能详了.网上也有很多介绍这方面的文章,写的都很好.我这里提这个是因为接下来我要分享的内容 ...
- ASP.NET Core WebApi构建API接口服务实战演练
一.ASP.NET Core WebApi课程介绍 人生苦短,我用.NET Core!提到Api接口,一般会想到以前用到的WebService和WCF服务,这三个技术都是用来创建服务接口,只不过Web ...
- 从 0 使用 SpringBoot MyBatis MySQL Redis Elasticsearch打造企业级 RESTful API 项目实战
大家好!这是一门付费视频课程.新课优惠价 699 元,折合每小时 9 元左右,需要朋友的联系爱学啊客服 QQ:3469271680:我们每课程是明码标价的,因为如果售价为现在的 2 倍,然后打 5 折 ...
- Asp.Net Core 5 REST API - Step by Step
翻译自 Mohamad Lawand 2021年1月19日的文章 <Asp.Net Core 5 Rest API Step by Step> [1] 在本文中,我们将创建一个简单的 As ...
随机推荐
- vue-cli3开启gzip压缩
首先添加yarn add 插件 "compression-webpack-plugin" 然后在vue.config.js里引入 在configureWebpack里增加插件代码. ...
- Element UI Table合并行
Vue使用Element-ui Table 合并行,官方只是一个非常简单的合并例子,通常业务都是相同的某个字段进行合并. 效果图 代码实现 1.Table <el-table :data=&qu ...
- confiparse遇到特殊字符的解析处理
一.背景:confiparse类解析mysql密码时发现包含特殊字符时出现报错的情况:配置文件如下: 代码如下: import configparser import os #读取配置 conf=co ...
- C++中不支持strdup(),使用_strdup()
1.问题 C4996 'strdup': The POSIX name for this item is deprecated. Instead, use the ISO C and C++ conf ...
- strlen和sizeof区别
strlen 和 sizeof 是 C/C++ 中用于获取字符串长度或数据类型大小的两个不同的操作. 1.strlen: strlen 是在 C 语言和 C++ 中用于获取以空字符 ('\0') 结尾 ...
- Qt5.9 UI设计(四)——布局设计及自定义界面
前言 前面我们已经创建了mainwindow ControlTabWidget ControlTreeWidget maintitlebar 4个UI几面,我们需要将其他三个UI放置到mainwind ...
- [转帖]linux 内核协议栈 TCP time_wait 原理、配置、副作用
https://my.oschina.net/u/4087916/blog/3051356 0. 手把手教你做中间件.高性能服务器.分布式存储技术交流群 手把手教你做中间件.高性能服务器.分布式存 ...
- [转帖]分享一个Navicat16最新版永久试用的办法
https://zhuanlan.zhihu.com/p/614621302 新建bat,就叫 navicat无限试用.bat @echo off echo Delete HKEY_CURRENT_U ...
- [转帖]VMware vCenter证书过期解决方法
https://www.yii666.com/blog/395521.html vCenter证书过期解决方法 目录 1 概述 2 详细操作步骤 2.1 检查关键的STS证书是否过期并修复 2.2 检 ...
- [转帖]Ubuntu Server安装图形界面
最早接触到的Linux系统是Ubuntu 10.04,当时在自己的一台Win7笔记本电脑上安装的Win/Ubuntu双系统,Ubuntu简洁的操作界面给我留下了深刻的印象. 后来开始做一些服务器开发, ...