Unity Inject зависимости в класс фильтра MVC с параметрами

Я использую инъекцию зависимостей Unity.MVC4 для доступа к моим службам. Все работает так, как и должно при внедрении в мой конструктор Controller, но сейчас я хотел бы использовать инъекцию свойств в моем классе фильтра, чтобы я мог получить доступ к своей базе данных изнутри.

До того, как я начал этот вопрос, я гуглил и пробовал разные примеры, но я не мог найти решение, которое сработало бы для меня..

Bootstrapper.cs

public static class Bootstrapper
{
    public static IUnityContainer Initialise()
    {
        var container = BuildUnityContainer();

        DependencyResolver.SetResolver(new UnityDependencyResolver(container));

        return container;
    }

    private static IUnityContainer BuildUnityContainer()
    {
        var container = new UnityContainer();
        container.RegisterType<IAccountRepository, AccountRepository>();
        container.RegisterType<IAdministrationRepository, AdministrationRepository>();
        container.RegisterType<IUploadDirectlyRepository, UploadDirectlyRepository>();
        container.RegisterType<IUserRepository, UserRepository>();
        container.RegisterType<INewsRepository, NewsRepository>();
        container.RegisterType<IContactRepository, ContactRepository>();

        // register all your components with the container here
        // it is NOT necessary to register your controllers

        // e.g. container.RegisterType<ITestService, TestService>();    
        RegisterTypes(container);

        return container;
    }

    public static void RegisterTypes(IUnityContainer container)
    {

    }
}

Application_Start

public class MvcApplication : System.Web.HttpApplication
    {
        protected void Application_Start()
        {
            AreaRegistration.RegisterAllAreas();

            WebApiConfig.Register(GlobalConfiguration.Configuration);
            FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);
            RouteConfig.RegisterRoutes(RouteTable.Routes);
            BundleConfig.RegisterBundles(BundleTable.Bundles);

            Bootstrapper.Initialise();
        }
    }

Рабочий пример

public class UserController : Controller
{
    private readonly IUserRepository _userRepository;

    public UserController(IUserRepository userRepository)
    {
        _userRepository = userRepository;
    }

    public ActionResult GetUser(int userID)
    {
        var user = _userRepository.GetUser(userID)

        return View(user);
    }
}

Следующий код, который я собираюсь показать вам, относится к атрибуту фильтра, который я хотел бы использовать в своих действиях. Я хочу передать параметр типа массив строк, чтобы я мог проверить, разрешено ли текущему пользователю доступ к действию.

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

Фильтр:

public class ClaimsAuthorizeAccountAccess : AuthorizeAttribute
{
    private IAccountRepository _accountRepository { get; set; }
    private String[] _permissions { get; set; }

    public ClaimsAuthorizeAccountAccess(IAccountRepository accountRepository, params String[] permissions)
    {
        _permissions = permissions;
        _accountRepository = accountRepository;
    }

    public override void OnAuthorization(AuthorizationContext filterContext)
    {
        if (HttpContext.Current.User.IsInRole("Account Owner"))
        {
            base.OnAuthorization(filterContext);
        }
        else
        {
            ClaimsIdentity claimsIdentity = (ClaimsIdentity)HttpContext.Current.User.Identity;
            List<AccountLinkPermissionDTO> accountLinkPermissions = new List<AccountLinkPermissionDTO>();

            int accountOwnerID = 0;
            Int32.TryParse(claimsIdentity.Claims.Where(c => c.Type == "AccountOwnerID").Select(c => c.Value).SingleOrDefault(), out accountOwnerID);
            int guestID = 0;
            Int32.TryParse(claimsIdentity.Claims.Where(c => c.Type == ClaimTypes.Sid).Select(c => c.Value).SingleOrDefault(), out guestID);

            //NULL
            accountLinkPermissions = _accountRepository.GetAccountLinkPermissions(accountOwnerID, guestID);

            if (accountLinkPermissions != null)
            {
                List<string> accountLinkPermissionsToString = accountLinkPermissions.Select(m => m.Permission.Name).ToList();
                int hits = accountLinkPermissionsToString.Where(m => _permissions.Contains(m)).Count();

                if (hits > 0)
                {
                    base.OnAuthorization(filterContext);
                }
            }
            else
            {
                //Guest doesnt have right permissions

                filterContext.Result = new RedirectToRouteResult(
                new RouteValueDictionary {
                        { "action", "AccessDenied" },
                        { "controller", "Account" }});
            }
        }
    }
}

Если бы я использовал этот фильтр, он бы выглядел примерно так...

[ClaimsAuthorizeAccountAccess("File read", "File write, File edit")]
public ActionResult Files()
{
    return View();
}

Однако это не работает, потому что фильтр ожидает два параметра (IRepository и string[]). Очевидно, что здесь также невозможно использовать инъекцию конструктора.

Затем я попытался реализовать решение Джона Аллерса, которое можно найти здесь. Это выглядело многообещающе, но это дало мне эту ошибку:

Исключение типа "Microsoft.Practices.Unity.ResolutionFailedException" возникло в Microsoft.Practices.Unity.dll, но не было обработано в коде пользователя.

Дополнительная информация: Не удалось разрешить зависимость, type = "Fildela.ClaimsAuthorizeAccountAccess", name = "(none)".

Исключение произошло во время: при разрешении.

Исключением является: InvalidOperationException - Свойство _accountRepository для типа Fildela.ClaimsAuthorizeAccountAccess не может быть установлено.


На момент исключения контейнер был:

Разрешение Fildela.ClaimsAuthorizeAccountAccess,(нет)

Любое предложение о том, как решить этот плохой мальчик?

Спасибо!

3 ответа

Решение

Сначала установите официальный пакет, Unity.Mvc вместо Unity.MVC4, Этот пакет автоматически устанавливается и регистрируется UnityFilterAttributeFilterProvider который нам нужен для внедрения зависимости атрибута. Вы можете проверить, правильно ли настроен ваш Unity, посмотрев App_Start > UnityMvcActivator"s Start метод. Вы должны увидеть следующие две строки:

public static void Start()
{
    // other codes

    FilterProviders.Providers.Remove(FilterProviders.Providers.OfType<FilterAttributeFilterProvider>().First());
    FilterProviders.Providers.Add(new UnityFilterAttributeFilterProvider(container));
}

Теперь вы можете добавить [Dependency] атрибут для открытых свойств фильтра.

public class ClaimsAuthorizeAccountAccess : AuthorizeAttribute
{
    [Dependency]
    public IAccountRepository AccountRepository { get; set; }
    private String[] _permissions { get; set; }

    public ClaimsAuthorizeAccountAccess(params String[] permissions)
    {
        _permissions = permissions;
    }
}

Согласно пост- пассивным атрибутам, DI-дружественное решение состоит в том, чтобы отделить AuthorizeAttribute на 2 части:

  1. Атрибут, который не содержит поведения для пометки ваших контроллеров и методов действий.
  2. DI-дружественный класс, который реализует IAuthorizationFilter и содержит желаемое поведение.

Для наших целей мы просто наследуем AuthorizeAttribute воспользоваться некоторыми из его встроенных функций.

Обратите внимание, что если вы воспользуетесь этим подходом, нет смысла использовать внедрение свойств для ваших зависимостей базы данных. В любом случае, инъекция конструктора всегда лучший выбор.

ClaimsIdentityAuthorizeAttribute

Во-первых, у нас есть наш атрибут, который не имеет поведения, чтобы пометить наши контроллеры и действия. Мы добавили немного сообразительности для разбора разрешений в массив, чтобы не нужно было делать это при каждой проверке авторизации.

[AttributeUsage(AttributeTargets.Method | AttributeTargets.Class, AllowMultiple = false)]
public class ClaimsAuthorizeAccountAccess : Attribute
{
    private readonly string[] _permissionsSplit;

    public ClaimsAuthorizeAccountAccess(string permissions)
    {
        _permissionsSplit = SplitString(value);
    }

    internal string[] PermissionsSplit
    {
        get { return this._permissionsSplit; }
    }

    internal static string[] SplitString(string original)
    {
        if (string.IsNullOrEmpty(original))
        {
            return new string[0];
        }
        return (from piece in original.Split(new char[] { ',' })
                let trimmed = piece.Trim()
                where !string.IsNullOrEmpty(trimmed)
                select trimmed).ToArray<string>();
    }
}

ClaimsIdentityAuthorizationFilter

Далее у нас есть фильтр авторизации, который будет действовать как глобальный фильтр.

Мы добавляем WhiteListMode что по умолчанию верно, потому что это рекомендуемый способ настройки безопасности (контроллеры и действия требуют входа в систему, если они не получили AllowAnonymousAttribute). К счастью, рамки для этого встроены в AuthorizeAttribute поэтому мы просто используем его как флаг, чтобы проверить глобально.

Мы также добавляем точку расширения, в которую может быть добавлен наш пользовательский сервис авторизации. 2 наиболее вероятные вещи для изменения:

  1. Проверка, чтобы определить, разрешено ли действие.
  2. Действие, которое нужно предпринять, когда пользователь не авторизован.

Так что это то, что мы добавляем к нашему сервису. При желании вы можете изменить это на 2 отдельных сервиса.

public class ClaimsIdentityAuthorizationFilter : AuthorizeAttribute
{
    private readonly IAuthorizationService _authorizationService;
    private string _permissions;
    private string[] _permissionsSplit = new string[0];
    private bool _whiteListMode = true;

    public ClaimsIdentityAuthorizationFilter(IAuthorizationService authorizationService)
    {
        if (authorizationService == null)
            throw new ArgumentNullException("authorizationService");

        this._authorizationService = authorizationService;
    }

    // Hide users and roles, since we aren't using them.
    [Obsolete("Not applicable in this class.")]
    [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
    [Browsable(false), EditorBrowsable(EditorBrowsableState.Never)]
    new public string Roles { get; set; }

    [Obsolete("Not applicable in this class.")]
    [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
    [Browsable(false), EditorBrowsable(EditorBrowsableState.Never)]
    new public string Users { get; set; }

    public string Permissions
    {
        get
        {
            return (this._permissions ?? string.Empty);
        }
        set
        {
            this._permissions = value;
            this._permissionsSplit = SplitString(value);
        }
    }

    public bool WhiteListMode
    {
        get { return this._whiteListMode; }
        set { this._whiteListMode = value; }
    }

    internal static string[] SplitString(string original)
    {
        if (string.IsNullOrEmpty(original))
        {
            return new string[0];
        }
        return (from piece in original.Split(new char[] { ',' })
                let trimmed = piece.Trim()
                where !string.IsNullOrEmpty(trimmed)
                select trimmed).ToArray<string>();
    }

    private ClaimsAuthorizeAccountAccess GetClaimsAuthorizeAccountAccess(ActionDescriptor actionDescriptor)
    {
        ClaimsAuthorizeAccountAccess result = null;

        // Check if the attribute exists on the action method
        result = (ClaimsAuthorizeAccountAccess)actionDescriptor
            .GetCustomAttributes(attributeType: typeof(ClaimsAuthorizeAccountAccess), inherit: true)
            .SingleOrDefault();

        if (result != null)
        {
            return result;
        }

        // Check if the attribute exists on the controller
        result = (ClaimsAuthorizeAccountAccess)actionDescriptor
            .ControllerDescriptor
            .GetCustomAttributes(attributeType: typeof(ClaimsAuthorizeAccountAccess), inherit: true)
            .SingleOrDefault();

        return result;
    }

    protected override bool AuthorizeCore(HttpContextBase httpContext)
    {
        var actionDescriptor = httpContext.Items["ActionDescriptor"] as ActionDescriptor;
        if (actionDescriptor != null)
        {
            var authorizeAttribute = this.GetClaimsAuthorizeAccountAccess(actionDescriptor);

            // If the authorization attribute exists
            if (authorizeAttribute != null)
            {
                // Run the authorization based on the attribute
                return this._authorizationService.HasPermission(
                    httpContext,
                    authorizeAttribute.PermissionsSplit);
            }
            else if (this.WhiteListMode)
            {
                // Run the global authorization
                return this._authorizationService.HasPermission(
                    httpContext,
                    this._permissionsSplit);
            }
        }

        return true;
    }

    public override void OnAuthorization(AuthorizationContext filterContext)
    {
        // Pass the current action descriptor to the AuthorizeCore
        // method on the same thread by using HttpContext.Items
        filterContext.HttpContext.Items["ActionDescriptor"] = filterContext.ActionDescriptor;
        base.OnAuthorization(filterContext);
    }

    protected override void HandleUnauthorizedRequest(AuthorizationContext filterContext)
    {
        filterContext.Result = this._authorizationService.GetUnauthorizedHandler(filterContext);
    }
}

IAuthorizationService

public interface IAuthorizationService
{
    bool HasPermission(HttpContextBase httpContext, string[] permissions);
    ActionResult GetUnauthorizedHandler(AuthorizationContext filterContext);
}

ClaimsIdentityAuthorizationService

Так что теперь мы делаем расширенную настройку для поддержки претензий. Мы разделяем это так, что есть шов, который мы можем использовать, чтобы внедрить другой экземпляр, если бизнес-логика изменится в будущем.

public class ClaimsIdentityAuthorizationService : IAuthorizationService
{
    private IAccountRepository _accountRepository { get; set; }

    public ClaimsIdentityAuthorizationService(IAccountRepository accountRepository)
    {
        if (accountRepository == null)
            throw new ArgumentNullException("accountRepository");

        _accountRepository = accountRepository;
    }

    public bool HasPermission(HttpContextBase httpContext, string[] permissions)
    {
        if (httpContext == null)
        {
            throw new ArgumentNullException("httpContext");
        }
        IPrincipal user = httpContext.User;
        if (!user.Identity.IsAuthenticated)
        {
            return false;
        }
        if (!user.IsInRole("Account Owner"))
        {
            ClaimsIdentity claimsIdentity = (ClaimsIdentity)user.Identity;
            List<AccountLinkPermissionDTO> accountLinkPermissions = new List<AccountLinkPermissionDTO>();

            int accountOwnerID = 0;
            Int32.TryParse(claimsIdentity.Claims.Where(c => c.Type == "AccountOwnerID").Select(c => c.Value).SingleOrDefault(), out accountOwnerID);
            int guestID = 0;
            Int32.TryParse(claimsIdentity.Claims.Where(c => c.Type == ClaimTypes.Sid).Select(c => c.Value).SingleOrDefault(), out guestID);

            //NULL
            accountLinkPermissions = _accountRepository.GetAccountLinkPermissions(accountOwnerID, guestID);

            if (accountLinkPermissions != null)
            {
                List<string> accountLinkPermissionsToString = accountLinkPermissions.Select(m => m.Permission.Name).ToList();
                int hits = accountLinkPermissionsToString.Where(m => permissions.Contains(m)).Count();

                if (hits == 0)
                {
                    return false;
                }
            }
            else
            {
                return false;
            }
        }
        return true;
    }

    public ActionResult GetUnauthorizedHandler(AuthorizationContext filterContext)
    {
        //Guest doesnt have right permissions
        return new RedirectToRouteResult(
            new RouteValueDictionary {
                    { "action", "AccessDenied" },
                    { "controller", "Account" }
            });
    }
}

использование

Зарегистрируйте ваш фильтр глобально и добавьте его зависимости в ваш контейнер.

public class FilterConfig
{
    public static void RegisterGlobalFilters(GlobalFilterCollection filters, IUnityContainer container)
    {
        filters.Add(new HandleErrorAttribute());
        filters.Add(container.Resolve<IAuthorizationFilter>());
    }
}

ПРИМЕЧАНИЕ. Если вам нужна какая-либо из зависимостей фильтра, чтобы время жизни было меньше, чем у синглтона, вам нужно будет использовать GlobalFilterProvider как в этом ответе.

Запускать

public class MvcApplication : System.Web.HttpApplication
{
    protected void Application_Start()
    {
        var container = Bootstrapper.Initialise();

        AreaRegistration.RegisterAllAreas();

        WebApiConfig.Register(GlobalConfiguration.Configuration);
        FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters, container);
        RouteConfig.RegisterRoutes(RouteTable.Routes);
        BundleConfig.RegisterBundles(BundleTable.Bundles);
    }
}

загрузчик

public static class Bootstrapper
{
    public static IUnityContainer Initialise()
    {
        var container = BuildUnityContainer();

        DependencyResolver.SetResolver(new UnityDependencyResolver(container));

        return container;
    }

    private static IUnityContainer BuildUnityContainer()
    {
        var container = new UnityContainer();
        container.RegisterType<IAccountRepository, AccountRepository>();
        container.RegisterType<IAdministrationRepository, AdministrationRepository>();
        container.RegisterType<IUploadDirectlyRepository, UploadDirectlyRepository>();
        container.RegisterType<IUserRepository, UserRepository>();
        container.RegisterType<INewsRepository, NewsRepository>();
        container.RegisterType<IContactRepository, ContactRepository>();

        // Register the types for the authorization filter
        container.RegisterType<IAuthorizationFilter, ClaimsIdentityAuthorizationFilter>(
            // Not sure whether you want white list or black list
            // but here is where it is set.
            new InjectionProperty("WhiteListMode", true),
            // For white list security, you can also set the default
            // permissions that every action gets if it is not overridden.
            new InjectionProperty("Permissions", "read"));
        container.RegisterType<IAuthorizationService, ClaimsIdentityAuthorizationService>();

        // register all your components with the container here
        // it is NOT necessary to register your controllers

        // e.g. container.RegisterType<ITestService, TestService>();    
        RegisterTypes(container);

        return container;
    }

    public static void RegisterTypes(IUnityContainer container)
    {

    }
}

И затем в вашем контроллере, для безопасности черного списка, вам нужно будет украсить каждое действие (или контроллер), чтобы заблокировать его.

public class HomeController : Controller
{

    // This is not secured at all
    public ActionResult Index()
    {
        return View();
    }

    [ClaimsAuthorizeAccountAccess("read")]
    public ActionResult About()
    {
        ViewBag.Message = "Your application description page.";

        return View();
    }

    [ClaimsAuthorizeAccountAccess("read,edit")]
    public ActionResult Contact()
    {
        ViewBag.Message = "Your contact page.";

        return View();
    }
}

Для безопасности белого списка вам нужно только украсить действия, к которым у каждого есть доступ AllowAnonymous или добавить ClaimsIdentityAuthorizeAttribute с более или менее ограничительными разрешениями, чем глобальный уровень или уровень контроллера.

public class HomeController : Controller
{
    // This is not secured at all
    [AllowAnonymous]
    public ActionResult Index()
    {
        return View();
    }

    // This is secured by ClaimsAuthorizeAccountAccess (read permission)
    public ActionResult About()
    {
        ViewBag.Message = "Your application description page.";

        return View();
    }

    [ClaimsAuthorizeAccountAccess("read,edit")]
    public ActionResult Contact()
    {
        ViewBag.Message = "Your contact page.";

        return View();
    }
}

Вы не можете внедрить зависимости как параметры конструктора в фильтры действий, потому что они реализованы как атрибуты в C#. Вы должны решить их, используя DependencyResolver.Current, Это своего рода сервисный локатор, и это не круто, но у вас нет выбора. ASP.NET MVC не использует контейнер DI для создания экземпляров фильтра действий.

public ClaimsAuthorizeAccountAccess(params string[] permissions)
{
    _permissions = permissions;
    _accountRepository = DependencyResolver.Current.GetService<IAccountRepository>();
} 
Другие вопросы по тегам