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 的Identity
和JWT
来实现鉴权(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,这是个数字签名,由
header
和payload
组合在一起生成的,还有就是它是基于一个secret
关键字加密,并且只有服务端知道。通常,服务端使用签名来校验token是否合法
所以,当恶意用户试图去修改payload
的内容的时候,他们需要重新生成签名,如果要生成签名,还必须要拥有secret
关键字,但是这个只有服务端知道。而在服务端,我们可以很容易就检查出内容是否被修改了,我们只需要根据客户端发送上来的header
和payload
重新计算生成签名,然后和客户端发送上来的签名进行比较,如果有内容被修改了,那么签名就不一致了
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")]
,添加允许访问的角色即可
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· DeepSeek 开源周回顾「GitHub 热点速览」
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· .NET 10首个预览版发布:重大改进与新特性概览!
· AI与.NET技术实操系列(二):开始使用ML.NET
· 单线程的Redis速度为什么快?