ASP.NET MVC - альтернатива провайдеру роли?
Я стараюсь избегать использования провайдера роли и провайдера членства, так как, по моему мнению, он слишком неуклюжий, и поэтому я пытаюсь сделать свою собственную "версию", которая является менее неуклюжей и более управляемой/гибкой. Теперь мой вопрос.. есть ли альтернатива провайдеру роли, которая приличная? (Я знаю, что я могу выполнять персонализацию роли, провайдера членства и т.д.)
Более управляемым/гибким я имею в виду, что я ограничен использованием статического класса Roles и не реализую непосредственно на моем уровне сервиса, который взаимодействует с контекстом базы данных, вместо этого я обязан использовать статический класс Roles, который имеет собственный контекст базы данных и т.д., также имена таблиц ужасны.
Спасибо заранее.
Ответы
Ответ 1
Я нахожусь в той же лодке, что и вы - я всегда ненавидел RoleProviders. Да, они здорово, если вы хотите завести вещи на небольшой веб-сайт, но они не очень реалистичны. Основной недостаток, который я всегда нашел, заключается в том, что они привязывают вас непосредственно к ASP.NET.
То, как я отправился на недавний проект, - это определение пары интерфейсов, которые являются частью уровня сервиса (ПРИМЕЧАНИЕ: я упростил их совсем немного, но вы можете легко добавить к ним):
public interface IAuthenticationService
{
bool Login(string username, string password);
void Logout(User user);
}
public interface IAuthorizationService
{
bool Authorize(User user, Roles requiredRoles);
}
Тогда ваши пользователи могут иметь Roles
перечисление:
public enum Roles
{
Accounting = 1,
Scheduling = 2,
Prescriptions = 4
// What ever else you need to define here.
// Notice all powers of 2 so we can OR them to combine role permissions.
}
public class User
{
bool IsAdministrator { get; set; }
Roles Permissions { get; set; }
}
Для вашего IAuthenticationService
у вас может быть базовая реализация, которая выполняет стандартную проверку пароля, а затем вы можете иметь FormsAuthenticationService
, которая делает немного больше, например, настройку файла cookie и т.д. Для вашего AuthorizationService
d нужно что-то вроде этого:
public class AuthorizationService : IAuthorizationService
{
public bool Authorize(User userSession, Roles requiredRoles)
{
if (userSession.IsAdministrator)
{
return true;
}
else
{
// Check if the roles enum has the specific role bit set.
return (requiredRoles & user.Roles) == requiredRoles;
}
}
}
В дополнение к этим базовым службам вы можете легко добавить сервисы к паролям reset и т.д.
Поскольку вы используете MVC, вы можете сделать авторизацию на уровне действия с помощью ActionFilter
:
public class RequirePermissionFilter : IAuthorizationFilter
{
private readonly IAuthorizationService authorizationService;
private readonly Roles permissions;
public RequirePermissionFilter(IAuthorizationService authorizationService, Roles requiredRoles)
{
this.authorizationService = authorizationService;
this.permissions = requiredRoles;
this.isAdministrator = isAdministrator;
}
private IAuthorizationService CreateAuthorizationService(HttpContextBase httpContext)
{
return this.authorizationService ?? new FormsAuthorizationService(httpContext);
}
public void OnAuthorization(AuthorizationContext filterContext)
{
var authSvc = this.CreateAuthorizationService(filterContext.HttpContext);
// Get the current user... you could store in session or the HttpContext if you want too. It would be set inside the FormsAuthenticationService.
var userSession = (User)filterContext.HttpContext.Session["CurrentUser"];
var success = authSvc.Authorize(userSession, this.permissions);
if (success)
{
// Since authorization is performed at the action level, the authorization code runs
// after the output caching module. In the worst case this could allow an authorized user
// to cause the page to be cached, then an unauthorized user would later be served the
// cached page. We work around this by telling proxies not to cache the sensitive page,
// then we hook our custom authorization code into the caching mechanism so that we have
// the final say on whether or not a page should be served from the cache.
var cache = filterContext.HttpContext.Response.Cache;
cache.SetProxyMaxAge(new TimeSpan(0));
cache.AddValidationCallback((HttpContext context, object data, ref HttpValidationStatus validationStatus) =>
{
validationStatus = this.OnCacheAuthorization(new HttpContextWrapper(context));
}, null);
}
else
{
this.HandleUnauthorizedRequest(filterContext);
}
}
private void HandleUnauthorizedRequest(AuthorizationContext filterContext)
{
// Ajax requests will return status code 500 because we don't want to return the result of the
// redirect to the login page.
if (filterContext.RequestContext.HttpContext.Request.IsAjaxRequest())
{
filterContext.Result = new HttpStatusCodeResult(500);
}
else
{
filterContext.Result = new HttpUnauthorizedResult();
}
}
public HttpValidationStatus OnCacheAuthorization(HttpContextBase httpContext)
{
var authSvc = this.CreateAuthorizationService(httpContext);
var userSession = (User)httpContext.Session["CurrentUser"];
var success = authSvc.Authorize(userSession, this.permissions);
if (success)
{
return HttpValidationStatus.Valid;
}
else
{
return HttpValidationStatus.IgnoreThisRequest;
}
}
}
Что вы можете украсить на своих действиях контроллера:
[RequirePermission(Roles.Accounting)]
public ViewResult Index()
{
// ...
}
Преимущество такого подхода заключается в том, что вы можете также использовать инъекцию зависимостей и контейнер IoC для подключения к сети. Кроме того, вы можете использовать его для нескольких приложений (а не только для ASP.NET). Вы должны использовать ORM для определения соответствующей схемы.
Если вам нужна дополнительная информация о службах FormsAuthorization/Authentication
или о том, куда идти отсюда, дайте мне знать.
EDIT: Чтобы добавить "обрезку безопасности", вы можете сделать это с помощью HtmlHelper. Это, вероятно, нужно немного больше... но вы поняли идею.
public static bool SecurityTrim<TModel>(this HtmlHelper<TModel> source, Roles requiredRoles)
{
var authorizationService = new FormsAuthorizationService();
var user = (User)HttpContext.Current.Session["CurrentUser"];
return authorizationService.Authorize(user, requiredRoles);
}
И затем внутри вашего представления (используя синтаксис Razor здесь):
@if(Html.SecurityTrim(Roles.Accounting))
{
<span>Only for accounting</span>
}
EDIT: UserSession
будет выглядеть примерно так:
public class UserSession
{
public int UserId { get; set; }
public string UserName { get; set; }
public bool IsAdministrator { get; set; }
public Roles GetRoles()
{
// make the call to the database or whatever here.
// or just turn this into a property.
}
}
Таким образом, мы не раскрываем хеш пароля и все другие детали внутри сеанса текущего пользователя, так как они действительно не нужны для жизни сеанса пользователя.
Ответ 2
Я внедрил поставщик роли, основанный на сообщении @TheCloudlessSky здесь. Есть несколько вещей, которые, как я думал, я могу добавить и поделиться тем, что я сделал.
Сначала, если вы хотите использовать класс RequirepPermission
для своих фильтров действий как атрибут, вам нужно реализовать класс ActionFilterAttribute
для класса RequirepPermission
.
Классы интерфейса IAuthenticationService
и IAuthorizationService
public interface IAuthenticationService
{
void SignIn(string userName, bool createPersistentCookie);
void SignOut();
}
public interface IAuthorizationService
{
bool Authorize(UserSession user, string[] requiredRoles);
}
FormsAuthenticationService
класс
/// <summary>
/// This class is for Form Authentication
/// </summary>
public class FormsAuthenticationService : IAuthenticationService
{
public void SignIn(string userName, bool createPersistentCookie)
{
if (String.IsNullOrEmpty(userName)) throw new ArgumentException(@"Value cannot be null or empty.", "userName");
FormsAuthentication.SetAuthCookie(userName, createPersistentCookie);
}
public void SignOut()
{
FormsAuthentication.SignOut();
}
}
UserSession
calss
public class UserSession
{
public string UserName { get; set; }
public IEnumerable<string> UserRoles { get; set; }
}
Другим пунктом является класс FormsAuthorizationService
и как мы можем назначить пользователя httpContext.Session["CurrentUser"]
. Мой подход в этой ситуации - создать новый экземпляр класса userSession и напрямую назначить пользователя из httpContext.User.Identity.Name
переменной userSession, как вы можете видеть в классе FormsAuthorizationService
.
[AttributeUsageAttribute(AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Constructor | AttributeTargets.Method, Inherited = false)]
public class RequirePermissionAttribute : ActionFilterAttribute, IAuthorizationFilter
{
#region Fields
private readonly IAuthorizationService _authorizationService;
private readonly string[] _permissions;
#endregion
#region Constructors
public RequirePermissionAttribute(string requiredRoles)
{
_permissions = requiredRoles.Trim().Split(',').ToArray();
_authorizationService = null;
}
#endregion
#region Methods
private IAuthorizationService CreateAuthorizationService(HttpContextBase httpContext)
{
return _authorizationService ?? new FormsAuthorizationService(httpContext);
}
public void OnAuthorization(AuthorizationContext filterContext)
{
var authSvc = CreateAuthorizationService(filterContext.HttpContext);
// Get the current user... you could store in session or the HttpContext if you want too. It would be set inside the FormsAuthenticationService.
if (filterContext.HttpContext.Session == null) return;
if (filterContext.HttpContext.Request == null) return;
var success = false;
if (filterContext.HttpContext.Session["__Roles"] != null)
{
var rolesSession = filterContext.HttpContext.Session["__Roles"];
var roles = rolesSession.ToString().Trim().Split(',').ToList();
var userSession = new UserSession
{
UserName = filterContext.HttpContext.User.Identity.Name,
UserRoles = roles
};
success = authSvc.Authorize(userSession, _permissions);
}
if (success)
{
// Since authorization is performed at the action level, the authorization code runs
// after the output caching module. In the worst case this could allow an authorized user
// to cause the page to be cached, then an unauthorized user would later be served the
// cached page. We work around this by telling proxies not to cache the sensitive page,
// then we hook our custom authorization code into the caching mechanism so that we have
// the final say on whether or not a page should be served from the cache.
var cache = filterContext.HttpContext.Response.Cache;
cache.SetProxyMaxAge(new TimeSpan(0));
cache.AddValidationCallback((HttpContext context, object data, ref HttpValidationStatus validationStatus) =>
{
validationStatus = OnCacheAuthorization(new HttpContextWrapper(context));
}, null);
}
else
{
HandleUnauthorizedRequest(filterContext);
}
}
private static void HandleUnauthorizedRequest(AuthorizationContext filterContext)
{
// Ajax requests will return status code 500 because we don't want to return the result of the
// redirect to the login page.
if (filterContext.RequestContext.HttpContext.Request.IsAjaxRequest())
{
filterContext.Result = new HttpStatusCodeResult(500);
}
else
{
filterContext.Result = new HttpUnauthorizedResult();
}
}
private HttpValidationStatus OnCacheAuthorization(HttpContextBase httpContext)
{
var authSvc = CreateAuthorizationService(httpContext);
if (httpContext.Session != null)
{
var success = false;
if (httpContext.Session["__Roles"] != null)
{
var rolesSession = httpContext.Session["__Roles"];
var roles = rolesSession.ToString().Trim().Split(',').ToList();
var userSession = new UserSession
{
UserName = httpContext.User.Identity.Name,
UserRoles = roles
};
success = authSvc.Authorize(userSession, _permissions);
}
return success ? HttpValidationStatus.Valid : HttpValidationStatus.IgnoreThisRequest;
}
return 0;
}
#endregion
}
internal class FormsAuthorizationService : IAuthorizationService
{
private readonly HttpContextBase _httpContext;
public FormsAuthorizationService(HttpContextBase httpContext)
{
_httpContext = httpContext;
}
public bool Authorize(UserSession userSession, string[] requiredRoles)
{
return userSession.UserRoles.Any(role => requiredRoles.Any(item => item == role));
}
}
то в вашем контроллере после аутентификации пользователя вы можете получить роли из базы данных и назначить ее сеансу ролей:
var roles = Repository.GetRolesByUserId(Id);
if (ControllerContext.HttpContext.Session != null)
ControllerContext.HttpContext.Session.Add("__Roles",roles);
FormsService.SignIn(collection.Name, true);
После выхода пользователя из системы вы можете очистить сеанс
FormsService.SignOut();
Session.Abandon();
return RedirectToAction("Index", "Account");
Предостережение в этой модели заключается в том, что при входе пользователя в систему, если роль назначена пользователю, авторизация не работает, если он не выходит из системы и не возвращается в систему.
Другое дело, что нет необходимости иметь отдельный класс для ролей, поскольку мы можем получать роли напрямую из базы данных и устанавливать его в сеанс ролей в контроллере.
После того как вы закончите реализацию всех этих кодов, последний шаг - привязать этот атрибут к вашим методам в вашем контроллере:
[RequirePermission("Admin,DM")]
public ActionResult Create()
{
return View();
}
Ответ 3
Если вы используете Injection Dependency Injection от Castle Windsor, вы можете вводить списки RoleProviders, которые могут использоваться для определения прав пользователя из любого источника, который вы хотите реализовать.
http://ivida.co.uk/2011/05/18/mvc-getting-user-roles-from-multiple-sources-register-and-resolve-arrays-of-dependencis-using-the-fluent-api/
Ответ 4
Вам не нужно использовать статический класс для ролей. Например, SqlRoleProvider позволяет определить роли в базе данных.
Конечно, если вы хотите получать роли с вашего собственного уровня обслуживания, не так сложно создать собственный поставщик ролей - на самом деле не так много методов для реализации.
Ответ 5
Вы можете реализовать свое собственное членство и роль поставщиков путем переопределения соответствующих интерфейсов.
Если вы хотите начать с нуля, обычно эти типы реализуются как пользовательский http-модуль, который хранит учетные данные пользователей либо в httpcontext или сеанс. В любом случае вы, вероятно, захотите установить cookie с каким-то токеном аутентификации.