前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >《ASP.NET Core 与 RESTful API 开发实战》-- (第8章)-- 读书笔记(中)

《ASP.NET Core 与 RESTful API 开发实战》-- (第8章)-- 读书笔记(中)

作者头像
郑子铭
发布2021-01-13 15:45:39
8600
发布2021-01-13 15:45:39
举报
文章被收录于专栏:DotNet NB && CloudNative

第 8 章 认证和安全

8.2 ASP.NET Core Identity

Identity 是 ASP.NET Core 中提供的对用户和角色等信息进行存储与管理的系统

Identity 由3层构成,最底层为 Store 层,即存储层,包含 IUserStore 接口与 IRoleStore 接口

IUserStore 接口定义如下:

代码语言:javascript
复制
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

代码语言:javascript
复制
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 的构造函数如下:

代码语言:javascript
复制
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 相关的数据表

代码语言:javascript
复制
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 类型参数是用户表与角色表主键字段的类型

代码语言:javascript
复制
public class LibraryDbContext : IdentityDbContext<User, Role, string>
{
    。。。
}

需要添加 nuget 包:Microsoft.AspNetCore.Identity.EntityFrameworkCore

接下来,在 startup 中添加 Identity 服务

代码语言:javascript
复制
services.AddIdentity<User, Role>()
.AddEntityFrameworkStores<LibraryDbContext>();

AddIdentity 方法会向容器添加 UserManager、RoleManager,以及它们所依赖的服务,并且会添加 Identity 用到的 Cookie 认证

AddEntityFrameworkStores 方法会将 EF Core 中对 IUserStore 接口和 IroleStore 接口的实现添加到容器中

添加 Identity 服务后,还应修改添加 DbContext 服务的代码为

代码语言:javascript
复制
services.AddDbContext<LibraryDbContext>(
config => config.UseSqlServer(Configuration.GetConnectionString("DefaultConnection"),
    optionBuilder => optionBuilder.MigrationsAssembly(typeof(Startup).Assembly.GetName().Name)));

MigrationsAssembly 方法为当前 DbContext 设置其迁移所在的程序集名称,这是由于 DbContext 与为其创建的迁移并不在同一个程序集中

接着,运行以下命令

代码语言:javascript
复制
Add-Migration AddIdentity
Update-Database

上述命令会创建一个名为 AddIdentity 的 EF Core 迁移,该迁移包含了创建与 Identity 相关的数据表操作,并将其修改应用到数据库中

接下来,在 AuthenticateController 中添加创建用户的方法,并修改原来对用户信息验证的逻辑

首先创建 RegisterUser 类,在创建用户时,请求中的信息将会反序列化为此类型

代码语言:javascript
复制
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 方法,用于创建用户

代码语言:javascript
复制
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 的方法

代码语言:javascript
复制
[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 创建相应的角色

代码语言:javascript
复制
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);
}

当创建用户或管理用户信息时,调用上述方法即可将用户添加到指定的角色中

代码语言:javascript
复制
await AddUserToRoleAsync(user, "Administrator");

当把用户添加到某一角色中时,如果要使某一个接口仅被指定的角色访问,那么只要在为其添加 [Authorize] 特性时指定 Roles 属性即可

代码语言:javascript
复制
[Authorize(Roles = "Administrator")]
public class BookController : ControllerBase
{
    。。。
}

允许多个角色访问,可通过逗号分隔角色名

代码语言:javascript
复制
[Authorize(Roles = "Administrator,Manager")]

同时需要具有多个角色才能访问

代码语言:javascript
复制
[Authorize(Roles = "Administrator")]
[Authorize(Roles = "Manager")]

基于 Claim 的授权则要求用户必须具有某一个指定类型的 Claim,要实现基于 Claim 的授权,需要创建授权策略并为其命名,然后在 [Authorize] 特性中指定 Policy 属性

要创建授权策略,只需在 startup 中添加并配置认证服务

代码语言:javascript
复制
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 属性即可

代码语言:javascript
复制
[Authorize(Policy = "ManagerOnly")]

复杂的授权策略需要通过 IAuthorizationRequirement 接口和 AuthorizationHandler 类实现

实现只有注册日期超过3天后才有权限访问

代码语言:javascript
复制
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 中即可

代码语言:javascript
复制
services.AddAuthorization(options =>
{
    options.AddPolicy("RegisteredMoreThen3DaysRequirement",
        builder => builder.Requirements.Add(new RegisteredMoreThen3DaysRequirement()));
});

之后通过特性指定策略名称即可

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2020-08-12,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 DotNet NB 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 第 8 章 认证和安全
    • 8.2 ASP.NET Core Identity
    相关产品与服务
    对象存储
    对象存储(Cloud Object Storage,COS)是由腾讯云推出的无目录层次结构、无数据格式限制,可容纳海量数据且支持 HTTP/HTTPS 协议访问的分布式存储服务。腾讯云 COS 的存储桶空间无容量上限,无需分区管理,适用于 CDN 数据分发、数据万象处理或大数据计算与分析的数据湖等多种场景。
    领券
    问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档