#asp.net #asp.net-mvc #permissions #access-control
#asp.net #asp.net-mvc #разрешения #управление доступом
Вопрос:
У меня есть ASP.NET Сайт MVC, который использует шаблон репозитория для доступа и изменения данных. Мой интерфейс репозитория передается каждому контроллеру через их конструкторы. Я также использую Ninject для ввода моего конкретного типа репозитория через DependencyResolver.SetResolver()
.
Пользователи на моем сайте должны иметь доступ только к тем данным, которые им назначены. Что я пытаюсь выяснить, так это где я должен проверить, что у текущего пользователя есть разрешение на выполнение текущего запроса?
Например, пользователь может отправить форму подтверждения удаления элемента по URL / Item / Delete / 123, которая удалит элемент с идентификатором 123. Однако я не хочу, чтобы пользователь мог манипулировать этим URL и в конечном итоге удалять элемент другого пользователя.
Должен ли я добавить код проверки пользователя в контроллеры, чтобы первое, что делает каждый метод действия, это проверяет, владеет ли текущий пользователь данными, которые они пытаются изменить? Похоже, что это добавило бы слишком много интеллектуальных данных контроллеру, который должен быть довольно тонким.
Я думаю, было бы разумнее добавить эту логику в мой репозиторий? Например, у меня может быть репозиторий.GetItem (идентификатор int, строка user), а не репозиторий.GetItem (int id), который будет генерировать исключение, если «пользователь» не владеет запрошенным элементом.
В качестве альтернативы я подумал, что каждый созданный экземпляр моего репозитория может быть назначен определенному пользователю при его создании. Затем этот пользовательский репозиторий будет генерировать исключения, если когда-либо будет предпринята попытка доступа или изменения данных, которые не принадлежат текущему пользователю. Затем контроллеру просто нужно будет перехватить эти исключения и перенаправить пользователя на страницу с ошибкой, если таковая будет обнаружена.
Ответ №1:
Недавно я столкнулся с точно такой же проблемой. В итоге я выбрал пользовательский 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 amp;amp; !isPageOwner || MustBeInRoleAndNotPageOwner amp;amp; 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 amp;amp; 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);
}
}
Пара замечаний:
Чтобы избежать «волшебных строк», я использовал массив UserRole
значений enum вместо одной строки. Кроме того, я создал его для обработки нескольких сценариев, с которыми я столкнулся:
- Пользователь должен быть в роли или владельцем страницы / данных (например, администратор может редактировать данные любого пользователя, и каждый может редактировать свои собственные данные)
- Пользователь должен быть в роли и владельцем страницы / данных (например, пользователь может редактировать только свою собственную страницу / данные — обычно используется без каких-либо ограничений роли)
- Пользователь должен быть в роли, а не владельцем страницы / данных (например, администратор может редактировать любую страницу / данные, кроме своих собственных — скажем, чтобы администратор не мог удалить свою учетную запись)
- Ни одному пользователю не разрешено просматривать эту страницу
AllowAnyRolesIfNoneSpecified = false
(например, у вас есть метод контроллера для страницы, которая не существует, но вам нужно включить метод, потому что ваш контроллер наследуется от базового класса, который имеет этот метод)
Вот пример объявления атрибута:
[AccountAuthorize(UserRole.Admin, MustBeInRoleAndNotPageOwner = true)]
public override ActionResult DeleteConfirmed(int id)
{
...
}
(Это означает, что администратор может удалить любую учетную запись, кроме своей собственной.)
Комментарии:
1. 1. Просто предложение. Вы могли бы использовать
params UserRole[] roles
в своем конструкторе, чтобы уйти от этого ужасного синтаксиса массива в вашем примере использования.2. @Ryan, отличное предложение. Я был разочарован уродливым синтаксисом, когда впервые писал этот код, но тогда я не понимал, что вы можете смешивать параметры конструктора и свойства в объявлениях атрибутов. Оказывается, вы можете, и результат намного чище. Я только что вставил новый код в свой ответ. Спасибо!