Как я могу получить информацию о пользователях и претензиях с помощью фильтров действий?

Сейчас я делаю это, чтобы получить нужную информацию:

В моем базовом контроллере:

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

Ответы

Ответ 1

На основе вашей сигнатуры метода (и последующих комментариев ниже) код предполагает, что вы используете Web API, а не MVC, хотя это также можно легко изменить для MVC.

Я хочу указать, что если вы посмотрите чисто на требования, как я могу создать поддерживаемую часть кода, которая будет повторно использована. В этом случае код получает информацию на основе утверждений и вводит его в ваши контроллеры. Тот факт, что вы запрашиваете фильтр, является техническим требованием, но я также собираюсь представить решение, которое не использует фильтр, а вместо него вместо IoC добавит некоторую гибкость (IMHO).

Некоторые советы

  • Старайтесь всегда использовать интерфейсы, когда это возможно. Это упрощает модульное тестирование, упрощает модификацию реализации и т.д. Я не буду вдаваться в это все, но вот ссылка.
  • В WebAPI, а также MVC не используйте System.Web.HttpContext.Current. Это очень сложно для кода unit test, который использует это. Mvc и Web API имеют общую абстракцию под названием HttpContextBase, используйте это, когда это возможно. Если нет другого пути (я этого еще не видел), используйте new HttpContextWrapper(System.Web.HttpContext.Current) и передайте этот экземпляр тому, что всегда будет использовать метод/класс (HttpContextWrapper происходит от HttpContextBase).

Предлагаемые решения

Это не в порядке. См. Конец базового про-списка каждого решения.

  • Фильтр веб-API - именно то, о чем вы просите. Фильтр действий веб-API для ввода информации, основанной на требованиях, в методы Web Api.
  • IoC/DI - очень гибкий подход к инъекциям зависимостей в ваши контроллеры и классы. Я использовал AutoFac в качестве рамки Di и иллюстрировал, как вы можете получить информацию, основанную на требованиях, введенную в ваш контроллер.
  • Фильтр авторизации - по существу расширение на решение 1, но используемое таким образом, чтобы обеспечить доступ к интерфейсу веб-интерфейса. Поскольку неясно, как вы хотели использовать эту информацию, я сделал скачок в этом предложении, чтобы вы хотели, чтобы у пользователя были достаточные привилегии.

Общий код

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 - Фильтр веб-API

Это решение демонстрирует то, что вы просили, повторно используемый фильтр веб-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);
        }
    }
}

Теперь вы можете использовать этот фильтр, применив его к вашим методам веб-API, например атрибуту или на уровне класса. Если вы хотите получить доступ повсюду, вы также можете добавить его в код WebApiConfig.cs, как это делается (необязательно).

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

WebApiTestController.cs

Здесь, как использовать его в методе веб-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

Вот ссылка на Inversion of Control и wiki на Dependency Инъекции. Эти термины 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 - Фильтр авторизации

Еще одно решение, о котором я думал. Вы никогда не указывали, зачем вам нужен идентификатор пользователя и роли. Возможно, вы хотите проверить уровень доступа в методе перед продолжением. Если это так, лучшим решением является не только реализация фильтра, но и создание переопределения System.Web.Http.Filters.AuthorizationFilterAttribute. Это позволяет вам выполнить проверку авторизации до того, как ваш код даже выполнится, что очень удобно, если у вас есть разные уровни доступа через ваш веб-интерфейс 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 к этому коду, если авторизация проходит, таким образом, у вас все еще есть доступ к пользовательской информации для других целей в вашем коде.

редактирует

  • Я добавил третье решение в список возможностей.
  • Добавлена ​​сводка решений в верхней части ответа.

Ответ 2

Создайте собственный класс 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
}

Надеюсь, это должно сделать это.