Пользовательская аутентификация, авторизация и реализация ролей MVC - PullRequest
14 голосов
/ 20 декабря 2011

Потерпи меня, пока я сообщу подробности вопроса ...

У меня есть сайт MVC, использующий FormsAuthentication и пользовательские классы обслуживания для аутентификации, авторизации, ролей / членства и т. Д.

Аутентификация

Существует три способа входа в систему: (1) электронная почта + псевдоним , (2) OpenID и (3) имя пользователя + пароль . Все трое получают пользовательский cookie-файл и начинают сеанс. Первые два используются посетителями (только сеанс), а третьи - авторами / администраторами с учетными записями в БД.

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

Роли

Простое перечисление флагов для разрешений:

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

Расширение Enum, помогающее перечислять перечисления флагов (вау!).

public static class EnumExtensions
{
    private static void IsEnumWithFlags<T>()
    {
        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<T> GetFlags<T>(this T value) where T : struct
    {
        IsEnumWithFlags<T>();
        return from flag in Enum.GetValues(typeof(T)).Cast<T>() let lValue = Convert.ToInt64(value) let lFlag = Convert.ToInt64(flag) where (lValue & lFlag) != 0 select flag;
    }
}

Авторизация

Сервис предлагает методы проверки ролей аутентифицированного пользователя.

public class AuthorizationService : IAuthorizationService
{
    // Convert role strings into a Roles enum flags using the additive "|" (OR) operand.
    public Roles AggregateRoles(IEnumerable<string> 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);
    }
}

Я решил использовать это в своих контроллерах через атрибут:

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

    /// <summary>
    /// Constructor
    /// </summary>
    /// <remarks>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).</remarks>
    public AuthorizationFilter(IAuthorizationService authorizationService, Roles authorizedRoles)
    {
        _authorizationService = authorizationService;
        _authorizedRoles = authorizedRoles;
    }

    /// <summary>
    /// Uses injected authorization service to determine if the session user 
    /// has necessary role privileges.
    /// </summary>
    /// <remarks>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.
    /// </remarks>
    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);             
        }
    }

Я украшаю Действия в моих контроллерах этим атрибутом, и, как и [Authorize] от Microsoft, отсутствие параметров означает, что кто-либо должен проходить проверку подлинности (для меня это Enum = 0, без обязательных ролей).

По поводу оборачивания фоновой информации (фу) ... и выписывая все это, я ответил на мой первый вопрос. На данный момент меня интересует правильность моей установки:

  1. Нужно ли вручную извлекать cookie-файл аутентификации и заполнять принцип FormsIdentity для HttpContext или это должно быть автоматически?

  2. Есть проблемы с проверкой аутентификации в атрибуте / фильтре OnAuthorization()?

  3. Каковы компромиссы в использовании Session[] для хранения моей модели представления по сравнению с сериализацией ее в файле cookie аутентификации?

  4. Достаточно ли похоже это решение на идеалы «разделения интересов»? (Бонус как это более ориентированный на мнение вопрос)

Ответы [ 3 ]

8 голосов
/ 02 января 2012

Перекрестный пост от моего ответа на CodeReview :

Я постараюсь ответить на ваши вопросы и дам несколько советов:

  1. Если вы настроили FormsAuthentication в web.config, он автоматически извлечет cookie для вас, поэтому вам не нужно вносить ручную заполнение FormsIdentity.В любом случае это довольно легко проверить.

  2. Возможно, вы захотите переопределить AuthorizeCore и OnAuthorization для эффективного атрибута авторизации.Метод AuthorizeCore возвращает логическое значение и используется для определения, имеет ли пользователь доступ к данному ресурсу.OnAuthorization не возвращается и обычно используется для запуска других вещей в зависимости от статуса аутентификации.

  3. Я думаю, что вопрос session-vs-cookie в значительной степени предпочтение, но я 'Я рекомендую пойти на сессию по нескольким причинам.Основная причина в том, что cookie-файл передается с каждым запросом, и хотя сейчас у вас может быть только небольшая часть данных, со временем, кто знает, что вы там напишите.Добавьте накладные расходы на шифрование, и оно может стать достаточно большим для замедления запросов.Хранение их в сеансе также дает право владения данными (вместо того, чтобы передавать их в руки клиента и полагаться на то, что вы расшифруете и будете их использовать).Одно из предложений, которое я хотел бы сделать, - обернуть этот сеанс доступа статическим UserContext классом, похожим на HttpContext, чтобы вы могли просто сделать вызов, например UserContext.Current.UserData.Ниже приведен пример кода.

  4. Я не могу сказать, является ли это хорошим разделением интересов, но для меня это выглядит хорошим решением.Это мало чем отличается от других подходов аутентификации MVC, которые я видел.На самом деле я использую что-то очень похожее в своих приложениях.

Последний вопрос - почему вы создали и установили cookie FormsAuthentication вместо использования FormsAuthentication.SetAuthCookie?Просто любопытно.

Пример кода для статического класса контекста

public class UserContext
{
    private UserContext()
    {
    }

    public static UserContext Current
    {
        get
        {
            if (HttpContext.Current == null || HttpContext.Current.Session == null)
                return null;

            if (HttpContext.Current.Session["UserContext"] == null)
                BuildUserContext();

            return (UserContext)HttpContext.Current.Session["UserContext"];
        }
    }

    private static void BuildUserContext()
    {
        BuildUserContext(HttpContext.Current.User);
    }

    private static void BuildUserContext(IPrincipal user)
    {
        if (!user.Identity.IsAuthenticated) return;

        // For my application, I use DI to get a service to retrieve my domain
        // user by the IPrincipal
        var personService = DependencyResolver.Current.GetService<IUserBaseService>();
        var person = personService.FindBy(user);

        if (person == null) return;

        var uc = new UserContext { IsAuthenticated = true };

        // Here is where you would populate the user data (in my case a SiteUser object)
        var siteUser = new SiteUser();
        // This is a call to ValueInjecter, but you could map the properties however
        // you wanted. You might even be able to put your object in there if it's a POCO
        siteUser.InjectFrom<FlatLoopValueInjection>(person);

        // Next, stick the user data into the context
        uc.SiteUser = siteUser;

        // Finally, save it into your session
        HttpContext.Current.Session["UserContext"] = uc;
    }


    #region Class members
    public bool IsAuthenticated { get; internal set; }
    public SiteUser SiteUser { get; internal set; }

    // I have this method to allow me to pull my domain object from the context.
    // I can't store the domain object itself because I'm using NHibernate and
    // its proxy setup breaks this sort of thing
    public UserBase GetDomainUser()
    {
        var svc = DependencyResolver.Current.GetService<IUserBaseService>();
        return svc.FindBy(ActiveSiteUser.Id);
    }

    // I have these for some user-switching operations I support
    public void Refresh()
    {
        BuildUserContext();
    }

    public void Flush()
    {
        HttpContext.Current.Session["UserContext"] = null;
    }
    #endregion
}

В прошлом я непосредственно помещал свойства в класс UserContext для доступа к пользовательским данным.необходимо, но поскольку я использовал это для других, более сложных проектов, я решил переместить его в класс SiteUser:

public class SiteUser
{
    public int Id { get; set; }
    public string FirstName { get; set; }
    public string LastName { get; set; }
    public string FullName
    {
        get { return FirstName + " " + LastName; }
    }
    public string AvatarUrl { get; set; }

    public int TimezoneUtcOffset { get; set; }

    // Any other data I need...
}
8 голосов
/ 20 декабря 2011

Хотя я думаю, что вы отлично справляетесь с этим, я спрашиваю, почему вы воссоздаете колесо. Поскольку Microsoft предоставляет для этого систему, называемую «Членство и поставщики ролей». Почему бы просто не написать свой собственный членство и поставщика ролей, тогда вам не нужно создавать свой собственный атрибут аутентификации и / или фильтры, и вы можете просто использовать встроенный.

1 голос
/ 29 декабря 2011

Ваша реализация аутентификации, авторизации и ролей MVC выглядит хорошо.Чтобы ответить на ваш первый вопрос, когда вы не используете членство пользователя, вы должны заполнить принципал FormsIdentity самостоятельно.Решение, которое я использую, описано здесь Мой блог

...