如何使用操作过滤器获取用户和声明信息?

Ala*_*an2 28 asp.net asp.net-mvc

现在我这样做是为了获得我需要的信息:

在我的基本控制器中:

    public int roleId { get; private set; }
    public int userId { get; private set; }

    public void setUserAndRole()
    {
        ClaimsIdentity claimsIdentity;
        var httpContext = HttpContext.Current;
        claimsIdentity = httpContext.User.Identity as ClaimsIdentity;
        roleId = Int32.Parse(claimsIdentity.FindFirst("RoleId").Value);
        userId = Int32.Parse(User.Identity.GetUserId());
    }
Run Code Online (Sandbox Code Playgroud)

在我的控制器方法中:

    public async Task<IHttpActionResult> getTest(int examId, int userTestId, int retrieve)
    {
        setUserAndRole();
Run Code Online (Sandbox Code Playgroud)

我希望roleId和userId在我的类的构造函数中可用并填充,但是从我理解的构造函数在授权信息可用之前触发.

有人能告诉我如何使用Action Filter做到这一点吗?理想情况下,我希望Action Filter位于控制器级别,但如果没有,则可以在方法级别完成.

我希望得到一些好的建议和意见.谢谢

更新以显示System.Web.Http

#region Assembly System.Web.Http, Version=5.2.2.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35
// C:\H\server\packages\Microsoft.AspNet.WebApi.Core.5.2.2\lib\net45\System.Web.Http.dll
#endregion

using System.Threading;
using System.Threading.Tasks;
using System.Web.Http.Controllers;

namespace System.Web.Http.Filters
{
    //
    // Summary:
    //     Represents the base class for all action-filter attributes.
    [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, Inherited = true, AllowMultiple = true)]
    public abstract class ActionFilterAttribute : FilterAttribute, IActionFilter, IFilter
    {
        //
        // Summary:
        //     Initializes a new instance of the System.Web.Http.Filters.ActionFilterAttribute
        //     class.
        protected ActionFilterAttribute();

        //
        // Summary:
        //     Occurs after the action method is invoked.
        //
        // Parameters:
        //   actionExecutedContext:
        //     The action executed context.
        public virtual void OnActionExecuted(HttpActionExecutedContext actionExecutedContext);
        public virtual Task OnActionExecutedAsync(HttpActionExecutedContext actionExecutedContext, CancellationToken cancellationToken);
        //
        // Summary:
        //     Occurs before the action method is invoked.
        //
        // Parameters:
        //   actionContext:
        //     The action context.
        public virtual void OnActionExecuting(HttpActionContext actionContext);
        public virtual Task OnActionExecutingAsync(HttpActionContext actionContext, CancellationToken cancellationToken);
    }
}
Run Code Online (Sandbox Code Playgroud)

Igo*_*gor 36

根据您的方法签名(以及后面的注释),代码假定您使用的是Web API而不是MVC,尽管这也可以轻松地针对MVC进行更改.

我想指出,如果你纯粹看看它的要求,我怎么能创建一个可重用的可维护代码片段.在这种情况下,代码获取基于声明的信息并将其注入您的控制器.您要求过滤器的事实是技术要求,但我也将提出一个不使用过滤器而是使用IoC的解决方案,这增加了一些灵活性(恕我直言).

一些技巧

  • 尝试在可能的情况下始终使用接口.它使单元测试更容易,更容易改变实现等.我不会在这里讨论所有内容,但这里有一个链接.
  • 在WebAPI和MVC中都不要使用System.Web.HttpContext.Current.很难对使用它的单元测试代码进行单元测试.Mvc和Web API有一个共同的抽象HttpContextBase,尽可能使用它.如果没有其他方法(我还没有看到),那么使用new HttpContextWrapper(System.Web.HttpContext.Current)并将此实例传递给您想要使用的任何方法/类(HttpContextWrapper派生自HttpContextBase).

提出的解决方案

这些没有特别的顺序.请参阅end以获取每个解决方案的基本专业列表.

  1. Web API过滤器 - 正是您要求的.Web API操作筛选器,用于将基于声明的信息注入Web Api方法.
  2. IoC/DI - 一种非常灵活的方法,可以将依赖项注入到控制器和类中.我使用AutoFac作为Di框架,并说明如何将基于声明的信息注入控制器.
  3. 授权过滤器 - 基本上是解决方案1的扩展,但以一种可以保护对Web API接口的访问的方式使用.由于不清楚你想如何使用这些信息,我在这个提案中跳了一下,你希望它确保用户有足够的权限.

共同准则

UserInfo.cs

这是我将在下面演示的两种解决方案中使用的常用代码.这是您希望访问的基于属性/声明的信息的常见抽象.这样,如果要添加对另一个属性的访问权限,只需扩展接口/类,就不必扩展方法.

using System;
using System.Security.Claims;
using System.Web;
using Microsoft.AspNet.Identity;

namespace MyNamespace
{
    public interface IUserInfo
    {
        int RoleId { get; }
        int UserId { get; }
        bool IsAuthenticated { get; }
    }

    public class WebUserInfo : IUserInfo
    {
        public int RoleId { get; set; }
        public int UserId { get; set; }
        public bool IsAuthenticated { get; set; }

        public WebUserInfo(HttpContextBase httpContext)
        {
            try
            {
                var claimsIdentity = httpContext.User.Identity as ClaimsIdentity;
                IsAuthenticated = httpContext.User.Identity.IsAuthenticated;
                if (claimsIdentity != null)
                {
                    RoleId = Int32.Parse(claimsIdentity.FindFirst("RoleId").Value);
                    UserId = Int32.Parse(claimsIdentity.GetUserId());
                }
            }
            catch (Exception ex)
            {
                IsAuthenticated = false;
                UserId = -1;
                RoleId = -1;

                // log exception
            }

        }
    }
}
Run Code Online (Sandbox Code Playgroud)

解决方案1 ​​ - Web API过滤器

此解决方案演示了您所要求的内容,这是一个可重用的Web API过滤器,用于填充基于声明的信息.

WebApiClaimsUserFilter.cs

using System.Web;
using System.Web.Http.Controllers;

namespace MyNamespace
{
    public class WebApiClaimsUserFilterAttribute : System.Web.Http.Filters.ActionFilterAttribute
    {
        public override void OnActionExecuting(HttpActionContext actionContext)
        {
            // access to the HttpContextBase instance can be done using the Properties collection MS_HttpContext
            var context = (HttpContextBase) actionContext.Request.Properties["MS_HttpContext"];
            var user = new WebUserInfo(context);
            actionContext.ActionArguments["claimsUser"] = user; // key name here must match the parameter name in the methods you want to populate with this instance
            base.OnActionExecuting(actionContext);
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

现在,您可以通过将此过滤器应用于Web API方法(如属性或类级别)来使用此过滤器.如果您想在任何地方访问,您也可以将其添加到WebApiConfig.cs代码中(如果可选).

public static class WebApiConfig
{
    public static void Register(HttpConfiguration config)
    {
        config.Filters.Add(new WebApiClaimsUserFilterAttribute());
        // rest of code here
    }
}
Run Code Online (Sandbox Code Playgroud)

WebApiTestController.cs

这里有如何在Web API方法中使用它.请注意,匹配是基于参数名称完成的,这必须与过滤器中指定的名称匹配actionContext.ActionArguments["claimsUser"].现在,您的方法将使用过滤器中添加的实例进行填充.

using System.Web.Http;
using System.Threading.Tasks;

namespace MyNamespace
{
    public class WebApiTestController : ApiController
    {
        [WebApiClaimsUserFilterAttribute] // not necessary if registered in webapiconfig.cs
        public async Task<IHttpActionResult> Get(IUserInfo claimsUser)
        {
            var roleId = claimsUser.RoleId;
            await Task.Delay(1).ConfigureAwait(true);
            return Ok();
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

解决方案2 - IoC/DI

这是一个关于控制反转的维基关于依赖注入维基.这些术语IoC和DI通常可互换使用.简而言之,您可以定义依赖项,使用DI或IoC框架注册它们,然后将这些依赖项实例注入到运行的代码中.

有很多IoC框架,我使用AutoFac,但你可以使用你想要的任何东西.按照这种方法,您可以定义一次注射,并随时随地访问它们.只需在构造函数中引用我的新接口,它就会在运行时注入实例.

DependencyInjectionConfig.cs

using System.Reflection;
using System.Web.Http;
using System.Web.Mvc;
using Autofac;
using Autofac.Integration.Mvc;
using Autofac.Integration.WebApi;

namespace MyNamespace
{
    public static class DependencyInjectionConfig
    {
        /// <summary>
        /// Executes all dependency injection using AutoFac
        /// </summary>
        /// <remarks>See AutoFac Documentation: https://github.com/autofac/Autofac/wiki
        /// Compare speed of AutoFac with other IoC frameworks: http://nareblog.wordpress.com/tag/ioc-autofac-ninject-asp-asp-net-mvc-inversion-of-control 
        /// </remarks>
        public static void RegisterDependencyInjection()
        {
            var builder = new ContainerBuilder();
            var config = GlobalConfiguration.Configuration;

            builder.RegisterApiControllers(Assembly.GetExecutingAssembly());

            builder.RegisterControllers(typeof(DependencyInjectionConfig).Assembly);

            builder.RegisterModule(new AutofacWebTypesModule());

            // here we specify that we want to inject a WebUserInfo wherever IUserInfo is encountered (ie. in a public constructor in the Controllers)
            builder.RegisterType<WebUserInfo>()
                .As<IUserInfo>()
                .InstancePerRequest();

            var container = builder.Build();
            // For Web API
            config.DependencyResolver = new AutofacWebApiDependencyResolver(container);

            // 2 lines for MVC (not web api)
            var resolver = new AutofacDependencyResolver(container);
            DependencyResolver.SetResolver(resolver);
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

现在我们只需要在应用程序启动时调用它,这可以在Global.asax.cs文件中完成.

using System;
using System.Web;
using System.Web.Mvc;
using System.Web.Routing;
using System.Web.Http;

namespace MyNamespace
{
    public class Global : HttpApplication
    {
        void Application_Start(object sender, EventArgs e)
        {
            DependencyInjectionConfig.RegisterDependencyInjection();
            // rest of code
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

现在我们可以在任何我们想要的地方使用它.

WebApiTestController.cs

using System.Web.Http;
using System.Threading.Tasks;

namespace MyNamespace
{
    public class WebApiTestController : ApiController
    {
        private IUserInfo _userInfo;
        public WebApiTestController(IUserInfo userInfo)
        {
            _userInfo = userInfo; // injected from AutoFac
        }
        public async Task<IHttpActionResult> Get()
        {
            var roleId = _userInfo.RoleId;
            await Task.Delay(1).ConfigureAwait(true);
            return Ok();
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

以下是您可以从NuGet获取此示例的依赖项.

Install-Package Autofac
Install-Package Autofac.Mvc5
Install-Package Autofac.WebApi2
Run Code Online (Sandbox Code Playgroud)

解决方案3 - 授权过滤器

我想到的另一种解决方案.您从未指定过为什么需要用户和角色ID.也许您想在继续之前检查方法中的访问级别.如果是这种情况,最好的解决方案是不仅要实现过滤器,还要创建覆盖System.Web.Http.Filters.AuthorizationFilterAttribute.这允许您在代码执行之前执行授权检查,如果您在Web api界面上具有不同的访问级别,则非常方便.我放在一起的代码说明了这一点,但您可以扩展它以添加对存储库的实际调用以进行检查.

WebApiAuthorizationClaimsUserFilterAttribute.cs

using System.Net;
using System.Net.Http;
using System.Web;
using System.Web.Http.Controllers;

namespace MyNamespace
{
    public class WebApiAuthorizationClaimsUserFilterAttribute : System.Web.Http.Filters.AuthorizationFilterAttribute
    {
        // the authorized role id (again, just an example to illustrate this point. I am not advocating for hard coded identifiers in the code)
        public int AuthorizedRoleId { get; set; }

        public override void OnAuthorization(HttpActionContext actionContext)
        {
            var context = (HttpContextBase) actionContext.Request.Properties["MS_HttpContext"];
            var user = new WebUserInfo(context);

            // check if user is authenticated, if not return Unauthorized
            if (!user.IsAuthenticated || user.UserId < 1)
                actionContext.Response = actionContext.Request.CreateResponse(HttpStatusCode.Unauthorized, "User not authenticated...");
            else if(user.RoleId > 0 && user.RoleId != AuthorizedRoleId) // if user is authenticated but should not have access return Forbidden
                actionContext.Response = actionContext.Request.CreateResponse(HttpStatusCode.Forbidden, "Not allowed to access...");
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

WebApiTestController.cs

using System.Web.Http;
using System.Threading.Tasks;

namespace MyNamespace
{
    public class WebApiTestController : ApiController
    {
        [WebApiAuthorizationClaimsUserFilterAttribute(AuthorizedRoleId = 21)] // some role id
        public async Task<IHttpActionResult> Get(IUserInfo claimsUser)
        {
            // code will only be reached if user is authorized based on the filter
            await Task.Delay(1).ConfigureAwait(true);
            return Ok();
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

解决方案的快速比较

  • 如果您想要灵活性,请使用AutoFac.您可以将其重用于解决方案/项目的许多移动部分.它使代码非常易于维护和可测试.一旦设置并运行,您可以非常轻松地扩展它.
  • 如果你想要一些保证不会改变的静态和简单的东西,并且你的移动部件数量很少,而DI框架就会过度,那么请使用Filter解决方案.
  • 如果要在单个位置执行授权检查,则自定义AuthorizationFilterAttribute是最佳方法.如果授权通过,您可以将解决方案#1中的过滤器中的代码添加到此代码中,这样您仍然可以在代码中访问用户信息以用于其他目的.

编辑

  • 我在可能性列表中添加了第三个解决方案.
  • 在答案的顶部添加了解决方案摘要.

  • 我真的很喜欢解决方案 2,但我有一个问题:您如何将 HttpContextBase 传递给 WebUserInfo?我没有看到任何被注册的东西。 (2认同)
  • @ Airn5475-正确,请参见[注册Web抽象](http://docs.autofac.org/en/latest/integration/mvc.html)。当您为Mvc和/或WebApi使用添加的2个Nuget程序包并注册您的主程序集时,这些抽象也将自动在AutoFac中注册。 (2认同)

Ric*_*ckL 9

创建自定义ActionFilter类(用于OnActionExecuting):

using System.Security.Claims;
using System.Web;
using System.Web.Mvc;
using Microsoft.AspNet.Identity;

namespace YourNameSpace
{
    public class CustomActionFilterAttribute : ActionFilterAttribute
    {
        public override void OnActionExecuting(ActionExecutingContext filterContext)
        {
            ClaimsIdentity claimsIdentity = HttpContext.Current.User.Identity as ClaimsIdentity;
            filterContext.ActionParameters["roleId"] = int.Parse(claimsIdentity.FindFirst("RoleId").Value);
            filterContext.ActionParameters["userId"] = int.Parse(claimsIdentity.GetUserId());
        }    
    }
}
Run Code Online (Sandbox Code Playgroud)

然后装饰一系列基本控制器,控制器或操作(取决于您要应用自定义过滤器的级别),并将roleId和userId指定为操作参数:

[CustomActionFilter]
public async Task<IHttpActionResult> getTest(int roleId, int userId, int examId, int userTestId, int retrieve)
{
    // roleId and userId available to use here
    // Your code here
}
Run Code Online (Sandbox Code Playgroud)

希望应该这样做.