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

时间:2016-03-03 06:58:57

标签: 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());
    }

在我的控制器方法中:

    public async Task<IHttpActionResult> getTest(int examId, int userTestId, int retrieve)
    {
        setUserAndRole();

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

有人可以告诉我如何使用动作过滤器执行此操作吗?理想情况下,我希望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);
    }
}

2 个答案:

答案 0 :(得分:36)

根据您的方法签名(以及后面的注释),代码假定您使用的是Web API而不是MVC,尽管这也可以很容易地为MVC更改。

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

一些提示

  • 尝试在可能的情况下始终使用接口。它使单元测试变得更容易,更容易改变实现等。我不会在这里讨论here is a link
  • 在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接口的访问的方式使用。由于目前尚不清楚您希望如何使用此信息,因此我在此提案中跳槽,确保用户拥有足够的权限。
  4. 通用代码

    <强> 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
                }
    
            }
        }
    }
    

    解决方案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);
            }
        }
    }
    

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

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

    <强> 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();
            }
        }
    }
    

    解决方案2 - IoC / DI

    这是wiki on Inversion of Controlwiki on Dependency Injection。这些术语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);
            }
        }
    }
    

    现在我们只需要在应用程序启动时调用它,这可以在 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
            }
        }
    }
    

    现在我们可以随时随地使用它。

    <强> 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();
            }
        }
    }
    

    以下是此示例中您可以从NuGet获得的依赖项。

    Install-Package Autofac
    Install-Package Autofac.Mvc5
    Install-Package Autofac.WebApi2
    

    解决方案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...");
            }
        }
    }
    

    <强> 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();
            }
        }
    }
    

    解决方案的快速比较

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

    编辑

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

答案 1 :(得分: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());
        }    
    }
}

然后选择基本控制器,控制器或操作(取决于您要应用自定义过滤器的级别),并将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
}

希望应该这样做。