Потерпи меня, пока я сообщу подробности вопроса ...
У меня есть сайт 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, без обязательных ролей).
По поводу оборачивания фоновой информации (фу) ... и выписывая все это, я ответил на мой первый вопрос. На данный момент меня интересует правильность моей установки:
Нужно ли вручную извлекать cookie-файл аутентификации и заполнять принцип FormsIdentity для HttpContext
или это должно быть автоматически?
Есть проблемы с проверкой аутентификации в атрибуте / фильтре OnAuthorization()
?
Каковы компромиссы в использовании Session[]
для хранения моей модели представления по сравнению с сериализацией ее в файле cookie аутентификации?
Достаточно ли похоже это решение на идеалы «разделения интересов»? (Бонус как это более ориентированный на мнение вопрос)