在通用控制器上使用自定义过滤器属性



我已经创建了一个新的authorizecclaim过滤器和属性的工作在我的API。它看起来像这样:

public class AuthorizeClaimFilter: IAuthorizationFilter
{
private readonly string[] _claims;
public AuthorizeClaimFilter(string[] claims) => _claims = claims;
public void OnAuthorization(AuthorizationFilterContext context)
{
if (_claims.Any())
{
var user = context.HttpContext.User;
if (user.IsInRole(SituIdentityConstants.Roles.Administrator)) return;
var hasClaim = user.Claims.Any(c =>
c.Type == JwtClaimTypes.Role &&
_claims.Any(x => x.Equals(c.Value)));
if (hasClaim) return;
}
context.Result = new ForbidResult();
}
}
public class AuthorizeClaimAttribute: TypeFilterAttribute
{
public AuthorizeClaimAttribute(params string[] values) : base(typeof(AuthorizeClaimFilter))
{
Arguments = new object[] {values};
}
}

这在我们的API中工作得很好,但在我无限的智慧中,不久前(大约一年前)我创建了一些通用控制器:

[ApiController]
public class GenericController<T> : GenericController<T, int> where T : BaseModel, IKey<int>
{
public GenericController(IMediator mediator) : base(mediator)
{
}
}
[ApiController]
public class GenericController<T, TKey> : ControllerBase where T: BaseModel, IKey<TKey>
{
public readonly IMediator Mediator;
public GenericController(IMediator mediator)
{
Mediator = mediator;
}
/// <summary>
/// Gets an entity by id
/// </summary>
/// <param name="id">The id of the entity</param>
/// <returns>The entity</returns>
[HttpGet("{id}")]
[ApiConventionMethod(typeof(AttemptApiConventions), nameof(AttemptApiConventions.AttemptGet))]
public virtual async Task<ActionResult<T>> GetAsync(TKey id) =>
Ok(await Mediator.Send(new GenericGet<T, TKey>(id)));
/// <summary>
/// Creates a new entity
/// </summary>
/// <param name="model">The entity to create</param>
/// <returns>The created entity</returns>
[HttpPost]
[ApiConventionMethod(typeof(AttemptApiConventions), nameof(AttemptApiConventions.AttemptPost))]
public virtual async Task<ActionResult<T>> CreateAsync(T model) =>
Ok(await Mediator.Send(new GenericCreate<T, TKey>(model, User)))
.WithMessage<T>(string.Format(Resources.EntityCreated, typeof(T).Name));
/// <summary>
/// Updates a new entity
/// </summary>
/// <param name="model">The entity to update</param>
/// <returns>The created entity</returns>
[HttpPut]
[ApiConventionMethod(typeof(AttemptApiConventions), nameof(AttemptApiConventions.AttemptPost))]
public virtual async Task<ActionResult<T>> UpdateAsync(T model) =>
Ok(await Mediator.Send(new GenericUpdate<T, TKey>(model, User)))
.WithMessage<T>(string.Format(Resources.EntityUpdated, typeof(T).Name));
/// <summary>
/// Deletes an entity
/// </summary>
/// <param name="id">The id of the entity</param>
/// <returns></returns>
[HttpDelete("{id}")]
[ApiConventionMethod(typeof(AttemptApiConventions), nameof(AttemptApiConventions.AttemptPost))]
public virtual async Task<ActionResult<bool>> DeleteAsync(TKey id) =>
Ok(await Mediator.Send(new GenericDelete<T, TKey>(id)))
.WithMessage<bool>(string.Format(Resources.EntityDeleted, typeof(T).Name));
}

这些都工作得很好,满足了我们的需求,但是现在我已经开始使用不同的声明来访问不同的端点(因此使用了新的属性过滤器)。我现在需要以某种方式将声明传递给通用控制器。我试着这样做:

[HttpGet("{id}")]
[AuthorizeClaim($"{typeof(T)}:read")]
[ApiConventionMethod(typeof(AttemptApiConventions), nameof(AttemptApiConventions.AttemptGet))]
public virtual async Task<ActionResult<T>> GetAsync(TKey id) =>
Ok(await Mediator.Send(new GenericGet<T, TKey>(id)));

但是我得到一个错误声明:

属性实参必须是属性形参类型

的常量表达式、typeof表达式或数组创建表达式。

是有意义的,所以我试着想另一个解决方案。我想到了注册一个类来保存我的声明列表:

public class RequiredClaimHandler : IRequiredClaimHandler
{
public readonly List<RequiredClaim> Claims;
public RequiredClaimHandler(List<RequiredClaim> claims) => Claims = claims;
public string[] Get(HttpMethod action, Type type)
{
var claim = Claims?.SingleOrDefault(m => m.Action == action && m.Type == type);
return claim != null && claim.UseDefault ? GetDefault(claim) : claim?.Claims;
}
private static string[] GetDefault(RequiredClaim claim)
{
var action = claim.Action == HttpMethod.Get ? "read" : "write";
return new[] {$"{claim.Type.Name.ToLower()}:{action}"};
}
}
我们的想法是将其注册为单例并创建一个工厂方法,如下所示:
public static class RequiredClaimHandlerFactory
{
public static IRequiredClaimHandler Create(List<RequiredClaim> claims) =>
new RequiredClaimHandler(claims);
}
我写了单元测试,然后创建了一个新的属性过滤器:
public class AuthorizeRequiredClaimFilter : IAuthorizationFilter
{
private readonly IRequiredClaimHandler _handler;
private readonly Type _type;
private readonly HttpMethod _method;
public AuthorizeRequiredClaimFilter(IRequiredClaimHandler handler, Type type, HttpMethod method)
{
_handler = handler;
_type = type;
_method = method;
}
public void OnAuthorization(AuthorizationFilterContext context)
{
if (_handler.Get(_method, _type) != null) return;
context.Result = new ForbidResult();
}
}
public class AuthorizeRequiredClaimAttribute: TypeFilterAttribute
{
public AuthorizeRequiredClaimAttribute(HttpMethod method, Type type) : base(typeof(AuthorizeRequiredClaimFilter))
{
Arguments = new object[] {method, type};
}
}

然后我更新了泛型方法:

[HttpGet("{id}")]
[AuthorizeRequiredClaim(HttpMethod.Get, typeof(T))]
[ApiConventionMethod(typeof(AttemptApiConventions), nameof(AttemptApiConventions.AttemptGet))]
public virtual async Task<ActionResult<T>> GetAsync(TKey id) =>
Ok(await Mediator.Send(new GenericGet<T, TKey>(id)));

当然,我回到了我开始的地方....这些参数不是常量。

有谁知道我怎么才能绕过这个?我更喜欢而不是放弃我的通用控制器,因为它们很有用。

如有任何帮助,不胜感激。

几个小时以来我一直在努力解决这个问题。但似乎没有办法,只有实现AuthorizationHandler和使用AuthorizationService

[HttpGet("{id}")]
[ApiConventionMethod(typeof(AttemptApiConventions), nameof(AttemptApiConventions.AttemptGet))]
public virtual async Task<ActionResult<T>> GetAsync(TKey id,[FromServices]IAuthorizationService _authorizationService)

更新答案

您可以在AuthorizeClaimFilter中注入IActionContextAccessor,以便从控制器(或动作参数等)获得泛型类型,并在检查声明时将其用作前缀。不要忘记在Startup中注册IActionContextAccessor

services.TryAddSingleton<IActionContextAccessor, ActionContextAccessor>();

AuthorizeClaimFilter

public class AuthorizeClaimFilter : IAuthorizationFilter
{
private readonly string[] _claims;
private readonly bool _useGenericPrefix;
private readonly IActionContextAccessor _httpContextAccessor;
private const string PrefixSeparator = ":";
public AuthorizeClaimFilter(string[] claims, bool useGenericPrefix, IActionContextAccessor httpContextAccessor)
{
_claims = claims;
_useGenericPrefix = useGenericPrefix;
_httpContextAccessor = httpContextAccessor;
}
public void OnAuthorization(AuthorizationFilterContext context)
{
if (_claims.Any())
{
//calculate values only if needed
var descriptor = (ControllerActionDescriptor)_httpContextAccessor.ActionContext.ActionDescriptor;
//add more complex for selecting generic parameter
//and it depends on your controller hierarchy
var type = descriptor.ControllerTypeInfo.BaseType.GenericTypeArguments.First();
var claims = _useGenericPrefix
? _claims.Select(a => $"{type.Name}{PrefixSeparator}{a}").ToArray()
: _claims;
var user = context.HttpContext.User;
if (user.IsInRole(SituIdentityConstants.Roles.Administrator)) return;
var hasClaim = user.Claims.Any(c =>
c.Type == JwtClaimTypes.Role &&
claims.Any(x => x.Equals(c.Value)));
if (hasClaim) return;
}
context.Result = new ForbidResult();
}
}

AuthorizeClaimAttribute

public class AuthorizeClaimAttribute : TypeFilterAttribute
{
private string[] _claims;
private bool _useGenericPrefix;
public bool UseGenericPrefix
{
get => _useGenericPrefix;
set
{
if (_useGenericPrefix != value)
{
Arguments = new object[] { _claims, value };
}
_useGenericPrefix = value;
}
}
public AuthorizeClaimAttribute(params string[] values) : base(typeof(AuthorizeClaimFilter))
{
_claims = values;
Arguments = new object[] { _claims, false };
}
}

使用

[HttpGet("claim")]
[AuthorizeClaim("read", UseGenericPrefix = true)]
public IActionResult TestClaim()

老回答。现在不能编译,但可能会在引入泛型属性的c#未来版本中工作。

你可以坚持第一个选项使用AuthorizeClaimFilter/AuthorizeClaimAttribute与更新的AuthorizeClaimAttribute

public class AuthorizeClaimAttribute : TypeFilterAttribute
{
private string[] _claims;
public string PrefixSeparator { get; set; } = ":";
private object _prefix;
public object Prefix
{
get => _prefix;
set
{
_prefix = value;
_claims = _claims.Select(a => $"{_prefix}{PrefixSeparator}{a}").ToArray();
Arguments = new object[] { _claims }; ;
}
}
public AuthorizeClaimAttribute(params string[] values) : base(typeof(AuthorizeClaimFilter))
{
_claims = values;
Arguments = new object[] { _claims };
}
}

用法是

[HttpGet("{id}")]
[AuthorizeClaim("read", Prefix = typeof(T)]
[ApiConventionMethod(typeof(AttemptApiConventions), nameof(AttemptApiConventions.AttemptGet))]
public virtual async Task<ActionResult<T>> GetAsync(TKey id) =>
Ok(await Mediator.Send(new GenericGet<T, TKey>(id)));

在这种情况下,您将获得以T全限定名称为前缀的声明。如果您只需要类型的短名称,您可以修改AuthorizeClaimAttribute以接受Type前缀并将_prefix.Name添加到声明中,或者为此实现更复杂的逻辑

public class AuthorizeClaimAttribute : TypeFilterAttribute
{
private string[] _claims;
public string PrefixSeparator { get; set; } = ":";
private Type _prefix;
public Type Prefix
{
get => _prefix;
set
{
_prefix = value;
_claims = _claims.Select(a => $"{_prefix.Name}{PrefixSeparator}{a}").ToArray();
Arguments = new object[] { _claims }; ;
}
}
public AuthorizeClaimAttribute(params string[] values) : base(typeof(AuthorizeClaimFilter))
{
_claims = values;
Arguments = new object[] { _claims };
}
}

最新更新