Где я должен разместить код для разрешения доступа на моем сайте ASP.NET MVC? - PullRequest
1 голос
/ 18 мая 2011

У меня есть сайт ASP.NET MVC, который использует шаблон репозитория для доступа и изменения данных.Мой интерфейс репозитория передается каждому контроллеру через их конструкторы.Я также использую Ninject для ввода моего конкретного типа хранилища через DependencyResolver.SetResolver().

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

Например, пользователь может отправить форму подтверждения удаления элемента в URL / Item / Delete /123, который удалит элемент с идентификатором 123. Однако я не хочу, чтобы пользователь мог манипулировать этим URL-адресом и в конечном итоге удалить элемент другого пользователя.

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

Я думаю, что было бы разумнее добавить эту логику в мой репозиторий?Например, у меня может быть Repository.GetItem (int id, string user), а не Repository.GetItem (int id), который будет генерировать исключение, если «пользователь» не владеет запрошенным элементом.

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

1 Ответ

5 голосов
/ 18 мая 2011

Недавно я столкнулся с точно такой же проблемой. В итоге я выбрал собственный ActionFilter, который наследуется от AuthorizeAttribute.

Он в основном имеет ту же функциональность, что и Authorize (проверяет, принадлежит ли пользователь хотя бы к одной из перечисленных ролей), но также добавляет возможность проверки, владеет ли пользователь конкретными данными.

Вот код, который вы можете использовать в качестве примера. Если что-то не понятно, пожалуйста, прокомментируйте, и я постараюсь объяснить.

[ Редактировать - Основываясь на предложении Райана, я сделал params UserRole[] параметром конструктора вместо открытого свойства и добавил AllowAnyRolesIfNoneSpecified.]

[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, Inherited = true, AllowMultiple = false)]
public class AccountAuthorizeAttribute : AuthorizeAttribute
{
    private readonly UserRole[] _userRoles;

    public bool MustBeInRoleOrPageOwner { get; set; }
    public bool MustBeInRoleAndPageOwner { get; set; }
    public bool MustBeInRoleAndNotPageOwner { get; set; }
    public bool AllowAnyRolesIfNoneSpecified { get; set; }
    public string AccessDeniedViewName { get; set; }

    public AccountAuthorizeAttribute(params UserRole[] userRoles)
    {
        _userRoles = userRoles;
        MustBeInRoleOrPageOwner = false;
        MustBeInRoleAndPageOwner = false;
        MustBeInRoleAndNotPageOwner = false;
        AllowAnyRolesIfNoneSpecified = true;
        AccessDeniedViewName = "AccessDenied";
    }

    protected void CacheValidateHandler(HttpContext context, object data, ref HttpValidationStatus validationStatus)
    {
        validationStatus = OnCacheAuthorization(new HttpContextWrapper(context));
    }

    public override void OnAuthorization(AuthorizationContext filterContext)
    {
        if (!filterContext.HttpContext.User.Identity.IsAuthenticated)
        {
            ShowLogOnPage(filterContext);
            return;
        }
        using (var dbContext = new MainDbContext())
        {
            var accountService = new AccountService(dbContext);
            var emailAddress = filterContext.HttpContext.User.Identity.Name;
            if (IsUserInRole(accountService, emailAddress))
            {
                var isPageOwner = IsUserPageOwner(filterContext, dbContext, accountService, emailAddress);
                if (MustBeInRoleAndPageOwner && !isPageOwner || MustBeInRoleAndNotPageOwner && isPageOwner)
                    ShowAccessDeniedPage(filterContext);
            }
            else
            {
                if (!MustBeInRoleOrPageOwner)
                    ShowAccessDeniedPage(filterContext);
                else if (!IsUserPageOwner(filterContext, dbContext, accountService, emailAddress))
                    ShowAccessDeniedPage(filterContext);
            }
        }
    }

    private bool IsUserInRole(AccountService accountService, string emailAddress)
    {
        if (_userRoles.Length == 0 && AllowAnyRolesIfNoneSpecified) return true;
        return accountService.IsUserInRole(emailAddress, _userRoles);
    }

    protected virtual bool IsUserPageOwner(
        AuthorizationContext filterContext, MainDbContext dbContext, AccountService accountService, string emailAddress)
    {
        var id = GetRouteId(filterContext);
        return IsUserPageOwner(dbContext, accountService, emailAddress, id);
    }

    protected int GetRouteId(AuthorizationContext filterContext)
    {
        return Convert.ToInt32(filterContext.RouteData.Values["id"]);
    }

    private bool IsUserPageOwner(MainDbContext dbContext, AccountService accountService, string emailAddress, int id)
    {
        return accountService.IsUserPageOwner(emailAddress, id);
    }

    private void ShowLogOnPage(AuthorizationContext filterContext)
    {
        filterContext.Result = new HttpUnauthorizedResult();
    }

    private void ShowAccessDeniedPage(AuthorizationContext filterContext)
    {
        filterContext.Result = new ViewResult { ViewName = "ErrorPages/" + AccessDeniedViewName };
    }

    private void PreventPageFromBeingCached(AuthorizationContext filterContext)
    {
        var cachePolicy = filterContext.HttpContext.Response.Cache;
        cachePolicy.SetProxyMaxAge(new TimeSpan(0));
        cachePolicy.AddValidationCallback(CacheValidateHandler, null);
    }
}

Пара замечаний:

Чтобы избежать "волшебных строк", я использовал массив значений enum UserRole вместо одной строки. Кроме того, я построил его для нескольких сценариев, с которыми я столкнулся:

  • Пользователь должен быть в роли или владельцем страницы / данных (например, администратор может редактировать любые данные, а любой может редактировать свои собственные данные)
  • Пользователь должен быть в роли и владельцем страницы / данных (например, пользователь может редактировать только свою собственную страницу / данные - обычно используется без каких-либо ограничений роли)
  • Пользователь должен быть в роли, а не - владельцем страницы / данных (например, администратор может редактировать любую страницу / данные, кроме своей, например, чтобы администратор не мог удалить свою собственную). счет)
  • Пользователю не разрешено просматривать эту страницу, AllowAnyRolesIfNoneSpecified = false (например, у вас есть метод контроллера для страницы, которая не существует, но вам нужно включить метод, потому что ваш контроллер наследует от базового класса, который имеет этот метод )

Вот пример объявления атрибута:

[AccountAuthorize(UserRole.Admin, MustBeInRoleAndNotPageOwner = true)]
public override ActionResult DeleteConfirmed(int id)
{
    ...
}

(Это означает, что администратор может удалить любую учетную запись, кроме своей.)

...