Ultimate ASP.NET CORE 6.0 Web API --- 读书笔记(27)

27 JWT And Identity

本文内容来自书籍: Marinko Spasojevic - Ultimate ASP.NET Core Web API - From Zero To Six-Figure Backend Developer (2nd edition)
需要本书和源码的小伙伴可以留下邮箱,有空看到会发送的

用户验证是一个系统的重要组成部分,它关系到如何确认系统用户的身份

如果不清除验证的过程,这将会是一个艰难的工作

这个章节将会使用ASP.NET Core 的IdentityJWT来实现鉴权(authentication)和授权(authorization),一步一步将Identity整合到已有的项目中

27.1 Implementing Identity in ASP.NET Core

Identity是一个在Web应用中的会员系统,它包括了会员资格、登陆和用户数据,它包含了丰富的服务集合,帮助我们做到创建用户、密码加密、创建用户数据库模型以及所有的鉴权

首先是,需要在Entities中安装包Microsoft.AspNetCore.Identity.EntityFrameworkCore

然后,创建一个用户类

public class User : IdentityUser
{
    public string FirstName { get; set; }
    public string LastName { get; set; }
}

接着,我们需要修改RepositoryContext,修改继承的类,是因为我们需要将Identity整合到数据库的上下文中

public class RepositoryContext : IdentityDbContext<User>
{
    public RepositoryContext(DbContextOptions options) : base(options)
    {
    }

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        base.OnModelCreating(modelBuilder);
        
        modelBuilder.ApplyConfiguration(new CompanyConfiguration());
        modelBuilder.ApplyConfiguration(new EmployeeConfiguration());
    }
    
    public DbSet<Company>? Companies { get; set; }
    public DbSet<Employee>? Employees { get; set; }
}

然后,是在主项目配置服务的相关信息

public static void ConfigureIdentity(this IServiceCollection services)
{
    var builder = services.AddIdentity<User, IdentityRole>(o =>
        {
            o.Password.RequireDigit = true;
            o.Password.RequireLowercase = false;
            o.Password.RequireUppercase = false;
            o.Password.RequireNonAlphanumeric = false;
            o.Password.RequiredLength = 10;
            o.User.RequireUniqueEmail = true;
        })
        .AddEntityFrameworkStores<RepositoryContext>()
        .AddDefaultTokenProviders();
}

builder.Services.AddAuthentication();
builder.Services.ConfigureIdentity();

app.UseAuthentication();
app.UseAuthorization();

现在,基本已经配置好了所需的信息,最后,因为用户的信息是用EF Core存储在数据库的,所以我们还需要做一次迁移和数据库架构的更新,然后就可以看到数据库中,看到自动创建的一些关于鉴权和授权的表

数据库准备好了之后,我们可以插入一些角色到AspNetRoles,因为是code first风格的,所以需要先建立配置或者模型,然后迁移并更新数据库架构

我们在Repository配置这些信息,并将新的配置加入到Context的初始化中

public class RoleConfiguration : IEntityTypeConfiguration<IdentityRole>
{
    public void Configure(EntityTypeBuilder<IdentityRole> builder)
    {
        builder.HasData(
            new IdentityRole
            {
                Name = "Manager",
                NormalizedName = "MANAGER"
            },
            new IdentityRole
            {
                Name = "Administrator",
                NormalizedName = "ADMINISTRATOR"
            }
        );
    }
}

// RepositoryContext
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    base.OnModelCreating(modelBuilder);
    
    modelBuilder.ApplyConfiguration(new CompanyConfiguration());
    modelBuilder.ApplyConfiguration(new EmployeeConfiguration());
    modelBuilder.ApplyConfiguration(new RoleConfiguration());
}

然后迁移并更新数据库架构,在表中就可以查看到新增的两个角色

27.3 User Creation

创建或者注册一个用户,需要有一个这样的API

[Route("api/authentication")]
[ApiController]
public class AuthenticationController : ControllerBase
{
    private readonly IServiceManager _service;
    public AuthenticationController(IServiceManager service) => _service = service;
}

创建一个DTO接收注册信息

public record UserForRegistrationDto
{
    public string? FirstName { get; init; }
    public string? LastName { get; init; }

    [Required(ErrorMessage = "Username is required")]
    public string? UserName { get; init; }

    [Required(ErrorMessage = "Password is required")]
    public string? Password { get; init; }

    public string? Email { get; init; }
    public string? PhoneNumber { get; init; }
    public ICollection<string>? Roles { get; init; }
}

创建自动映射关系

CreateMap<UserForRegistrationDto, User>();

创建一个处理注册或者鉴权信息的服务接口

public interface IAuthenticationService
{
    Task<IdentityResult> RegisterUser(UserForRegistrationDto userForRegistration);
}

然后是实现,将用户信息和角色,放到数据库,下面的做法是在成功将用户数据插入数据库之后,将角色信息插入到表中,不过也可以在AddToRolesAsync之前,检查这个角色是否存在或合法,不过检查需要依赖注入一个角色管理类RoleManager<TRole>,然后使用RoleExistsAsync方法

internal sealed class AuthenticationService : IAuthenticationService
{
    private readonly ILoggerManager _logger;
    private readonly IMapper _mapper;
    private readonly UserManager<User> _userManager;
    private readonly IConfiguration _configuration;

    public AuthenticationService(ILoggerManager logger, IMapper mapper,
        UserManager<User> userManager, IConfiguration configuration)
    {
        _logger = logger;
        _mapper = mapper;
        _userManager = userManager;
        _configuration = configuration;
    }

    public async Task<IdentityResult> RegisterUser(UserForRegistrationDto userForRegistration)
    {
        var user = _mapper.Map<User>(userForRegistration);
        var result = await _userManager.CreateAsync(user, userForRegistration.Password);
        if (result.Succeeded)
            await _userManager.AddToRolesAsync(user, userForRegistration.Roles);
        return result;
    }
}

服务有了之后,需要注册到ServiceManager

public interface IServiceManager
{
    ICompanyService CompanyService { get; }
    IEmployeeService EmployeeService { get; }
    
    IAuthenticationService AuthenticationService { get; }
}

public sealed class ServiceManager : IServiceManager
{
    private readonly IMapper _mapper;
    private readonly Lazy<ICompanyService> _companyService;
    private readonly Lazy<IEmployeeService> _employeeService;
    private readonly Lazy<IAuthenticationService> _authenticationService;

    public ServiceManager(IRepositoryManager repositoryManager, ILoggerManager logger, IMapper mapper,
        UserManager<User> userManager,
        IConfiguration configuration)
    {
        _mapper = mapper;
        _companyService = new Lazy<ICompanyService>(() => new CompanyService(repositoryManager, logger, mapper));
        _employeeService = new Lazy<IEmployeeService>(() => new EmployeeService(repositoryManager, logger, mapper));
        _authenticationService =
            new Lazy<IAuthenticationService>(
                () => new AuthenticationService(logger, mapper, userManager, configuration));
    }

    public ICompanyService CompanyService => _companyService.Value;

    public IEmployeeService EmployeeService => _employeeService.Value;

    public IAuthenticationService AuthenticationService => _authenticationService.Value;
}

最后,我们就可以在控制器使用服务注册用户

[Route("api/authentication")]
[ApiController]
public class AuthenticationController : ControllerBase
{
    private readonly IServiceManager _service;
    public AuthenticationController(IServiceManager service) => _service = service;

    [HttpPost]
    [ServiceFilter(typeof(ValidationFilterAttribute))]
    public async Task<IActionResult> RegisterUser([FromBody] UserForRegistrationDto userForRegistration)
    {
        var result = await _service.AuthenticationService.RegisterUser(userForRegistration);

        if (result.Succeeded) return StatusCode(201);

        foreach (var error in result.Errors)
        {
            ModelState.TryAddModelError(error.Code, error.Description);
        }

        return BadRequest(ModelState);
    }
}

27.4 Big Picture

在实现鉴权和授权之前,快速总览一下功能。

系统应该有一个登陆的页面,当用户输入用户名和密码之后,客户端(浏览器)就会将用户的信息发送到服务器的API端点

然后服务器在验证了用户的证件并确认用户是合法的之后,会返回一个编码的JWT给客户端。

一个JWT其实是一个JavaScript的对象,然后包含着一些已经登陆的用户的属性,比如用户名,用户主题,角色,还有一些有用的信息

27.5 About JWT

JWT(JSON Web Token)并没有加密,它可以使用base64解码,所以在payload中包含任何敏感信息

JWT又三个基础部分组成:

  • header,一个JSON对象并使用base64格式编码,这部分是标准实现,不需要关心这个部分,它包含了这个token的类型和使用签名算法
  • payload,同样是一个JSON对象并使用base64格式编码,内部包含关于用户的一些基础信息
  • signature,这是个数字签名,由headerpayload组合在一起生成的,还有就是它是基于一个secret关键字加密,并且只有服务端知道。通常,服务端使用签名来校验token是否合法

所以,当恶意用户试图去修改payload的内容的时候,他们需要重新生成签名,如果要生成签名,还必须要拥有secret关键字,但是这个只有服务端知道。而在服务端,我们可以很容易就检查出内容是否被修改了,我们只需要根据客户端发送上来的headerpayload重新计算生成签名,然后和客户端发送上来的签名进行比较,如果有内容被修改了,那么签名就不一致了

27.6 JWT Configuration

首先,需要增加JWT相关基础信息配置,在appsettings.json中添加

{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning"
    }
  },
  "AllowedHosts": "*",
  "ConnectionStrings": {
    "sqlConnection": "Data Source=my.db"
  },
  "JwtSettings": {
    "validIssuer": "CodeMazeAPI",
    "validAudience": "https://localhost:5001"
  }
}

除了有这些基础信息,前面说过,还需要一个secret,然后我们并没有将其写入我们的项目中,而是通过环境变量的方式导入系统,这样会更加安全

SECRET "CodeMazeSecretKey",设置环境变量的方式很多,不同的OS也不一样,所以主要约定好,给出的key是什么就好了

然后,我们就需要在配置文件中,读取JWT的相关信息,这里配置成扩展方法,而且还需要安装包
Microsoft.AspNetCore.Authentication.JwtBearer

public static void ConfigureJWT(this IServiceCollection services, IConfiguration configuration)
    {
        var jwtSettings = configuration.GetSection("JwtSettings");
        var secretKey = Environment.GetEnvironmentVariable("SECRET");
        services.AddAuthentication(opt =>
            {
                opt.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
                opt.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
            })
            .AddJwtBearer(options =>
            {
                options.TokenValidationParameters = new TokenValidationParameters
                {
                    ValidateIssuer = true,
                    ValidateAudience = true,
                    ValidateLifetime = true,
                    ValidateIssuerSigningKey = true,
                    ValidIssuer = jwtSettings["validIssuer"],
                    ValidAudience = jwtSettings["validAudience"],
                    IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(secretKey))
                };
            });
    }

builder.Services.ConfigureJWT(builder.Configuration);

这个配置使用AddAuthentication注册服务,然后提供几个需要JWT验证的属性,这里提供的属性和客户端提供的做比较校验

27.7 Protecting Endpoints

我们需要在需要保护起来的controller或者action上添加特性[Authorize],这样的情况下,我们就不能访问这个方法了,响应是401 Unauthorzied

27.8 Implementing Authentication

开始准备验证的API,首先需要一个DTO

public record UserForAuthenticationDto
{
    [Required(ErrorMessage = "User name is required")]
    public string? UserName { get; init; }

    [Required(ErrorMessage = "Password name is required")]
    public string? Password { get; init; }
}

然后,给我们的验证服务添加两个接口

public interface IAuthenticationService
{
    Task<IdentityResult> RegisterUser(UserForRegistrationDto userForRegistration);

    Task<bool> ValidateUser(UserForAuthenticationDto userForAuth);

    Task<string> CreateToken();
}

在实现这个接口之前,需要安装一个包System.IdentityModel.Tokens.Jwt,然后是实现

public async Task<bool> ValidateUser(UserForAuthenticationDto userForAuth)
{
    _user = await _userManager.FindByNameAsync(userForAuth.UserName);
    var result = _user != null && await _userManager.CheckPasswordAsync(_user, userForAuth.Password);
    if (!result)
        _logger.LogWarn($"{nameof(ValidateUser)}: Authentication failed. Wrong user name or password.");
    return result;
}

public async Task<string> CreateToken()
{
    var signingCredentials = GetSigningCredentials();
    var claims = await GetClaims();
    var tokenOptions = GenerateTokenOptions(signingCredentials, claims);
    return new JwtSecurityTokenHandler().WriteToken(tokenOptions);
}

private SigningCredentials GetSigningCredentials()
{
    var key = Encoding.UTF8.GetBytes(Environment.GetEnvironmentVariable("SECRET") ?? "asdnAAjhaskdijhciwn");
    var secret = new SymmetricSecurityKey(key);
    return new SigningCredentials(secret, SecurityAlgorithms.HmacSha256);
}

private async Task<List<Claim>> GetClaims()
{
    var claims = new List<Claim>
    {
        new(ClaimTypes.Name, _user.UserName)
    };

    var roles = await _userManager.GetRolesAsync(_user);

    foreach (var role in roles)
    {
        claims.Add(new Claim(ClaimTypes.Role, role));
    }

    return claims;
}

private JwtSecurityToken GenerateTokenOptions(SigningCredentials signingCredentials, IEnumerable<Claim> claims)
{
    var jwtSettings = _configuration.GetSection("JwtSettings");
    var tokenOptions = new JwtSecurityToken
    (
        issuer: jwtSettings["validIssuer"],
        audience: jwtSettings["validAudience"],
        claims: claims,
        expires: DateTime.Now.AddMinutes(Convert.ToDouble(jwtSettings["expires"])),
        signingCredentials: signingCredentials
    );
    return tokenOptions;
}

一共新增两个方法:一个验证用户,一个创建token;最后,添加一个action,登录,然后验证,通过就下发token,而验证这个token我们在之前的配置Identity的时候,已经约定好了。

也就是说,ASP.NET Core Identity,只是帮我们做了token的验证,用户信息管理以及授权的功能,至于token的下发、用户的登录,需要我们自己做

[HttpPost("login")]
[ServiceFilter(typeof(ValidationFilterAttribute))]
public async Task<IActionResult> Authenticate([FromBody] UserForAuthenticationDto user)
{
    if (!await _service.AuthenticationService.ValidateUser(user)) return Unauthorized();

    return Ok(new { Token = await _service.AuthenticationService.CreateToken() });
}

27.9 Role-Based Authorization

如果我们需要某个API只能由某个角色访问,那么只需要在
[Authorize(Roles = "Manager")],添加允许访问的角色即可

posted @   huang1993  阅读(143)  评论(3编辑  收藏  举报
相关博文:
阅读排行:
· DeepSeek 开源周回顾「GitHub 热点速览」
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· .NET 10首个预览版发布:重大改进与新特性概览!
· AI与.NET技术实操系列(二):开始使用ML.NET
· 单线程的Redis速度为什么快?
点击右上角即可分享
微信分享提示