Проведя часы, я смог сделать эту работу. Поэтому я просто хотел опубликовать это как ответ на мой вопрос, но мои " захватывающие дух вопросы " (см. В конце моего вопроса выше) все еще остаются. Так что имейте в виду, что это решение не гарантирует никаких проблем.
Одна из проблем, с которыми я столкнулся при написании кода в StartUp.cs
в моем вопросе выше, была:
// if I have 100s of policies, would all of them have to be defined here?
services.AddAuthorization(options =>
options.AddPolicy("CanReadData", policy => policy.Requirements.Add(new NeedsPolicyAttribute(PolicyEnum.CanReadData))));
, потому что примеры кода добавляли их одну за другой в виде жестко закодированной строки, которая была беспокоит меня с самого начала, так как я хочу использовать Enum, а не жестко закодированные значения. И я не хотел добавлять много строк в Startup.cs
, который также нужно было обновлять каждый раз, когда я добавляю новую политику в приложение.
Так что на самом деле это было легко. Все, что я сделал:
Я написал расширение для получения всех значений перечисления, как показано ниже:
public static class EnumUtils {
public static IEnumerable < T > GetAllEnumValues < T > () {
return System.Enum.GetValues(typeof(T)).Cast < T > ();
}
}
Так что я смог использовать его, как показано ниже. Таким образом, я могу использовать любое новое созданное значение перечисления Policy поверх методов API в качестве атрибута, не касаясь StartUp.cs
сейчас.
services.AddAuthorization(options => {
// add all the policies to option to be able to use in ExtendedAuthorizeAttribute on api methods.
foreach(var policyEnum in EnumUtils.GetAllEnumValues < PolicyEnum > ())
options.AddPolicy(policyEnum.ToString(), policy => policy.Requirements.Add(new ExtendedAuthorizeAttribute(policyEnum)));
});
Затем я добавил политики, которые имеет пользователь:
public List < Claim > GetUserClaims(AuthRequestDto authRequestDto) {
var userRoles = _unitOfWork.Roles.GetUserRoles(authRequestDto.UserId);
var policies = userRoles.SelectMany(x = >x.RolePolicies.Where(p = >p.Policy.IsActive).Select(y = >y.Policy.Name)).Distinct().ToList();
var claims = new List < Claim > ();
policies.ForEach(policy = >claims.Add(new Claim("UserPolicy", policy)));
claims.Add(new Claim("Id", authRequestDto.UserId.ToString()));
return claims;
}
И прикрепил их к моему токену, чтобы, как только пользователь сделал запрос с этим токеном, я мог разрешить его и проверить на соответствие требованиям политики по методу API.
Затем я создал новый Attribute
как ExtendedAuthorizeAttribute
который происходит от AuthroizeAttribute
И реализует IAuthorizationRequirement
Итак, 2 вещи здесь: я получил свой пользовательский атрибут из AuthroizeAttribute
, потому что я хочу, чтобы он автоматически запускался для авторизации, чтобы проверить, имеет ли пользователь требуемую политику для этот метод API. И я реализовал IAuthorizationRequirement
, потому что это позволяет мне использовать мой атрибут как «требование» в методе HandleRequirementAsync
.
Итак, атрибут, который я создал, был:
/// <summary>
/// Extended Authorize Attribute is derived from Authorize Attribute
/// also implements IAuthorizationRequirement.
/// Deriving from AuthorizeAttribute accepts only string for policy names
/// By using this extension class, it let's me use Policy Enum then it converts it to string
/// before passing it to AuthorizeAttribute which was not possible in controller.
/// </summary>
public class ExtendedAuthorizeAttribute: AuthorizeAttribute,
IAuthorizationRequirement {
public ExtendedAuthorizeAttribute(PolicyEnum policyEnum = PolicyEnum.General) : base(policyEnum.ToString()) {}
}
И TokenValidationHandler
стал как показано ниже:
public class TokenValidationHandler: AuthorizationHandler < ExtendedAuthorizeAttribute > {
private readonly JwtSettings _jwtSettings;
private readonly IHttpContextAccessor _contextAccessor;
public TokenValidationHandler(JwtSettings jwtSettings, IHttpContextAccessor contextAccessor) {
_jwtSettings = jwtSettings;
_contextAccessor = contextAccessor;
}
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, ExtendedAuthorizeAttribute requirement) {
// injected the IHttpContextAccessor to get the token from the request.
var rawToken = !_contextAccessor.HttpContext.Request.Headers.ContainsKey("Authorization") ? string.Empty: _contextAccessor ? .HttpContext ? .Request ? .Headers["Authorization"].ToString();
if (string.IsNullOrEmpty(rawToken)) {
context.Fail();
return Task.CompletedTask;
}
var token = ScrubToken(rawToken);
var handler = new JwtSecurityTokenHandler();
try {
// validates the given token and returns claims principal for user if validated.
var user = handler.ValidateToken(token, _jwtSettings.TokenValidationParameters, out SecurityToken _);
// Check if UserPolicies claims include the required the policy
if (IsRequiredPolicyExistOnUser(user.Claims ? .ToList(), requirement)) {
context.Succeed(requirement);
} else {
context.Fail();
}
} catch(Exception e) {
// TODO: Logging!
context.Fail();
}
return Task.CompletedTask;
}
private bool IsRequiredPolicyExistOnUser(List < Claim > userClaims, ExtendedAuthorizeAttribute requirement) {
return userClaims != null && userClaims.Any() && userClaims.Where(x = >x.Type == "UserPolicy").Any(c = >c.Value == requirement.Policy.ToString());
}
private string ScrubToken(string rawToken) {
return rawToken.Replace("Bearer ", "");
}
}
И, наконец, я смог использовать это в моих методах API, как показано ниже:
[HttpGet]
[Route("get/{id}")]
[ExtendedAuthorize(PolicyEnum.CanReadData)]
public ActionResult < AppUserDto > GetAppUser(int id) {
return _appUserManager.Get(id);
}
, и все заработало так, как я хотел. Но, опять же, захватывающих вопросов все еще существуют!