Нестандартная система разграничения доступа в приложении ASP. Net MVC
В одном из проектов, написанном на ASP. Net MVC 3 потребовалась авторизация пользователей.
Проект предусматривал несколько фиксированных ролей пользователей и авторизацию на уровне контроллеров и действий.
Казалось бы, что проще, используем существующее решение в виде атрибута [Authorize(Roles = «Role1»)], проставленного у контроллера или действия.
Так и было сделано, но оказалось что это оооочень неудобно в применении:
- Атрибуты разбросаны по всему проекту, приходилось держать в голове кто что и где может сделать.
- В случае добавления новой роли приходилось открывать все (!) файлы с контроллерами и добавлять соответствующие записи к атрибутам.
- Тоже самое в случае переименования роли (хотя тут позже был найден выход путем вынесения названия ролей в static const string).
Было решено применить иной подход:
- оставить авторизацию на уровне контроллер/действие
- вынести назначение прав в одно место (файл), что бы все было перед глазами
- сохранить или улучшить управление правами путем внедрения понятий «все действия в контроллере», «любая роль» и т.д.
Перейдем сразу к решению
Основа решения — добавление фильтра «Безопасность» (класс MySecurityFilter) к ряду уже зарегистрированных (пример использования кода в конце статьи).
Вот исходный код этого фильтра:
public class MySecurityFilter : FilterAttribute, IAuthorizationFilter { public void OnAuthorization(AuthorizationContext filterContext) { var role = filterContext.HttpContext.User.GetRole(); var action = (string)filterContext.RouteData.Values["action"]; var controllerType = filterContext.Controller.GetType(); if (!MySecurity.Instance.IsGrantAction(role, controllerType, action)) { filterContext.Result = new HttpUnauthorizedResult(); } } }
Думаю особых вопросов здесь не должно возникнуть. Мы наследуемся от интерфейса IAuthorizationFilter и обрабатываем событие OnAuthorization:
- Получаем контроллер и экшн
- Запрашиваем у инстанс MySecurity разрешено ли нам действие
- Если запрещено задаем filterContext.Result = new HttpUnauthorizedResult();
Посмотрим, что внутри у MySecurity:
{ private static readonly MySecurity InstanceField = new MySecurity(); public static MySecurity Instance{get { return InstanceField;}} private readonly Dictionary<string, List<string>> dict; public MySecurity() { dict = new Dictionary<string, List<string>>(); } public PermissionTableBuilder Table() { return new PermissionTableBuilder(this); } // методы класса, описание ниже // ... }
Несколько замечаний по данному классу:
1. Словарь содержит таблицу прав доступа. Ключ — название роли. Значение — список пермишшнов, в формате «ControllerName+Action»
Стоит отметить, что для ролей допустимо указание "*" и "?" (любой авторизованный и просто любой пользователь).
Для Action допустимо указание "*" — любой экшн в контроллере.
2. PermissionTableBuilder — специальный класс для упрощения построения этой таблицы
Методы класса MySecurity
Добавляем права:
public class MySecurity public void AddPerm(string role, string perm) { if (!dict.ContainsKey(role)) { dict[role] = new List<string>(); } dict[role].Add(perm); }
Проверяем права доступа:
public bool IsGrant(string role, string perm) { switch (role) { case "?": return Any(perm); case "*": return Any(perm) || All(perm); default: return Any(perm) || All(perm) || Role(role, perm); } } // проверяем права доступа для любого пользователя private bool Any(string perm) { return Role("?", perm); } // проверяем права доступа для любого авторозованного, т.е. с ролью пользователя private bool All(string perm) { return Role("*", perm); } // проверяем права доступа для конкретной роли private bool Role(string role, string perm) { if (dict.ContainsKey(role)) { return dict[role].Contains(perm); } return false; }
Дополнительные методы для упрощения проверки прав доступа:
public bool IsGrantAction(string role, Type controllerType, string action) { return IsGrantAction(role, controllerType.Name, action); } public bool IsGrantAction(string role, string controllerName, string action) { // проверим сначала нет ли Permission, в котором разрешены любые действия if (IsGrant(role, MySecurityHelper.GetPermission(controllerName, "*"))) { return true; } return IsGrant(role, MySecurityHelper.GetPermission(controllerName, action)); }
Класс PermissionTableBuilder, служащий для упрощения построения таблицы прав доступа.
Его функционал может показаться непонятным, поэтому стоит взглянуть на пример его использования в конце статьи.
public class PermissionTableBuilder { private readonly MySecurity table; private readonly List<string > roles; public PermissionTableBuilder(MySecurity table) { this.table = table; roles = new List<string>(); } public PermissionTableBuilder WithRole(string role) { roles.Add(role); return this; } public PermissionTableBuilder AddPerm<TController>(params string [] actions) { ProcessPerm(actions, x=> MySecurityHelper.GetPermission(typeof (TController), x)); return this; } public PermissionTableBuilder AddPerm(params string[] actions) { ProcessPerm(actions, x=>x); return this; } private void ProcessPerm(string[] actions, Func<string, string> getPermNameFunc) { if (roles.Count == 0) { throw new InvalidOperationException("Call withRole first"); } foreach (var action in actions) { foreach (var role in roles) { string perm = getPermNameFunc(action); table.AddPerm(role, perm); } } } }
Тройка вспомогательных методов, оформленных отдельно:
public static class MySecurityHelper { public static string GetPermission(Type controllerType, string actionName) { return String.Format("{0}+{1}", controllerType.Name, actionName); } public static string GetPermission(string controllerName, string actionName) { if (controllerName.EndsWith("Controller")) { return String.Format("{0}+{1}", controllerName, actionName); } return String.Format("{0}Controller+{1}", controllerName, actionName); } public static string GetRole(this IPrincipal user) { var principal = user as RolePrincipal; if (principal == null) { return "?"; } return principal.GetRoles().SingleOrDefault() ?? "?"; } }
А теперь смотрим на пример использования всей системы целиком:
Global.asax:
public static void RegisterGlobalFilters(GlobalFilterCollection filters) { // прочие фильтры // ... filters.Add(new MySecurityFilter()); } protected void Application_Start() { // ... PermissionListCreator.Create(MySecurity.Instance); // ... }
Вспомогательный класс PermissionListCreator.cs, содержащий код, создающий таблицу прав:
public static class PermissionListCreator { public static void Create(MySecurity security) { // Открытые для всех пользователей страницы security.Table().WithRole("?").AddPerm<HomeController>("*"); security.Table().WithRole("?").AddPerm<LogOnController>("*"); security.Table().WithRole("?").AddPerm<ErrorController>("*"); // Личный кабинет security.Table().WithRole("*").AddPerm<CabinetController>("*"); // Управление пользователями security.Table(). WithRole(MyRoles.Director).AddPerm<UsersController>("*"); security.Table(). WithRole(MyRoles.Admin).AddPerm<UsersController>("Add", "Edit"); // просмотр лога security.Table(). WithRole(MyRoles.Admin). WithRole(MyRoles.Director). WithRole(MyRoles.Commander). WithRole(MyRoles.Developer).AddPerm<LogsController>("*"); // и так далее, со всеми контроллерами }
- разрешили доступ всем пользователям к домашней странице проекта, форме авторизации, и сообщениям об ошибках.
- разрешили всем авторизованным пользователям доступ к личному кабинету
- разрешили администратору добавлять и изменять пользователей(но не удалять), а директору и удалять
- разрешили 4-м ролям просматривать лог
Обратите внимание, что все явно не разрешенные действия — запрещены!
На мой взгляд, получилось достаточно простое в использование и надежное решение. Правда сфера его применения ограничена, но ничто не мешает использовать его с любой другой системой разграничения доступа.