framework - ASP.NET MVC: ¿alternativa al proveedor de roles?
identity owin (5)
Estoy en el mismo barco que tú. Siempre he odiado a los RoleProviders. Sí, son geniales si quieres poner las cosas en marcha para un sitio web pequeño, pero no son muy realistas. La desventaja principal que siempre he encontrado es que te atan directamente a ASP.NET.
La forma en que elegí un proyecto reciente fue definir un par de interfaces que forman parte de la capa de servicio (NOTA: las simplifiqué un poco, pero podría agregarlas fácilmente):
public interface IAuthenticationService
{
bool Login(string username, string password);
void Logout(User user);
}
public interface IAuthorizationService
{
bool Authorize(User user, Roles requiredRoles);
}
Entonces sus usuarios podrían tener una enumeración 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; }
}
Para su IAuthenticationService
, podría tener una implementación base que haga una verificación estándar de contraseñas y luego podría tener un FormsAuthenticationService
que haga un poco más, como configurar la cookie, etc. Para su AuthorizationService
, necesitaría algo como esto:
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;
}
}
}
Además de estos servicios básicos, puede agregar fácilmente servicios para restablecer contraseñas, etc.
Dado que está utilizando MVC, puede hacer una autorización en el nivel de acción utilizando un 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;
}
}
}
Que luego puedes decorar en tus acciones de controlador:
[RequirePermission(Roles.Accounting)]
public ViewResult Index()
{
// ...
}
La ventaja de este enfoque es que también puede usar la inyección de dependencia y un contenedor IoC para conectar las cosas. Además, puede usarlo en múltiples aplicaciones (no solo en ASP.NET). Utilizaría su ORM para definir el esquema apropiado.
Si necesita más detalles acerca de los servicios de FormsAuthorization/Authentication
o a dónde ir desde aquí, hágamelo saber.
EDITAR: para agregar "recorte de seguridad", puede hacerlo con un HtmlHelper. Esto probablemente necesite un poco más ... pero entiendes la idea.
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);
}
Y luego dentro de su vista (usando la sintaxis de Razor aquí):
@if(Html.SecurityTrim(Roles.Accounting))
{
<span>Only for accounting</span>
}
EDITAR: La UserSession
se vería así:
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.
}
}
De esta manera, no exponemos el hash de la contraseña y todos los demás detalles dentro de la sesión del usuario actual, ya que realmente no son necesarios para la duración de la sesión del usuario.
Estoy tratando de evitar el uso del Proveedor de funciones y el Proveedor de membresía porque es demasiado torpe en mi opinión, y por lo tanto estoy tratando de crear mi propia "versión", que sea menos torpe y más manejable / flexible. Ahora es mi pregunta ... ¿hay una alternativa al Proveedor de funciones que sea decente? (Sé que puedo hacer Provier personalizado de roles, membresía, etc.)
Por más manejable / flexible quiero decir que estoy limitado a usar la clase estática Roles y no implementar directamente en mi capa de servicio que interactúe con el contexto de la base de datos, en cambio estoy obligado a usar la clase estática Roles que tiene su propio contexto de base de datos etc., también los nombres de las tablas son horribles ...
Gracias por adelantado.
Implementé un proveedor de roles basado en la publicación @TheCloudlessSky aquí. Hay pocas cosas que pensé que podía agregar y compartir lo que hice. Primero, si desea utilizar la clase RequirepPermission
para sus filtros de acción como un atributo, necesita implementar la clase RequirepPermission
para la clase RequirepPermission
.
Clases de interfaz IAuthenticationService
y IAuthorizationService
public interface IAuthenticationService
{
void SignIn(string userName, bool createPersistentCookie);
void SignOut();
}
public interface IAuthorizationService
{
bool Authorize(UserSession user, string[] requiredRoles);
}
Clase 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; }
}
Otro punto es la clase FormsAuthorizationService
y cómo podemos asignar un usuario a httpContext.Session["CurrentUser"]
. Mi enfoque en esta situación es crear una nueva instancia de clase userSession y asignar directamente el usuario de httpContext.User.Identity.Name
a la variable userSession como se puede ver en la clase 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));
}
}
Luego, en su controlador después de que el usuario se haya autenticado, puede obtener roles de la base de datos y asignarlos a la sesión de roles:
var roles = Repository.GetRolesByUserId(Id);
if (ControllerContext.HttpContext.Session != null)
ControllerContext.HttpContext.Session.Add("__Roles",roles);
FormsService.SignIn(collection.Name, true);
Después de que el usuario cierre sesión en el sistema, puede borrar la sesión
FormsService.SignOut();
Session.Abandon();
return RedirectToAction("Index", "Account");
La advertencia en este modelo es que, cuando el usuario inicia sesión en el sistema, si se le asigna una función, la autorización no funciona a menos que cierre la sesión y vuelva a iniciar sesión en el sistema.
Otra cosa es que no hay necesidad de tener una clase separada para los roles, ya que podemos obtener roles directamente de la base de datos y configurarlos en una sesión de roles en un controlador.
Una vez que haya terminado de implementar todos estos códigos, un último paso es vincular este atributo a sus métodos en su controlador:
[RequirePermission("Admin,DM")]
public ActionResult Create()
{
return View();
}
No necesita usar una clase estática para roles. Por ejemplo, SqlRoleProvider permite definir los roles en una base de datos.
Por supuesto, si desea recuperar roles desde su propia capa de servicio, no es tan difícil crear su propio proveedor de roles, realmente no hay tantos métodos para implementar.
Puede implementar su propia membership y proveedores de role anulando las interfaces apropiadas.
Si desea comenzar desde cero, normalmente este tipo de cosas se implementan como un módulo http personalizado que almacena las credenciales de los usuarios, ya sea en el contexto HTTP o en la sesión. De cualquier manera, es probable que desee establecer una cookie con algún tipo de token de autenticación.
Si utiliza Castle Windsor Dependency Injection, puede insertar listas de RoleProviders que se pueden utilizar para determinar los derechos de usuario de cualquier fuente que elija implementar.