How can I get user and claim information using action filters?

前端 未结 2 741
轻奢々
轻奢々 2020-12-23 12:19

Right now I am doing this to get the information I need:

In my base controller:

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


        
相关标签:
2条回答
  • 2020-12-23 12:44

    Based on your method signature (and later comments below) the code assumes that you are using Web API and not MVC although this could easily be changed for MVC as well.

    I do want to specify that if you look purely at the requirements its how can I create a maintainable piece of code that is reused. In this case the code gets claims based information and injects it into your controllers. The fact that you are asking for a Filter is a technical requirement but I am also going to present a solution that does not use a Filter but an IoC instead which adds some flexibility (IMHO).

    Some Tips

    • Try to always use interfaces when/where possible. It makes for easier unit testing, easier to alter the implementation, etc. I will not go into that all here but here is a link.
    • In WebAPI and also MVC do not use the System.Web.HttpContext.Current. It is very hard to unit test code that makes use of this. Mvc and Web API have a common abstraction called HttpContextBase, use this when possible. If there is no other way (I have not seen this yet) then use new HttpContextWrapper(System.Web.HttpContext.Current) and pass this instance in to what ever method/class you want to use (HttpContextWrapper derives from HttpContextBase).

    Proposed Solutions

    These are in no particular order. See end for a basic pro list of each solution.

    1. Web API Filter - exactly what you are asking for. A Web API action filter to inject the claims based information into your Web Api methods.
    2. IoC/DI - A very flexible approach to injecting dependencies into your Controllers and classes. I used AutoFac as the Di framework and illustrate how you can get the claims based info injected into your controller.
    3. Authorization Filter - Essentially an extension on solution 1 but used in a manner in which you can secure access to your Web API interface. As it was not clear how you wanted to use this information I made the jump in this proposal that you wanted it to ensure the user had sufficient privileges.

    Common Code

    UserInfo.cs

    This is common code used in both solutions that I will demo below. This is a common abstraction around the properties / claims based info you want access to. This way you do not have to extend methods if you want to add access to another property but just extend the interface / class.

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

    Solution 1 - Web API Filter

    This solution demos what you asked for, a reusable Web API filter that populates the claims based info.

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

    Now you can use this filter by applying it to your Web API methods like an attribute or at the class level. If you want access everywhere you can also add it to the WebApiConfig.cs code like so (optional).

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

    WebApiTestController.cs

    Here how to use it in a Web API method. Note that the matching is done based on the parameter name, this has to match the name assigned in the filter actionContext.ActionArguments["claimsUser"]. Your method will now be populated with the added instance from your filter.

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

    Solution 2 - IoC / DI

    Here is a wiki on Inversion of Control and a wiki on Dependency Injection. These terms, IoC and DI, are usually used interchangeably. In a nutshell you define dependencies, register them with a DI or IoC framework, and these dependency instances are then injected in your running code for you.

    There are many IoC frameworks out there, I used AutoFac but you can use whatever you want. Following this method you define your injectibles once and get access to them wherever you want. Just by referencing my new interface in the constructor it will be injected with the instance at run time.

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

    Now we just have to call this when our application starts, this can be done in the Global.asax.cs file.

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

    Now we can use it where ever we want.

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

    Here are the dependencies you can get from NuGet for this example.

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

    Solution 3 - Authorization Filter

    One more solution I thought of. You never specified why you needed the user and role id. Maybe you want to check access level in the method before proceeding. If this is the case the best solution is to not only implement a Filter but to create an override of System.Web.Http.Filters.AuthorizationFilterAttribute. This allows you to execute an authorization check before your code even executes which is very handy if you have varying levels of access across your web api interface. The code I put together illustrates the point but you could extend it to add actual calls to a repository for checks.

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

    Quick Comparison of Solutions

    • If you want flexibility go with AutoFac. You can reuse this for many of the moving parts of your solution/project. It makes for very maintainable and testable code. You can extend it very easily once its setup and running.
    • If you want something static and simple that is guaranteed not to change and you have minimal number of moving parts where an DI framework would be overkill then go with the Filter solution.
    • If you want to execute authorization checks in a single location then a custom AuthorizationFilterAttribute is the best way to go. You can add the code from the filter in solution #1 to this code if authorization passes, this way you still have access to the user information for other purposes in your code.

    Edits

    • I added a 3rd solution to the list of possibilities.
    • Added a solution summary at the top of the answer.
    0 讨论(0)
  • 2020-12-23 12:46

    Create a custom ActionFilter class (for 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());
            }    
        }
    }
    

    Then decorate a choice of Base Controller, Controller or Action(s) (depending on the level you want to apply the custom filter), and specify roleId and userId as Action parameters:

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

    Hopefully that should do it.

    0 讨论(0)
提交回复
热议问题