Custom user claims#
在控制器如何自定义claims?
添加国家
至少20年
创建至少2个Restaurants
在CleanArchitecture.Infrastructure项目中#
Constants.cs
namespace CleanArchitecture.Infrastructure.Authorization;
public static class PolicyNames
{
public const string HasNationality = "HasNationality";
public const string AtLeast20 = "AtLeast20";
public const string CreatedAtleast2Restaurants = "CreatedAtleast2Restaurants";
}
public static class AppClaimTypes
{
public const string Nationality = "Nationality";
public const string DateOfBirth = "DateOfBirth";
}
RestaurantsUserClaimsPrincipalFactory
using CleanArchitecture.Domain.Entities;
using Microsoft.AspNetCore.Identity;
using Microsoft.Extensions.Options;
using System.Security.Claims;
namespace CleanArchitecture.Infrastructure.Authorization;
public class RestaurantsUserClaimsPrincipalFactory(UserManager<User> userManager,
RoleManager<IdentityRole> roleManager,
IOptions<IdentityOptions> options)
: UserClaimsPrincipalFactory<User, IdentityRole>(userManager, roleManager, options)
{
public override async Task<ClaimsPrincipal> CreateAsync(User user)
{
var id = await GenerateClaimsAsync(user);
if (user.Nationality != null)
{
id.AddClaim(new Claim(AppClaimTypes.Nationality, user.Nationality));
}
if (user.DateOfBirth != null)
{
id.AddClaim(new Claim(AppClaimTypes.DateOfBirth, user.DateOfBirth.Value.ToString("yyyy-MM-dd")));
}
return new ClaimsPrincipal(id);
}
}
ServiceCollectionExtensions.cs
using CleanArchitecture.Domain.Entities;
using CleanArchitecture.Domain.Repositories;
using CleanArchitecture.Infrastructure.Authorization;
using CleanArchitecture.Infrastructure.Persistence;
using CleanArchitecture.Infrastructure.Repositories;
using CleanArchitecture.Infrastructure.Seeders;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
namespace CleanArchitecture.Infrastructure.Extensions;
public static class ServiceCollectionExtensions
{
public static void AddInfrastructure(this IServiceCollection services, IConfiguration configuration)
{
services.AddDbContext<RestaurantsDbContext>(options => options.UseSqlServer(configuration.GetConnectionString("RestaurantsDb"))
.EnableSensitiveDataLogging());
services.AddIdentityApiEndpoints<User>()
.AddRoles<IdentityRole>()
.AddClaimsPrincipalFactory<RestaurantsUserClaimsPrincipalFactory>()
.AddEntityFrameworkStores<RestaurantsDbContext>();
services.AddScoped<IRestaurantSeeder, RestaurantSeeder>();
services.AddScoped<IRestaurantsRepository, RestaurantsRepository>();
services.AddScoped<IDishesRepository, DishesRepository>();
services.AddAuthorizationBuilder()
.AddPolicy(PolicyNames.HasNationality,
builder => builder.RequireClaim(AppClaimTypes.Nationality, "German", "Polish"));
}
}
在CleanArchitecture.Api项目中
RestaurantsController.cs
[HttpGet("{id}")]
[Authorize(Policy = PolicyNames.HasNationality)]
public async Task<IActionResult> GetId([FromRoute] int id)
{
var restaurant = await mediator.Send(new GetRestaurantByIdQuery(id));
return Ok(restaurant);
}
Custom authorization requirements#
添加20年claims,比较复杂的
在CleanArchitecture.Infrastructure项目添加以下文件 MinimumAgeRequirement.cs
using Microsoft.AspNetCore.Authorization;
namespace CleanArchitecture.Infrastructure.Authorization.Requirements;
public class MinimumAgeRequirement(int minimumAge) : IAuthorizationRequirement
{
public int MinimumAge { get; } = minimumAge;
}
MinimumAgeRequirement.cs
using CleanArchitecture.Application.Users;
using Microsoft.AspNetCore.Authorization;
using Microsoft.Extensions.Logging;
namespace CleanArchitecture.Infrastructure.Authorization.Requirements;
internal class MinimumAgeRequirementHandler(ILogger<MinimumAgeRequirementHandler> logger,
IUserContext userContext) : AuthorizationHandler<MinimumAgeRequirement>
{
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, MinimumAgeRequirement requirement)
{
var currentUser = userContext.GetCurrentUser();
logger.LogInformation("User: {Email}, date of birth {DoB} - Handling MinimumAgeRequirement",
currentUser.Email,
currentUser.DateOfBirth);
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;
}
}
修改ServiceCollectionExtensions.cs
using CleanArchitecture.Domain.Entities;
using CleanArchitecture.Domain.Repositories;
using CleanArchitecture.Infrastructure.Authorization;
using CleanArchitecture.Infrastructure.Authorization.Requirements;
using CleanArchitecture.Infrastructure.Persistence;
using CleanArchitecture.Infrastructure.Repositories;
using CleanArchitecture.Infrastructure.Seeders;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
namespace CleanArchitecture.Infrastructure.Extensions;
public static class ServiceCollectionExtensions
{
public static void AddInfrastructure(this IServiceCollection services, IConfiguration configuration)
{
services.AddDbContext<RestaurantsDbContext>(options => options.UseSqlServer(configuration.GetConnectionString("RestaurantsDb"))
.EnableSensitiveDataLogging());
services.AddIdentityApiEndpoints<User>()
.AddRoles<IdentityRole>()
.AddClaimsPrincipalFactory<RestaurantsUserClaimsPrincipalFactory>()
.AddEntityFrameworkStores<RestaurantsDbContext>();
services.AddScoped<IRestaurantSeeder, RestaurantSeeder>();
services.AddScoped<IRestaurantsRepository, RestaurantsRepository>();
services.AddScoped<IDishesRepository, DishesRepository>();
services.AddAuthorizationBuilder()
.AddPolicy(PolicyNames.HasNationality,
builder => builder.RequireClaim(AppClaimTypes.Nationality, "German", "Polish"))
.AddPolicy(PolicyNames.AtLeast20,
builder => builder.AddRequirements(new MinimumAgeRequirement(20)));
services.AddScoped<IAuthorizationHandler, MinimumAgeRequirementHandler>();
}
}
用户登录的时候需要存储DateOfBirth,修改CleanArchitecture.Application项目
CurrentUser.cs
namespace CleanArchitecture.Application.Users;
public record CurrentUser(string Id,
string Email,
IEnumerable<string> Roles,
string? Nationality,
DateOnly? DateOfBirth)
{
public bool IsInRole(string role) => Roles.Contains(role);
}
UserContext.cs
using Microsoft.AspNetCore.Http;
using System.Security.Claims;
namespace CleanArchitecture.Application.Users
{
public interface IUserContext
{
CurrentUser? GetCurrentUser();
}
public class UserContext(IHttpContextAccessor httpContextAccessor) : IUserContext
{
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);
}
}
}
在CleanArchitecture.Api项目中
DishesController.cs
[HttpGet]
[Authorize(Policy = PolicyNames.AtLeast20)]
public async Task<ActionResult<IEnumerable<DishDto>>> GetAllForRestaurant([FromRoute] int restaurantId)
{
var dishes = await mediator.Send(new GetDishesForRestaurantQuery(restaurantId));
return Ok(dishes);
}
Resource based authorization#
在CleanArchitecture.Domain项目中添加ResourceOperation.cs
namespace CleanArchitecture.Domain.Constants;
public enum ResourceOperation
{
Create,
Read,
Update,
Delete
}
在CleanArchitecture.Domain项目中添加IRestaurantAuthorizationService.cs
using CleanArchitecture.Domain.Constants;
using CleanArchitecture.Domain.Entities;
namespace CleanArchitecture.Domain.Interfaces;
public interface IRestaurantAuthorizationService
{
bool Authorize(Restaurant restaurant, ResourceOperation resourceOperation);
}
在CleanArchitecture.Domain项目中添加ForbidException.cs
namespace CleanArchitecture.Domain.Exceptions;
public class ForbidException : Exception
{
}
在CleanArchitecture.Infrastructure项目中添加RestaurantAuthorizationService.cs
using CleanArchitecture.Application.Users;
using CleanArchitecture.Domain.Constants;
using CleanArchitecture.Domain.Entities;
using CleanArchitecture.Domain.Interfaces;
using Microsoft.Extensions.Logging;
namespace CleanArchitecture.Infrastructure.Authorization.Services;
public class RestaurantAuthorizationService(ILogger<RestaurantAuthorizationService> logger,
IUserContext userContext) : IRestaurantAuthorizationService
{
public bool Authorize(Restaurant restaurant, ResourceOperation resourceOperation)
{
var user = userContext.GetCurrentUser();
logger.LogInformation("Authorizing user {UserEmail}, to {Operation} for restaurant {RestaurantName}",
user.Email,
resourceOperation,
restaurant.Name);
if (resourceOperation == ResourceOperation.Read || resourceOperation == ResourceOperation.Create)
{
logger.LogInformation("Create/read operation - successful authorization");
return true;
}
if (resourceOperation == ResourceOperation.Delete && user.IsInRole(UserRoles.Admin))
{
logger.LogInformation("Admin user, delete operation - successful authorization");
return true;
}
if ((resourceOperation == ResourceOperation.Delete || resourceOperation == ResourceOperation.Update)
&& user.Id == restaurant.OwnerId)
{
logger.LogInformation("Restaurant owner - successful authorization");
return true;
}
return false;
}
}
在CleanArchitecture.Infrastructure项目中修改ServiceCollectionExtensions.cs
using CleanArchitecture.Domain.Entities;
using CleanArchitecture.Domain.Interfaces;
using CleanArchitecture.Domain.Repositories;
using CleanArchitecture.Infrastructure.Authorization;
using CleanArchitecture.Infrastructure.Authorization.Requirements;
using CleanArchitecture.Infrastructure.Authorization.Services;
using CleanArchitecture.Infrastructure.Persistence;
using CleanArchitecture.Infrastructure.Repositories;
using CleanArchitecture.Infrastructure.Seeders;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
namespace CleanArchitecture.Infrastructure.Extensions;
public static class ServiceCollectionExtensions
{
public static void AddInfrastructure(this IServiceCollection services, IConfiguration configuration)
{
services.AddDbContext<RestaurantsDbContext>(options => options.UseSqlServer(configuration.GetConnectionString("RestaurantsDb"))
.EnableSensitiveDataLogging());
services.AddIdentityApiEndpoints<User>()
.AddRoles<IdentityRole>()
.AddClaimsPrincipalFactory<RestaurantsUserClaimsPrincipalFactory>()
.AddEntityFrameworkStores<RestaurantsDbContext>();
services.AddScoped<IRestaurantSeeder, RestaurantSeeder>();
services.AddScoped<IRestaurantsRepository, RestaurantsRepository>();
services.AddScoped<IDishesRepository, DishesRepository>();
services.AddAuthorizationBuilder()
.AddPolicy(PolicyNames.HasNationality,
builder => builder.RequireClaim(AppClaimTypes.Nationality, "German", "Polish"))
.AddPolicy(PolicyNames.AtLeast20,
builder => builder.AddRequirements(new MinimumAgeRequirement(20)));
services.AddScoped<IAuthorizationHandler, MinimumAgeRequirementHandler>();
services.AddScoped<IRestaurantAuthorizationService, RestaurantAuthorizationService>();
}
}
在CleanArchitecture.API项目中修改ErrorHandlingMiddleware.cs
using CleanArchitecture.Domain.Exceptions;
namespace CleanArchitecture.API.Middlewares;
public class ErrorHandlingMiddleware(ILogger<ErrorHandlingMiddleware> logger) : IMiddleware
{
public async Task InvokeAsync(HttpContext context, RequestDelegate next)
{
try
{
await next.Invoke(context);
}
catch (NotFoundException notFound)
{
context.Response.StatusCode = 404;
await context.Response.WriteAsync(notFound.Message);
logger.LogWarning(notFound.Message);
}
catch (ForbidException)
{
context.Response.StatusCode = 403;
await context.Response.WriteAsync("Access forbidden");
}
catch (Exception ex)
{
logger.LogError(ex, ex.Message);
context.Response.StatusCode = 500;
await context.Response.WriteAsync("Something went wrong");
}
}
}
在CleanArchitecture.Application项目中修改Delete等相关代码
using CleanArchitecture.Domain.Constants;
using CleanArchitecture.Domain.Entities;
using CleanArchitecture.Domain.Exceptions;
using CleanArchitecture.Domain.Interfaces;
using CleanArchitecture.Domain.Repositories;
using MediatR;
using Microsoft.Extensions.Logging;
namespace CleanArchitecture.Application.Restaurants.Commands.DeleteRestaurant;
public class DeleteRestaurantCommandHandler(ILogger<DeleteRestaurantCommandHandler> logger,
IRestaurantsRepository restaurantsRepository,
IRestaurantAuthorizationService restaurantAuthorizationService) : IRequestHandler<DeleteRestaurantCommand>
{
public async Task Handle(DeleteRestaurantCommand request, CancellationToken cancellationToken)
{
logger.LogInformation("Deleting restaurant with id: {RestaurantId}", request.Id);
var restaurant = await restaurantsRepository.GetIdAsync(request.Id);
if (restaurant is null)
throw new NotFoundException(nameof(Restaurant), request.Id.ToString());
if (!restaurantAuthorizationService.Authorize(restaurant, ResourceOperation.Delete))
throw new ForbidException();
await restaurantsRepository.DeleteAsync(restaurant);
}
}