《ASP.NET Core 與 RESTful API 開(kāi)發(fā)實(shí)戰(zhàn)》-- (第8章)-- 讀書(shū)筆記(中)

第 8 章 認(rèn)證和安全
8.2 ASP.NET Core Identity
Identity 是 ASP.NET Core 中提供的對(duì)用戶和角色等信息進(jìn)行存儲(chǔ)與管理的系統(tǒng)
Identity 由3層構(gòu)成,最底層為 Store 層,即存儲(chǔ)層,包含 IUserStore
IUserStore
namespace Microsoft.AspNetCore.Identity
{
public interface IUserStore : 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 CreateAsync(
TUser user,
CancellationToken cancellationToken);
Task UpdateAsync(
TUser user,
CancellationToken cancellationToken);
Task DeleteAsync(
TUser user,
CancellationToken cancellationToken);
Task FindByIdAsync(string userId, CancellationToken cancellationToken);
Task FindByNameAsync(string normalizedUserName, CancellationToken cancellationToken);
}
}
兩個(gè)接口定義極為類似,分別用來(lái)管理用戶與角色,在它們的定義中均包含了對(duì)各自的泛型參數(shù) TUser 和 TRole 的查找、創(chuàng)建、更新、刪除等數(shù)據(jù)讀取與存儲(chǔ)操作
對(duì)于這兩個(gè)接口的實(shí)現(xiàn)將決定用戶與角色數(shù)據(jù)是如何存儲(chǔ)的,比如存儲(chǔ)在數(shù)據(jù)庫(kù)中或者文件中,甚至存儲(chǔ)在內(nèi)存中
在 Microsoft.AspNetCore.Identity 中定義了兩種形式的 UserStoreBase 抽象類,它們均實(shí)現(xiàn)了 IUserStore
public abstract class UserStoreBaseTKey, TUserClaim, TUserLogin, TUserToken> : IUserLoginStore, IUserStore, IDisposable, IUserClaimStore, IUserPasswordStore, IUserSecurityStampStore, IUserEmailStore, IUserLockoutStore, IUserPhoneNumberStore, IQueryableUserStore, IUserTwoFactorStore, IUserAuthenticationTokenStore, IUserAuthenticatorKeyStore, IUserTwoFactorRecoveryCodeStore
where TUser : IdentityUser
where TKey : IEquatable
where TUserClaim : IdentityUserClaim, new()
where TUserLogin : IdentityUserLogin, new()
where TUserToken : IdentityUserToken, new()
{
。。。
}
public abstract class UserStoreBaseTRole, TKey, TUserClaim, TUserRole, TUserLogin, TUserToken, TRoleClaim> :
UserStoreBase,
IUserRoleStore
where TUser : IdentityUser
where TRole : IdentityRole
where TKey : IEquatable
where TUserClaim : IdentityUserClaim, new()
where TUserRole : IdentityUserRole, new()
where TUserLogin : IdentityUserLogin, new()
where TUserToken : IdentityUserToken, new()
where TRoleClaim : IdentityRoleClaim, new()
{
。。。
}
第一種僅處理對(duì)用戶的操作,第二種處理對(duì)用戶與角色的操作
Identity 的第二層為 Managers 層,它包括 UserManager 與 RoleManager 兩個(gè)類,分別用于處理與用戶和角色相關(guān)的業(yè)務(wù)操作
UserManager 的構(gòu)造函數(shù)如下:
public class UserManager : IDisposable where TUser : class
{
public UserManager(
IUserStore store,// 實(shí)現(xiàn)對(duì)用戶的存儲(chǔ)與讀取操作
IOptions optionsAccessor,// 訪問(wèn)在程序中添加Identity服務(wù)時(shí)的IdentityOptions配置
IPasswordHasher passwordHasher,// 用于創(chuàng)建密碼散列值以及驗(yàn)證密碼
IEnumerable> userValidators,// 驗(yàn)證用戶的規(guī)則集合
IEnumerable> passwordValidators,// 驗(yàn)證密碼的規(guī)則集合
ILookupNormalizer keyNormalizer,// 用于對(duì)用戶名進(jìn)行規(guī)范化,從而便于查詢
IdentityErrorDescriber errors,// 用于提供錯(cuò)誤信息
IServiceProvider services,// 用于獲取需要的依賴
ILogger> logger)// 用于記錄日志
{
。。。
}
}
Identity 的最上層,即 Extensions 層,提供了一些輔助類(如 SignInManager 類),它包含了一系列與登錄相關(guān)的方法
使用 Identity
由于用戶和角色等數(shù)據(jù)均存儲(chǔ)在數(shù)據(jù)表中,因此需要?jiǎng)?chuàng)建一個(gè) EF Core 遷移,并通過(guò)該遷移在數(shù)據(jù)庫(kù)中創(chuàng)建與 Identity 相關(guān)的數(shù)據(jù)表
namespace Library.API.Entities
{
public class User : IdentityUser
{
public DateTimeOffset BirthDate { get; set; }
}
}
namespace Library.API.Entities
{
public class Role : IdentityRole
{
}
}
接下來(lái),修改 LibraryDbContext,使其派生自 IdentityDbContext
public class LibraryDbContext : IdentityDbContextRole, string>
{
。。。
}
需要添加 nuget 包:Microsoft.AspNetCore.Identity.EntityFrameworkCore
接下來(lái),在 startup 中添加 Identity 服務(wù)
services.AddIdentityRole >()
.AddEntityFrameworkStores ();
AddIdentity 方法會(huì)向容器添加 UserManager、RoleManager,以及它們所依賴的服務(wù),并且會(huì)添加 Identity 用到的 Cookie 認(rèn)證
AddEntityFrameworkStores 方法會(huì)將 EF Core 中對(duì) IUserStore
添加 Identity 服務(wù)后,還應(yīng)修改添加 DbContext 服務(wù)的代碼為
services.AddDbContext(
config => config.UseSqlServer(Configuration.GetConnectionString("DefaultConnection"),
optionBuilder => optionBuilder.MigrationsAssembly(typeof(Startup).Assembly.GetName().Name)));
MigrationsAssembly 方法為當(dāng)前 DbContext 設(shè)置其遷移所在的程序集名稱,這是由于 DbContext 與為其創(chuàng)建的遷移并不在同一個(gè)程序集中
接著,運(yùn)行以下命令
Add-Migration AddIdentity
Update-Database
上述命令會(huì)創(chuàng)建一個(gè)名為 AddIdentity 的 EF Core 遷移,該遷移包含了創(chuàng)建與 Identity 相關(guān)的數(shù)據(jù)表操作,并將其修改應(yīng)用到數(shù)據(jù)庫(kù)中
接下來(lái),在 AuthenticateController 中添加創(chuàng)建用戶的方法,并修改原來(lái)對(duì)用戶信息驗(yàn)證的邏輯
首先創(chuàng)建 RegisterUser 類,在創(chuàng)建用戶時(shí),請(qǐng)求中的信息將會(huì)反序列化為此類型
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 方法,用于創(chuàng)建用戶
public class AuthenticateController : ControllerBase
{
public IConfiguration Configuration { get; set; }
public RoleManager RoleManager { get; set; }
public UserManager UserManager { get; set; }
public AuthenticateController(IConfiguration configuration, RoleManager roleManager, UserManager userManager)
{
Configuration = configuration;
RoleManager = roleManager;
UserManager = userManager;
}
[HttpPost("register", Name = nameof(AddUserAsync))]
public async Task 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);
}
}
。。。
}
接著添加一個(gè)根據(jù)用戶信息生成 Bearer Token 的方法
[HttpPost("token2", Name = nameof(GenerateTokenAsync))]
public async Task 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
{
new Claim(JwtRegisteredClaimNames.Sub, user.UserName),
new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()),
new Claim(JwtRegisteredClaimNames.Email, user.Email)
};
claims.AddRange(userClaims);
// 此處為生成token代碼,與GenerateToken方法中的內(nèi)容相同
if (loginUser.UserName != "demouser" || loginUser.Password != "demopassword")
{
return Unauthorized();
}
//var claims = new List
//{
// 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 不支持銷毀以及撤回功能,因此在設(shè)置它的有效時(shí)間時(shí),應(yīng)該設(shè)置一個(gè)較短的時(shí)間
signingCredentials: signCredential);
return Ok(new
{
token = new JwtSecurityTokenHandler().WriteToken(jwtToken),
expiration = TimeZoneInfo.ConvertTimeFromUtc(jwtToken.ValidTo, TimeZoneInfo.Local)
});
}
在上述方法中,首先驗(yàn)證用戶信息是否存在以及用戶信息是否正確,如果通過(guò)驗(yàn)證,則獲取該用戶相關(guān)的 Claim 以及角色,這些信息最終都會(huì)包含在生成的 Token 中
運(yùn)行程序,注冊(cè)用戶,獲取用戶信息后請(qǐng)求 token2
接下來(lái)介紹授權(quán)及其實(shí)現(xiàn)
通過(guò) UserManager
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);
}
當(dāng)創(chuàng)建用戶或管理用戶信息時(shí),調(diào)用上述方法即可將用戶添加到指定的角色中
await AddUserToRoleAsync(user, "Administrator");
當(dāng)把用戶添加到某一角色中時(shí),如果要使某一個(gè)接口僅被指定的角色訪問(wèn),那么只要在為其添加 [Authorize] 特性時(shí)指定 Roles 屬性即可
[Authorize(Roles = "Administrator")]
public class BookController : ControllerBase
{
。。。
}
允許多個(gè)角色訪問(wèn),可通過(guò)逗號(hào)分隔角色名
[Authorize(Roles = "Administrator,Manager")]
同時(shí)需要具有多個(gè)角色才能訪問(wèn)
[Authorize(Roles = "Administrator")]
[Authorize(Roles = "Manager")]
基于 Claim 的授權(quán)則要求用戶必須具有某一個(gè)指定類型的 Claim,要實(shí)現(xiàn)基于 Claim 的授權(quán),需要?jiǎng)?chuàng)建授權(quán)策略并為其命名,然后在 [Authorize] 特性中指定 Policy 屬性
要?jiǎng)?chuàng)建授權(quán)策略,只需在 startup 中添加并配置認(rèn)證服務(wù)
services.AddMvc();
services.AddAuthorization(options =>
{
options.AddPolicy("ManagerOnly", builder => builder.RequireClaim("ManagerId"));
options.AddPolicy("LimitedUsers",
builder => builder.RequireClaim("UserId", new string[] {"1", "2", "3"}));
});
上述方法添加了兩個(gè)授權(quán)策略,ManagerOnly 要求用戶必須具有類型為 ManagerId 的 Claim,而 LimitedUsers 則要求用戶必須具有類型為 UserId 的 Claim,且它的值必須為指定的值
創(chuàng)建之后,只要在添加 [Authorize] 特性的時(shí)候指定 Policy 屬性即可
[Authorize(Policy = "ManagerOnly")]
復(fù)雜的授權(quán)策略需要通過(guò) IAuthorizationRequirement 接口和 AuthorizationHandler
實(shí)現(xiàn)只有注冊(cè)日期超過(guò)3天后才有權(quán)限訪問(wèn)
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()));
});
之后通過(guò)特性指定策略名稱即可
相關(guān)文章
《ASP.NET Core 與 RESTful API 開(kāi)發(fā)實(shí)戰(zhàn)》-- (第8章)-- 讀書(shū)筆記(上)
《ASP.NET Core 與 RESTful API 開(kāi)發(fā)實(shí)戰(zhàn)》-- (第7章)-- 讀書(shū)筆記(下)
《ASP.NET Core 與 RESTful API 開(kāi)發(fā)實(shí)戰(zhàn)》-- (第7章)-- 讀書(shū)筆記(中)
《ASP.NET Core 與 RESTful API 開(kāi)發(fā)實(shí)戰(zhàn)》-- (第7章)-- 讀書(shū)筆記(上)
《ASP.NET Core 與 RESTful API 開(kāi)發(fā)實(shí)戰(zhàn)》-- (第6章)-- 讀書(shū)筆記(下)
《ASP.ENT Core 與 RESTful API 開(kāi)發(fā)實(shí)戰(zhàn)》-- (第6章)-- 讀書(shū)筆記(上)
《ASP.ENT Core 與 RESTful API 開(kāi)發(fā)實(shí)戰(zhàn)》-- (第5章)-- 讀書(shū)筆記(下)
《ASP.ENT Core 與 RESTful API 開(kāi)發(fā)實(shí)戰(zhàn)》-- (第5章)-- 讀書(shū)筆記(中)
《ASP.ENT Core 與 RESTful API 開(kāi)發(fā)實(shí)戰(zhàn)》-- (第5章)-- 讀書(shū)筆記(上)
《ASP.ENT Core 與 RESTful API 開(kāi)發(fā)實(shí)戰(zhàn)》-- (第4章)-- 讀書(shū)筆記(下)
《ASP.ENT Core 與 RESTful API 開(kāi)發(fā)實(shí)戰(zhàn)》-- (第4章)-- 讀書(shū)筆記(上)
《ASP.ENT Core 與 RESTful API 開(kāi)發(fā)實(shí)戰(zhàn)》(第3章)-- 讀書(shū)筆記(下)
《ASP.ENT Core 與 RESTful API 開(kāi)發(fā)實(shí)戰(zhàn)》(第3章)-- 讀書(shū)筆記(中)
《ASP.ENT Core 與 RESTful API 開(kāi)發(fā)實(shí)戰(zhàn)》(第3章)-- 讀書(shū)筆記(上)
《ASP.ENT Core 與 RESTful API 開(kāi)發(fā)實(shí)戰(zhàn)》-- 讀書(shū)筆記(第2章)
《ASP.ENT Core 與 RESTful API 開(kāi)發(fā)實(shí)戰(zhàn)》-- 讀書(shū)筆記(第1章)
