Ответ 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 к этому коду, если авторизация проходит, таким образом, у вас все еще есть доступ к пользовательской информации для других целей в вашем коде.
редактирует
- Я добавил третье решение в список возможностей.
- Добавлена сводка решений в верхней части ответа.