Autenticazione personalizzata, autorizzazione e implementazione dei ruoli

  • Ho un sito MVC, utilizzo FormsAuthenticatione classi di servizio personalizzate per Authentication, Authorization, Roles/ Membership, ecc.

    Autenticazione

    Ci sono tre modi per accedere:


    1. Email + Alias

    2. OpenID

    3. Nome utente + password

    Tutti e tre ottengono all'utente un cookie di autenticazione e avviano una sessione. I primi due sono utilizzati dai visitatori (solo sessione) e il terzo per autori/admin con account 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

    Un semplice flag enum per i permessi:

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

    Estensione Enum per aiutare a enumerare le enumerazioni di flag:

    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

    Il servizio offre metodi per controllare i ruoli di un utente autenticato.

    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);
    }
    }

    Ho scelto di usarlo nei miei controller tramite un attributo:

    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);
    }
    }

    Decoro le azioni nei miei controller con questo attributo e, come l' [Authorize]assenza di parametri di Microsoft, significa consentire a chiunque di autenticarsi (per me è Enum = 0, nessun ruolo richiesto).

    Sono curioso di sapere l'adeguatezza della mia configurazione:


    1. Devo afferrare manualmente il cookie di autenticazione e popolare il FormsIdentityprincipale per il HttpContexto dovrebbe essere automatico?


    2. Ci sono problemi con il controllo dell'autenticazione all'interno dell'attributo/filtro OnAuthorization()?


    3. Quali sono i compromessi nell'utilizzo Session[]per archiviare il mio modello di visualizzazione rispetto alla serializzazione all'interno del cookie di autenticazione?


    4. Questa soluzione sembra seguire abbastanza bene gli ideali della "separazione delle preoccupazioni"?


  • Josh Anderson

    Josh Anderson Risposta corretta

    9 anni fa

    Mi impegnerò a rispondere alle tue domande e fornirò alcuni suggerimenti:


    1. Se hai configurato FormsAuthentication in web.config, estrarrà automaticamente il cookie per te, quindi non dovresti dover eseguire alcun popolamento manuale di FormsIdentity. Questo è abbastanza facile da testare in ogni caso.


    2. Probabilmente vorrai sovrascrivere entrambi AuthorizeCoree OnAuthorizationper un attributo di autorizzazione efficace. Il AuthorizeCoremetodo restituisce un booleano e viene utilizzato per determinare se l'utente ha accesso a una determinata risorsa. Il OnAuthorizationnon restituisce e viene generalmente utilizzato per attivare l'altro in base allo stato di autenticazione.


    3. Penso che la domanda sessione-contro-cookie sia in gran parte una preferenza, ma consiglierei di seguire la sessione per alcuni motivi. La ragione principale è che il cookie viene trasmesso con ogni richiesta, e mentre in questo momento potresti avere solo pochi dati al suo interno, con il passare del tempo chissà cosa ci inserirai. Aggiungi un sovraccarico di crittografia e potrebbe diventare abbastanza grande da rallentare le richieste. La memorizzazione nella sessione mette anche la proprietà dei dati nelle tue mani (anziché metterli nelle mani del cliente e fare affidamento su di te per decrittografarli e utilizzarli). Un suggerimento che farei è avvolgere l'accesso alla sessione in una UserContextclasse statica , simile a HttpContext, quindi potresti semplicemente effettuare una chiamata comeUserContext.Current.UserData. Posso fornire del codice per questo se hai bisogno di suggerimenti. Questo approccio ti consentirà di nascondere l'implementazione in modo da poter passare dalla sessione al cookie e viceversa senza influire sull'altro codice che lo utilizza.


    4. Non posso davvero parlare se sia una buona separazione delle preoccupazioni, ma mi sembra una buona soluzione. Non è diverso da altri approcci di autenticazione MVC che ho visto. Sto usando qualcosa di molto simile nelle mie app in effetti.


    Un'ultima domanda: perché hai creato e impostato manualmente il cookie FormsAuthentication invece di usare FormsAuthentication.SetAuthCookie? Solo curioso.

    Josh

    Grazie per la recensione/risposta. È molto utile. Questa è una domanda che ho fatto su StackOverflow (http://stackoverflow.com/questions/8567358/mvc-custom-authentication-authorization-and-roles-implementation) e non sono stato soddisfatto della risposta. Se vuoi, metti lì la tua risposta e te la darò. Inoltre, se desideri condividere il tuo esempio "UserContext" anche per la domanda n. 3, sarebbe ancora più fantastico. Per quanto riguarda il cookie, è arrivato prima che usassi `Session[]` e lo stavo impostando manualmente con un biglietto crittografato serializzato... "solo codice avanzato.

    L'ho incrociato e ho aggiunto il codice "UserContext". Spero che sia d'aiuto.

Licenza sotto CC-BY-SA con attribuzione


Contenuto datato prima del 24/07/2021 11:53