46

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 de roles personalizado, proveedor de membresía, etc.)ASP.NET MVC: ¿alternativa al proveedor de funciones?

Por más manejable/flexible me refiero a que estoy limitado a usar la clase estática de 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 utilizar la clase estática Roles que tiene su propio contexto de base de datos, etc, también los nombres de la tabla es horrible ...

Gracias de antemano.

+0

estoy ... no muy seguro de lo que "UnitOfWork" tiene que ver con los derechos de acceso de usuarios (roles). ¿No está eso más relacionado con las transacciones que con la autorización? –

+0

@Matti Virkkunen - Es cierto, olvídate de esa parte :) – ebb

+2

¿Podrías explicar lo que quieres decir con "más manejable/flexible"? Actualmente parece que ni siquiera estás seguro de lo que quieres. –

Respuesta

87

Estoy en el mismo barco que usted - Siempre he odiado a los proveedores de Role. Sí, son geniales si quieres poner las cosas en marcha para un pequeño sitio web , pero no son muy realistas. La desventaja principal que siempre he encontrado es que te atan directamente a ASP.NET.

La forma en que fui para un proyecto reciente fue definir un par de interfaces que son parte de la capa de servicio (NOTA: Me simplificado éstos un poco - pero aquí se puede añadir a ellos):

public interface IAuthenticationService 
{ 
    bool Login(string username, string password); 
    void Logout(User user); 
} 

public interface IAuthorizationService 
{ 
    bool Authorize(User user, Roles requiredRoles); 
} 

a continuación, sus usuarios podrían tener un Roles enumeración:

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, usted podría tener una aplicación de base que hace la comprobación de contraseñas estándar y entonces se podría tener un FormsAuthenticationService que hace un poco más como conjunto ting la cookie, etc. Para su AuthorizationService, que había necesidad de 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, que fácilmente podría añadir servicios para restablecer las contraseñas etc.

Dado que está utilizando MVC, que podría hacer la 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 se puede adornar en sus acciones del 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 sobre los servicios de FormsAuthorization/Authentication o 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 la vista (utilizando la sintaxis Razor aquí):

@if(Html.SecurityTrim(Roles.Accounting)) 
{ 
    <span>Only for accounting</span> 
} 

EDIT: El UserSession sería algo como esto:

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 lo hacemos exponer el hash de la contraseña y todos los demás detalles dentro de la sesión del usuario actual, ya que son realmente no necesarios para la sesión del usuario toda la vida.

+3

¡Nada menos que perfecto! Solo una pregunta curiosa: ¿cómo verificaría si un usuario tiene un rol en la vista? (Para representar diferentes elementos del menú para un Usuario regular y un Administrador)? – ebb

+0

Esto tiene más sentido porque la única razón por la que uso rolesprovider es obtener el atributo '[Authorize (Roles =" stuff ")]' –

+1

@ebb - Actualicé mi publicación. – TheCloudlessSky

1

No necesita usar una clase estática para las funciones. Por ejemplo, el SqlRoleProvider le permite definir los roles en una base de datos.

Por supuesto, si desea recuperar roles de su propia capa de servicio, no es tan difícil crear su propio proveedor de roles; realmente no hay tantos métodos para implementar.

+1

@Matti Virkkunen: Lo que intento hacer es hacer que el Proveedor de funciones y el Proveedor de membresía formen parte de mis asignaciones de ORM, ya que me permitirán más flexibilidad. – ebb

+2

@ebb: Estás siendo vago de nuevo. ¿Qué es lo concreto que quieres hacer? Puede llamar a cualquier método ORM desde su proveedor. –

+0

@Matti Virkkunen, por el momento tengo un servicio personalizado llamado "UserService.cs" que de todos modos interactúa con el proveedor de membresía, pero solo tiene una lógica simple y estúpida para crear/obtener/eliminar usuarios. Lo que intento hacer es hacer lo mismo con Role Provider ... En teoría es posible, sin embargo, entrar en un muro cuando se trata de verificar si un usuario está en una función de cert desde el IPrincipal User.IsInRole() no sabrá los roles de los usuarios? – ebb

0

Puede implementar sus propios proveedores membership y role anulando las interfaces apropiadas.

Si desea comenzar desde cero, normalmente este tipo de cosas se implementan como custom http module que almacena las credenciales de los usuarios en el httpcontext o en la sesión. De cualquier manera, es probable que desee establecer una cookie con algún tipo de token de autenticación.

5

Implementé un proveedor de funciones 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 atributo, debe implementar la clase ActionFilterAttribute 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); 
} 

FormsAuthenticationService clase

/// <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 FormsAuthorizationService clase y cómo podemos asignar un usuario a la httpContext.Session["CurrentUser"]. Mi enfoque en esta situación es crear una nueva instancia de clase userSession y asignar directamente al usuario desde 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)); 
    } 
} 

entonces en su controlador después de que el usuario es autenticado puede obtener papeles de la base de datos y asignarlo al papel de sesión:

vez que el usuario se registra fuera del sistema puede borrar la sesión

FormsService.SignOut(); 
Session.Abandon(); 
return RedirectToAction("Index", "Account"); 

la advertencia en este modelo es que, cuando el usuario está firmado en el sistema, si una función se asigna al usuario, la autorización no funciona a menos que se conecta a cabo una d vuelve a iniciar sesión en el sistema.

Otra cosa es que no hay necesidad de tener una clase separada para las funciones, ya que podemos obtener roles directamente de la base de datos y configurarla en la sesión de roles en un controlador.

Después de que haya terminado con la aplicación de todos estos códigos de un último paso consiste en vincular este atributo a sus métodos en el controlador:

[RequirePermission("Admin,DM")] 
public ActionResult Create() 
{ 
return View(); 
} 
Cuestiones relacionadas