如何根据 ASP.Net Core (WebAPI) 中的 HTTP 动词和其他属性声明性地指定授权策略

问题描述

注册了几个授权策略:

ConfigureServices()
{
    services.AddAuthorization(authorisationoptions =>
    {
        authorisationoptions.AddPolicy(StandardAuthorizationPolicy.Name,StandardAuthorizationPolicy.Value);
        authorisationoptions.AddPolicy(MutatingActionAuthorizationPolicy.Name,MutatingActionAuthorizationPolicy.Value);
    }); 
}

& 然后我在所有端点上设置认授权策略:

Configure(IApplicationBuilder app,IWebHostEnvironment env)
{
    app.UseEndpoints(endpoints =>
    {
        endpoints
            .MapControllers()
            .RequireAuthorization(StandardAuthorizationPolicy.Name); // Declaratively require the standard authorization policy on all controller endpoints
    });
}

在我想要指定变异策略的端点上,我目前执行以下操作:

[HttpPut]
[Authorize(MutatingActionAuthorizationPolicy.Name)] // Because 'PUT'. NOT DECLaraTIVE! :-(
public async Task<IActionResult> AddOrUpdateOverride(SourceOverride sourceOverride,CancellationToken cancellationToken)
{
  // ..
}

我真正想要的是更多的控制来声明性地应用基于 HttpVerb(即 POST、PUT、PATCH、DELETE)的变异策略。

知道如何实现这一目标吗?允许我在控制器方法/类上使用其他属性的奖励积分,而不仅仅是 [HttpPost] 等。

注意:我已经看到了一些涉及投射内容解决方案(并且似乎围绕单一访问策略)。我真的宁愿坚持使用多个访问策略。

如果我卡住了,我可能最终会为它编写一个约定测试。

解决方法

您可以实现自定义的 RequireAuthorization 扩展,该扩展将 HTTP 动词过滤功能作为参数并检查每个端点元数据的 HttpMethodAttribute

public static class AuthorizationEndpointConventionBuilderExtensions
    {
        /// <summary>
        /// Adds authorization policies with the specified <see cref="IAuthorizeData"/> to the endpoint(s) filtered by supplied filter function
        /// </summary>
        /// <param name="builder">The endpoint convention builder.</param>
        /// <param name="filterOnHttpMethods">Filters http methods that we applying specific policies to</param>
        /// <param name="authorizeData">
        /// A collection of <paramref name="authorizeData"/>. If empty,the default authorization policy will be used.
        /// </param>
        /// <returns>The original convention builder parameter.</returns>
        public static TBuilder RequireAuthorizationForHttpMethods<TBuilder>(this TBuilder builder,Func<IEnumerable<HttpMethod>,bool> filterOnHttpMethods,params IAuthorizeData[] authorizeData)
            where TBuilder : IEndpointConventionBuilder
        {
            if (builder == null)
            {
                throw new ArgumentNullException(nameof(builder));
            }

            if (authorizeData == null)
            {
                throw new ArgumentNullException(nameof(authorizeData));
            }

            if (authorizeData.Length == 0)
            {
                authorizeData = new IAuthorizeData[] { new AuthorizeAttribute(),};
            }

            builder.Add(endpointBuilder =>
            {
                var appliedHttpMethodAttributes = endpointBuilder.Metadata
                .Where(x => x is HttpMethodAttribute)
                .Cast<HttpMethodAttribute>();

                if (appliedHttpMethodAttributes.Any(x => filterOnHttpMethods(x.HttpMethods
                                              .Select(method => new HttpMethod(method)))))
                {
                    foreach (var data in authorizeData)
                    {
                        endpointBuilder.Metadata.Add(data);
                    }
                }
            });
            return builder;
        }

        /// <summary>
        /// Adds authorization policies with the specified names to the endpoint(s) for filtered endpoints that return for filterOnHttpMethod
        /// </summary>
        /// <param name="builder">The endpoint convention builder.</param>
        /// <param name="filterOnHttpMethods">Filters http methods that we applying specific policies to</param>
        /// <param name="policyNames">A collection of policy names. If empty,the default authorization policy will be used.</param>
        /// <returns>The original convention builder parameter.</returns>
        public static TBuilder RequireAuthorizationForHttpMethods<TBuilder>(this TBuilder builder,params string[] policyNames)
        where TBuilder : IEndpointConventionBuilder
        {
            if (builder == null)
            {
                throw new ArgumentNullException(nameof(builder));
            }

            if (policyNames == null)
            {
                throw new ArgumentNullException(nameof(policyNames));
            }

            return builder.RequireAuthorizationForHttpMethods(filterOnHttpMethods,policyNames.Select(n => new AuthorizeAttribute(n)).ToArray());
        }
    }

然后在原始扩展旁边使用此扩展:

        app.UseEndpoints(endpoints =>
        {
            var mutatingHttpMethods = new HashSet<HttpMethod>()
            {
                HttpMethod.Post,HttpMethod.Put,HttpMethod.Delete
            };

            endpoints
                .MapControllers()
                .RequireAuthorization(StandardAuthorizationPolicy.Name)
                .RequireAuthorizationForHttpMethods(httpMethods => 
                 httpMethods.Any(httpMethod => mutatingHttpMethods.Contains(httpMethod)),MutatingActionAuthorizationPolicy.Name);
            });
        }