文章目录
项目地址
- 教程作者:
- 教程地址:
- 代码仓库地址:
- 所用到的框架和插件:
dbt
airflow
一、Authentication(身份认证)
回答“你是谁”的问题,用来判断访问API的用户是什么角色
1.1 配置环境(解决类库包无法引用)/启用Identity
1.1.1 安装Identity
Restaurants.Domain
层安装
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="8.0.11" />
1.1.2 初始化Identity数据服务
- 在
Restaurants.Domain/Entities/User.cs
里创建User实体,用来继承IdentityUser的,里面包含了一些用户管理的基础信息如:UserName,Email,等
using Microsoft.AspNetCore.Identity;
namespace Restaurants.Domain.Entities
{
public class User:IdentityUser
{
}
}
- 更改
Restaurants.Infrastructure/Persistence/RestaurantsDbContext.cs
之前的DbContext,使用IdentityDbContext
- 在
Restaurants.Infrastructure/Extensions/ServiceCollectionExtensions.cs
里注册AddIdentityApiEndpoints
services.AddIdentityApiEndpoints<User>()
.AddEntityFrameworkStores<RestaurantsDbContext>();
- 在引入
AddIdentityApiEndpoints
时,一直无法引入,原因是:在类库项目中不能直接引用WebApplicationBuilder、ApplicationBuilder等类,这些类位于Microsoft.ASPNetCore程序集中,但是无法通过Nuget包引用
- 在程序入口注册服务
Program.cs
app.MapIdentityApi<User>();
- EF将User表写入数据库在类库
Restaurants.Infrastructure
下
add-migration IdentityAdded
update-database
- 迁移成功后,数据库里就有了权限表
- 发送一个post请求,注册一个用户
1.1.3 将Identity服务添加到Swagger里面
- 在
Program.cs
里添加服务,让Identity的Api显示在Swagger里
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllers();
- Swagger里出现服务,但是这些路由地址都是默认的
1.1.4 更改Identity的路由路径
- 上面的问题是 identity的路由路径都是默认的,我们想自定义 路由路径:
1.1.5 将所有关于Identity的服务集中注册
- 创建
Restaurants.API/Extensions/WebApplicationBuilderExtensions.cs
在Api层
using Microsoft.OpenApi.Models;
using Restaurants.API.Middlewares;
using Serilog;
namespace Restaurants.API.Extensions;
public static class WebApplicationBuilderExtensions
{
public static void AddPresentation(this WebApplicationBuilder builder)
{
builder.Services.AddAuthentication();
builder.Services.AddControllers();
builder.Services.AddSwaggerGen(c =>
{
c.AddSecurityDefinition("bearerAuth", new OpenApiSecurityScheme
{
Type = SecuritySchemeType.Http,
Scheme = "Bearer"
});
c.AddSecurityRequirement(new OpenApiSecurityRequirement
{
{
new OpenApiSecurityScheme
{
Reference = new OpenApiReference { Type = ReferenceType.SecurityScheme, Id = "bearerAuth"}
},
[]
}
});
});
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddScoped<ErrorHandlingMiddleware>();
builder.Services.AddScoped<RequestTimeLoggingMiddleware>();
builder.Host.UseSerilog((context, configuration) =>
configuration.ReadFrom.Configuration(context.Configuration)
);
}
}
1.2 使用Authentication控制Controller的访问
原理:通过在请求头里添加验证信息来控制
- 在
Restaurants.API
层的Extensions文件夹
里注册服务,并
2. 使用
UseAuthorization()
中间件,这样在请求头的token里就会有
app.MapGroup("api/identity")
.MapIdentityApi<User>();
app.UseAuthorization();
app.MapControllers();
app.Run();
- 在需要添加验证的Controller类上面添加
[Authorize]
,如果类里有不需要验证就可以访问的api,在该controller上添加[AllowAnonymous]
3. 设置成功后,当访问https://localhost:7044/api/restaurants/1
报错401Unauthorized
; 但是访问https://localhost:7044/api/restaurants
可以获取所有restaurants的列表
1.3 获取User的Context
在程序的任何地方获取用户信息
1.3.1 在Application下创建User文件夹
- 创建User文件用来管理和获取权限中的User信息
1. 创建CurrentUser
创建Restaurants.Application/User/CurrentUser.cs
,将用户的Id, Email , Roles,封装到CurrentUser类里,并且通过IsInRole方法,检查用户的角色集合中是否包含指定的角色。返回true 或者 false
namespace Restaurants.Application.User
{
public record CurrentUser(string Id, string Email, IEnumerable<string> Roles)
{
public bool IsInRole(string role) => Roles.Contains(role);
}
}
2. 创建UserContext.cs
提供接口给程序使用
- 创建
Restaurants.Application/User/UserContext.cs
文件 - 从当前 HTTP 请求的上下文中获取用户的身份信息(CurrentUser),并提供一个接口 IUserContext 供应用程序使用。
- 主要逻辑:
- 获取
IHttpContextAccessor
服务httpContextAccessor
; - 通过
httpContextAccessor
服务获取到HttpContext
的User信息; - 判断user信息,通过Claim里的数据进行查询,获取用户信息;
- 将获取的信息,封装成为:
new一个CurrentUser
类; - 创建一个
IUserContext
接口,该接口的功能,作用时注册到服务里,这样程序的任何地方只需要获取服务就可以获得CurrentUser的信息;
- 获取
using Microsoft.AspNetCore.Http;
using System.Security.Claims;
namespace Restaurants.Application.User
{
public interface IUserContext
{
CurrentUser? GetCurrentUser();
}
public class UserContext : IUserContext
{
private readonly IHttpContextAccessor httpContextAccessor;
public UserContext(IHttpContextAccessor httpContextAccessor)
{
this.httpContextAccessor = httpContextAccessor;
}
public CurrentUser? GetCurrentUser()
{
var user = httpContextAccessor?.HttpContext?.User;
if (user == null)
{
throw new InvalidOperationException("User context is not present");
}
if (user.Identity == null || !user.Identity.IsAuthenticated)
{
return null;
}
var userId = user.FindFirst(c => c.Type == ClaimTypes.NameIdentifier)!.Value;
var email = user.FindFirst(c => c.Type == ClaimTypes.Email)!.Value;
var roles = user.Claims.Where(c => c.Type == ClaimTypes.Role)!.Select(c => c.Value);
return new CurrentUser(userId, email, roles);
}
}
}
3. 注册IUserContext接口到服务里
- 在Application的Extensions文件里注册 IUserContext的接口
1.4 添加自己属性在User里
1.4.1 给dbo.AspNetUsers添加字段
- 现在我们的User使用的Identity自己定义,如果我们需要给User添加例如生日,国籍,等其他信息,就需要扩展
- Domian的Entities(Model)层里创建User.cs,表示自定义的User,继承IdentityUser的接口
using Microsoft.AspNetCore.Identity;
namespace Restaurants.Domain.Entities
{
public class User:IdentityUser
{
public DateOnly? DateOfBirth { get; set; }
public string? Nationality { get; set; }
}
}
- 在Infrustructure层执行迁移,添加的字段加入到表里
1.4.2 添加IdentityController
- 创建
IdentitiesController.cs
控制器,用来处理IdentityUser的增删改查
using MediatR;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Restaurants.Application.Users.Commands.UpdateUserDetials;
namespace Restaurants.API.Controllers
{
[ApiController]
[Route("api/identity")]
public class IdentitiesController: ControllerBase
{
private readonly IMediator mediator;
public IdentitiesController(IMediator mediator)
{
this.mediator = mediator;
}
[HttpPatch("user")]
[Authorize]
public async Task<ActionResult> UpdateUserDetails(UpdateUserDetailsCommand command)
{
await mediator.Send(command);
return NoContent();
}
}
}
1.4.3 添加更新用户的Command和Handler
1. Command
- 添加
UpdateUserDetailsCommand.cs
文件,表示需要更新需要传递的属性
using MediatR;
namespace Restaurants.Application.Users.Commands.UpdateUserDetials
{
public class UpdateUserDetailsCommand : IRequest
{
public DateOnly? DateOfBirth { get; set; }
public string? Nationality { get; set; }
public UpdateUserDetailsCommand(DateOnly dateOfBirth, string nationality)
{
DateOfBirth = dateOfBirth;
Nationality = nationality;
}
}
}
2. handler
- 创建
UpdateUserDetailsCommandHandler.cs
需要注意的是:- 获取的是
private readonly IUserStore<User> userStore;
服务; - 从
IUserStore<User> userStore
里获取当前登录的用户信息;
- 获取的是
using MediatR;
using Microsoft.AspNetCore.Identity;
using Microsoft.Extensions.Logging;
using Restaurants.Domain.Entities;
using Restaurants.Domain.Exceptions;
namespace Restaurants.Application.Users.Commands.UpdateUserDetials
{
public class UpdateUserDetailsCommandHandler : IRequestHandler<UpdateUserDetailsCommand>
{
private readonly ILogger<UpdateUserDetailsCommandHandler> logger;
private readonly IUserContext userContext;
//是 ASP.NET Core Identity 提供的一个接口,定义了用于管理用户实体的基础存储操作。它是实现用户数据存储和管理的核心接口之一
private readonly IUserStore<User> userStore;
public UpdateUserDetailsCommandHandler
(
ILogger<UpdateUserDetailsCommandHandler> logger,
IUserContext userContext,
IUserStore<User> userStore
)
{
this.logger = logger;
this.userContext = userContext;
this.userStore = userStore;
}
public async Task Handle(UpdateUserDetailsCommand request, CancellationToken cancellationToken)
{
logger.LogInformation("Update user details command handler");
//1.获取当前用户
var user = userContext.GetCurrentUser();
//2.根据用户Id查找用户
var dbUser = await userStore.FindByIdAsync(user!.Id, cancellationToken);
if (dbUser == null)
{
throw new NotFoundException("User not found");
}
//3.更新用户信息
dbUser.DateOfBirth = request.DateOfBirth;
dbUser.Nationality = request.Nationality;
//4.更新数据库
await userStore.UpdateAsync(dbUser, cancellationToken);
}
}
}
二、Authorization(授权)
回答“回答的是“你能做什么”的问题”
2.1 Role Base Authorization
2.1 添加角色
- 在餐厅系统里,我们需要添加三个不同的角色:用户(user),餐厅老板(Owner),以及管理员(admin) ,在
Restaurants.Domain/Constants/UserRoles.cs
添加角色
namespace Restaurants.Domain.Constants
{
public static class UserRoles
{
public const string Admin = "Admin";
public const string User = "User";
public const string Owner = "Owner";
}
}
- 将三个角色通过Seed添加到
dbo.AspNetRoles
表里
private IEnumerable<IdentityRole> GetRoles()
{
List<IdentityRole> roles =
[
new (UserRoles.User)
{
NormalizedName = UserRoles.User.ToUpper()
},
new (UserRoles.Owner)
{
NormalizedName = UserRoles.Owner.ToUpper()
},
new (UserRoles.Admin)
{
NormalizedName = UserRoles.Admin.ToUpper()
},
];
return roles;
}
- 添加成功后,
5. 通过注册接口,将三个角色注册https://localhost:7044/api/identity/register
{
"email":"user1@test.com",
"password":"Password1!"
}
- 给每个用户分配权限在
AspNetUserRoles
表,手动插入
insert into AspNetUserRoles
(UserId,RoleId)
VALUES
('aa18d542-ec4c-4d53-a709-f087a0218ee9','23fa674e-405b-4c5d-928f-d2aa3bfdd9f6'),
('30ad7ebe-6fb6-45fc-9ca4-d09c83df78e8','f7df975a-48dc-44b9-b047-619528fea585'),
('6cd69086-109f-4071-bc10-b528056a76f0','7bc860f9-b469-4df1-bc3f-ac7e1a459681')
2.2 根据角色分配功能
2.2.1 简单的授权
- 创建
CreateRestaurant
控制器上分配,只有Owner权限的人才可以访问该Api
[HttpPost]
[Authorize(Roles = UserRoles.Owner)]
public async Task<IActionResult> CreateRestaurant([FromBody] CreateRestaurantCommand command)
{
int id = await mediator.Send(command);
return CreatedAtAction(nameof(GetById), new { id }, null);
}
- 登录之后,创建一个restaurants,发送请求
{
"Name": "Tasty Tests3",
"Description": "A cozy restaurant serving a variety of delicious dishes.",
"Category": "Indian",
"HasDelivery": true,
"ContactEmail": "info@test.com",
"ContactNumber": "555-1234",
"City": "New York",
"Street": "123 Main Street",
"PostalCode": "10-010"
}
- 此时,还是403,因为我们没有给服务添加AddRoles,在
Infrastructure/Extensions/ServiceCollectionExtensions.cs
里,注册服务
2.3 添加AssignUserRole权限用户的api
2.3.1 添加AssignUserRole管理用户的controller
- 创建
Restaurants.API/Controllers/IdentityController.cs
,用于添加用户权限
[HttpPost("userRole")]
[Authorize(Roles = UserRoles.Admin)]
public async Task<IActionResult> AssignUserRole(AssignUserRoleCommand command)
{
await mediator.Send(command);
return NoContent();
}
1. Command
Restaurants.Application/Users/Commands/AssignUserRole/AssignUserRoleCommand.cs
using MediatR;
namespace Restaurants.Application.Users.Commands.AssignUserRole;
public class AssignUserRoleCommand : IRequest
{
public string UserEmail { get; set; } = default!;
public string RoleName { get; set; } = default!;
}
2.Handler
- 创建
Restaurants.Application/Users/Commands/AssignUserRole/AssignUserRoleCommandHandler.cs
,其中UserManager<User>
和RoleManager<IdentityRole> roleManager
是直接从系统服务里获取,无需自己创建服务,可以直接进行数据库的读写;
using MediatR;
using Microsoft.AspNetCore.Identity;
using Microsoft.Extensions.Logging;
using Restaurants.Domain.Entities;
using Restaurants.Domain.Exceptions;
namespace Restaurants.Application.Users.Commands.AssignUserRole;
public class AssignUserRoleCommandHandler(ILogger<AssignUserRoleCommandHandler> logger,
UserManager<User> userManager,
RoleManager<IdentityRole> roleManager) : IRequestHandler<AssignUserRoleCommand>
{
public async Task Handle(AssignUserRoleCommand request, CancellationToken cancellationToken)
{
logger.LogInformation("Assigning user role: {@Request}", request);
var user = await userManager.FindByEmailAsync(request.UserEmail)
?? throw new NotFoundException(nameof(User), request.UserEmail);
var role = await roleManager.FindByNameAsync(request.RoleName)
?? throw new NotFoundException(nameof(IdentityRole), request.RoleName);
await userManager.AddToRoleAsync(user, role.Name!);
}
}
2.3.2 测试功能
- 使用admin账号登录
http://localhost:5090/api/identity/login
{
"email": "admin@test.com",
"password": "Password1!"
}
- 将token复制到
http://localhost:5090/api/identity/userRole
里,发送post请求,给test用户分配到User用户组;
{
"userEmail": "testuser@test.com",
"roleName": "User"
}
- 发送成功后,AspNetUserRoles表里就会看到用户id和对应的role的id
2.4 UnassignUserRole删除权限
2.4.1 UnassignUserRole删除权限的Controller
Restaurants.API/Controllers/IdentityController.cs
[HttpDelete("userRole")]
[Authorize(Roles = UserRoles.Admin)]
public async Task<IActionResult> UnassignUserRole(UnassignUserRoleCommand command)
{
await mediator.Send(command);
return NoContent();
}
1. Command
Restaurants.Application/Users/Commands/UnassignUserRole/UnassignUserRoleCommand.cs
using MediatR;
namespace Restaurants.Application.Users.Commands.UnassignUserRole;
public class UnassignUserRoleCommand : IRequest
{
public string UserEmail { get; set; } = default!;
public string RoleName { get; set; } = default!;
}
2. Handler
Restaurants.Application/Users/Commands/UnassignUserRole/UnassignUserRoleCommandHandler.cs
using MediatR;
using Microsoft.AspNetCore.Identity;
using Microsoft.Extensions.Logging;
using Restaurants.Application.Users.Commands.AssignUserRole;
using Restaurants.Domain.Entities;
using Restaurants.Domain.Exceptions;
namespace Restaurants.Application.Users.Commands.UnassignUserRole;
public class UnassignUserRoleCommandHandler(ILogger<UnassignUserRoleCommandHandler> logger,
UserManager<User> userManager,
RoleManager<IdentityRole> roleManager) : IRequestHandler<UnassignUserRoleCommand>
{
public async Task Handle(UnassignUserRoleCommand request, CancellationToken cancellationToken)
{
logger.LogInformation("Unassigning user role: {@Request}", request);
var user = await userManager.FindByEmailAsync(request.UserEmail)
?? throw new NotFoundException(nameof(User), request.UserEmail);
var role = await roleManager.FindByNameAsync(request.RoleName)
?? throw new NotFoundException(nameof(IdentityRole), request.RoleName);
await userManager.RemoveFromRoleAsync(user, role.Name!);
}
}
2.5 Attribute Base
- 存在问题:现有的用户的验证只有一些很基础的,例如用户名,用户密码,角色等。如果,需要验证其他的,例如用户的国籍,用户的年龄才可以访问页面,就需要添加自定义的属性;
例如:给user1 添加了属性Nationality
和DateOfBirth
,判断之后添加这两个属性到程序Identity里,之后,我们可以通过判断用户是否有国际,或者生日计算的年龄,判断用户是否可以访问api;检查用户是否有 Nationality 和 DateOfBirth 属性。如果存在,向用户的声明列表中添加自定义声明;
2.5.1 添加自定义属性给身份验证
通过继承
UserClaimsPrincipalFactory
,override里面的方法实现
- 创建
Restaurants.Infrastructure/Authorization/RestaurantsUserClaimsPrincipalFactory.cs
,继承系统的工厂方法,使用override重写里面的功能
using Microsoft.AspNetCore.Identity;
using Microsoft.Extensions.Options;
using Restaurants.Domain.Entities;
using System.Security.Claims;
namespace Restaurants.Infrastructure.Authorization
{
public class RestaurantsUserClaimsPrincipalFactory : UserClaimsPrincipalFactory<User, IdentityRole>
{
private readonly UserManager<User> userManager;
private readonly RoleManager<IdentityRole> roleManager;
private readonly IOptions<IdentityOptions> options;
public RestaurantsUserClaimsPrincipalFactory
(
UserManager<User> userManager,
RoleManager<IdentityRole> roleManager,
IOptions<IdentityOptions> options
) : base(userManager, roleManager, options)
{
this.userManager = userManager;
this.roleManager = roleManager;
this.options = options;
}
public override async Task<ClaimsPrincipal> CreateAsync(User user)
{
var id = await GenerateClaimsAsync(user);
if (user.Nationality != null)
{
id.AddClaim(new Claim("Nationality", user.Nationality));
}
if (user.DateOfBirth != null)
{
id.AddClaim(new Claim("DateOfBirth", user.DateOfBirth.Value.ToString("yyyy-MM-dd")));
}
return new ClaimsPrincipal(id);
}
}
}
- 在
Restaurants.Infrastructure/Extensions/ServiceCollectionExtensions.cs
里注册自定义的Claim
services.AddIdentityApiEndpoints<User>()
.AddRoles<IdentityRole>()
.AddClaimsPrincipalFactory<RestaurantsUserClaimsPrincipalFactory>()
.AddEntityFrameworkStores<RestaurantsDbContext>();
- 如果登录用户这两个属性有值的话,我们就会在token里看到;例如,管理员和owner在创建的时候没有写生日和国籍,普通用户需要在注册的时候写,这是,普通用户的token里,就有了这两个属性;
2.5.2 添加claim属性验证,对国籍进行认证
- 添加对国籍进行认证的属性,只有Nationality属性有值的用户才可以访问特定页面
Restaurants.Infrastructure/Extensions/ServiceCollectionExtensions.cs
services.AddAuthorizationBuilder()
.AddPolicy("HasNationnality", builder => builder.RequireClaim("Nationality"));
- 给指定的controller添加验证
2.5.3 封装认证的Policy
存在问题:现在认证的属性是hard code直接写成字符串在程序里,需要将他封装起来
- 创建
Restaurants.Infrastructure/Authorization/Constants.cs
用于存放PolicyName认证的名称和认证的字段
namespace Restaurants.Infrastructure.Authorization
{
//方法名
public static class PolicyNames
{
public const string HasNationality = "HasNationality";
}
//字段名
public static class AppClaimTypes
{
public const string Nationality = "Nationality";
public const string DateOfBirth = "DateOfBirth";
}
}
- 使用封装的名称
services.AddAuthorizationBuilder()
//.AddPolicy("HasNationnality", builder => builder.RequireClaim("Nationality"));
.AddPolicy(PolicyNames.HasNationality, builder => builder.RequireClaim(AppClaimTypes.Nationality));
2.5.4 添加对年龄的认证
- 在Policy的Constants里添加验证的方法名
Restaurants.Infrastructure/Authorization/Constants.cs
- 添加验证的requirement,
Restaurants.Infrastructure/Authorization/Requirements/MinimumAgeRequirement.cs
using Microsoft.AspNetCore.Authorization;
namespace Restaurants.Infrastructure.Authorization.Requirements
{
public class MinimumAgeRequirement : IAuthorizationRequirement
{
public int MinimumAge { get; }
public MinimumAgeRequirement(int minimumAge)
{
MinimumAge = minimumAge;
}
}
}
- 添加handler,
Restaurants.Infrastructure/Authorization/Requirements/MinimumAgeRequirementHandler.cs
using Microsoft.AspNetCore.Authorization;
using Microsoft.Extensions.Logging;
using Restaurants.Application.Users;
namespace Restaurants.Infrastructure.Authorization.Requirements
{
public class MinimumAgeRequirementHandler : AuthorizationHandler<MinimumAgeRequirement>
{
private readonly ILogger<MinimumAgeRequirementHandler> _logger;
private readonly IUserContext _userContext;
public MinimumAgeRequirementHandler(ILogger<MinimumAgeRequirementHandler> _logger, IUserContext userContext)
{
this._userContext = userContext;
this._logger = _logger;
}
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, MinimumAgeRequirement requirement)
{
var currentUser = _userContext.GetCurrentUser();
if (currentUser.DateOfBirth == null)
{
_logger.LogWarning("User date of birth is null");
context.Fail();
return Task.CompletedTask;
}
if (currentUser.DateOfBirth.Value.AddYears(requirement.MinimumAge) <= DateOnly.FromDateTime(DateTime.Today))
{
_logger.LogInformation("Authorization succeeded");
context.Succeed(requirement);
}
else
{
context.Fail();
}
return Task.CompletedTask;
}
}
}
4.将Nationality和DateOfBirth添加到UserContext和CurrentUser里,这样handler可以获取这两个属性
Restaurants.Application/Users/CurrentUser.cs
Restaurants.Application/Users/UserContext.cs