Implémentation personnalisée de l'authentification, de l'autorisation et des rôles

  • J'ai un site MVC, utilisant FormsAuthenticationet des classes de service personnalisées pour Authentication, Authorization, Roles/ Membership, etc.

    Authentification

    Il existe trois façons de se connecter :


    1. E-mail + Alias

    2. Identifiant ouvert

    3. Nom d'utilisateur + Mot de passe

    Tous les trois obtiennent un cookie d'autorisation pour l'utilisateur et démarrent une session. Les deux premiers sont utilisés par les visiteurs (session uniquement) et le troisième pour les auteurs/administrateurs avec des comptes DB.

    public class BaseFormsAuthenticationService : IAuthenticationService
    {
    // Disperse auth cookie and store user session info.
    public virtual void SignIn(UserBase user, bool persistentCookie)
    {
    var vmUser = new UserSessionInfoViewModel { Email = user.Email, Name = user.Name, Url = user.Url, Gravatar = user.Gravatar };

    if(user.GetType() == typeof(User)) {
    // roles go into view model as string not enum, see Roles enum below.
    var rolesInt = ((User)user).Roles;
    var rolesEnum = (Roles)rolesInt;
    var rolesString = rolesEnum.ToString();
    var rolesStringList = rolesString.Split(',').Select(role => role.Trim()).ToList();
    vmUser.Roles = rolesStringList;
    }

    // i was serializing the user data and stuffing it in the auth cookie
    // but I'm simply going to use the Session[] items collection now, so
    // just ignore this variable and its inclusion in the cookie below.
    var userData = "";

    var ticket = new FormsAuthenticationTicket(1, user.Email, DateTime.UtcNow, DateTime.UtcNow.AddMinutes(30), false, userData, FormsAuthentication.FormsCookiePath);
    var encryptedTicket = FormsAuthentication.Encrypt(ticket);
    var authCookie = new HttpCookie(FormsAuthentication.FormsCookieName, encryptedTicket) { HttpOnly = true };
    HttpContext.Current.Response.Cookies.Add(authCookie);
    HttpContext.Current.Session["user"] = vmUser;
    }
    }

    Roles

    Une simple énumération d'indicateurs pour les autorisations :

    [Flags]
    public enum Roles
    {
    Guest = 0,
    Editor = 1,
    Author = 2,
    Administrator = 4
    }

    Extension Enum pour aider à énumérer les énumérations d'indicateurs :

    public static class EnumExtensions
    {
    private static void IsEnumWithFlags()
    {
    if (!typeof(T).IsEnum)
    throw new ArgumentException(string.Format("Type '{0}' is not an enum", typeof (T).FullName));
    if (!Attribute.IsDefined(typeof(T), typeof(FlagsAttribute)))
    throw new ArgumentException(string.Format("Type '{0}' doesn't have the 'Flags' attribute", typeof(T).FullName));
    }

    public static IEnumerable GetFlags(this T value) where T : struct
    {
    IsEnumWithFlags();
    return from flag in Enum.GetValues(typeof(T)).Cast() let lValue = Convert.ToInt64(value) let lFlag = Convert.ToInt64(flag) where (lValue & lFlag) != 0 select flag;
    }
    }

    Authorization

    Le service propose des méthodes pour vérifier les rôles d'un utilisateur authentifié.

    public class AuthorizationService : IAuthorizationService
    {
    // Convert role strings into a Roles enum flags using the additive "|" (OR) operand.
    public Roles AggregateRoles(IEnumerable roles)
    {
    return roles.Aggregate(Roles.Guest, (current, role) => current | (Roles)Enum.Parse(typeof(Roles), role));
    }

    // Checks if a user's roles contains Administrator role.
    public bool IsAdministrator(Roles userRoles)
    {
    return userRoles.HasFlag(Roles.Administrator);
    }

    // Checks if user has ANY of the allowed role flags.
    public bool IsUserInAnyRoles(Roles userRoles, Roles allowedRoles)
    {
    var flags = allowedRoles.GetFlags();
    return flags.Any(flag => userRoles.HasFlag(flag));
    }

    // Checks if user has ALL required role flags.
    public bool IsUserInAllRoles(Roles userRoles, Roles requiredRoles)
    {
    return ((userRoles & requiredRoles) == requiredRoles);
    }

    // Validate authorization
    public bool IsAuthorized(UserSessionInfoViewModel user, Roles roles)
    {
    // convert comma delimited roles to enum flags, and check privileges.
    var userRoles = AggregateRoles(user.Roles);
    return IsAdministrator(userRoles) || IsUserInAnyRoles(userRoles, roles);
    }
    }

    J'ai choisi de l'utiliser dans mes contrôleurs via un attribut :

    public class AuthorizationFilter : IAuthorizationFilter
    {
    private readonly IAuthorizationService _authorizationService;
    private readonly Roles _authorizedRoles;

    ///
    /// Constructor
    ///

    /// The AuthorizedRolesAttribute is used on actions and designates the
    /// required roles. Using dependency injection we inject the service, as well
    /// as the attribute's constructor argument (Roles).
    public AuthorizationFilter(IAuthorizationService authorizationService, Roles authorizedRoles)
    {
    _authorizationService = authorizationService;
    _authorizedRoles = authorizedRoles;
    }

    ///
    /// Uses injected authorization service to determine if the session user
    /// has necessary role privileges.
    ///

    /// As authorization code runs at the action level, after the
    /// caching module, our authorization code is hooked into the caching
    /// mechanics, to ensure unauthorized users are not served up a
    /// prior-authorized page.
    /// Note: Special thanks to TheCloudlessSky on StackOverflow.
    ///
    public void OnAuthorization(AuthorizationContext filterContext)
    {
    // User must be authenticated and Session not be null
    if (!filterContext.HttpContext.User.Identity.IsAuthenticated || filterContext.HttpContext.Session == null)
    HandleUnauthorizedRequest(filterContext);
    else {
    // if authorized, handle cache validation
    if (_authorizationService.IsAuthorized((UserSessionInfoViewModel)filterContext.HttpContext.Session["user"], _authorizedRoles)) {
    var cache = filterContext.HttpContext.Response.Cache;
    cache.SetProxyMaxAge(new TimeSpan(0));
    cache.AddValidationCallback((HttpContext context, object o, ref HttpValidationStatus status) => AuthorizeCache(context), null);
    }
    else
    HandleUnauthorizedRequest(filterContext);
    }
    }

    Je décore les actions dans mes contrôleurs avec cet attribut, et comme Microsoft, [Authorize]aucun paramètre ne signifie laisser entrer toute personne authentifiée (pour moi, c'est Enum = 0, aucun rôle requis).

    Je suis curieux de savoir si ma configuration est appropriée :


    1. Dois-je saisir manuellement le cookie d'authentification et remplir le FormsIdentityprincipal pour le HttpContextou cela devrait-il être automatique ?


    2. Y a-t-il des problèmes avec la vérification de l'authentification dans l'attribut/le filtreOnAuthorization() ?


    3. Quels sont les compromis à faire Session[]pour stocker mon modèle de vue par rapport à sa sérialisation dans le cookie d'authentification ?


    4. Cette solution semble-t-elle suivre suffisamment les idéaux de « séparation des préoccupations » ?


  • Je vais essayer de répondre à vos questions et vous faire quelques suggestions :


    1. Si vous avez configuré FormsAuthentication dans web.config, il extraira automatiquement le cookie pour vous, vous ne devriez donc pas avoir à remplir manuellement le FormsIdentity. C'est assez facile à tester dans tous les cas.


    2. Vous souhaiterez probablement remplacer les deux AuthorizeCoreet OnAuthorizationpour un attribut d'autorisation efficace. La AuthorizeCoreméthode renvoie un booléen et est utilisée pour déterminer si l'utilisateur a accès à une ressource donnée. Le OnAuthorizationne retourne pas et est généralement utilisé pour déclencher d'autres choses en fonction de l'état d'authentification.


    3. Je pense que la question session vs cookie est largement préférée, mais je recommanderais d'aller avec la session pour plusieurs raisons. La principale raison est que le cookie est transmis à chaque demande, et bien qu'à l'heure actuelle, vous n'ayez peut-être qu'un peu de données, au fil du temps, qui sait ce que vous y mettrez. Ajoutez une surcharge de chiffrement et il pourrait devenir suffisamment volumineux pour ralentir les demandes. Le stockage dans la session met également la propriété des données entre vos mains (au lieu de les mettre entre les mains du client et de compter sur vous pour les déchiffrer et les utiliser). Une suggestion que je ferais consiste à envelopper cet accès à la session dans une UserContextclasse statique , similaire à HttpContext, afin que vous puissiez simplement passer un appel commeUserContext.Current.UserData. Je peux fournir du code pour cela si vous avez besoin de suggestions. Cette approche vous permettra de masquer l'implémentation afin que vous puissiez passer d'une session à un cookie et inversement sans affecter les autres codes qui l'utilisent.


    4. Je ne peux pas vraiment dire si c'est une bonne séparation des préoccupations, mais cela me semble être une bonne solution. Ce n'est pas différent des autres approches d'authentification MVC que j'ai vues. J'utilise quelque chose de très similaire dans mes applications en fait.


    Une dernière question - pourquoi avez-vous créé et défini le cookie FormsAuthentication manuellement au lieu de l'utiliser FormsAuthentication.SetAuthCookie? Juste curieux.

    Josh

    Merci pour l'avis/réponse. C'est très utile. C'est une question que j'ai posée sur StackOverflow (http://stackoverflow.com/questions/8567358/mvc-custom-authentication-authorization-and-roles-implementation) et je n'ai pas été satisfait de la réponse. Si vous le souhaitez, postez votre réponse ici et je vous la donnerai. De plus, si vous souhaitez également partager votre exemple « UserContext » pour la question n° 3, ce serait encore plus génial. Quant au cookie, il est venu avant que j'utilise `Session[]` et je le configurais manuellement avec un ticket crypté sérialisé...'juste un code restant.

    J'ai posté ceci et ajouté le code `UserContext`. J'espère que ça aide.

Licence sous CC-BY-SA avec attribution


Contenu daté avant 24/07/2021 11:53