Потерпите меня, когда я подробно расскажу о проблеме...
У меня есть сайт MVC, используя FormsAuthentication
и пользовательские классы обслуживания для аутентификации, авторизации, роли/членства и т.д.
Аутентификация
Существует три способа входа в систему: (1) Email + Alias, (2) OpenID и (3) Username + Password. Все три пользователя получают файл cookie auth и начинают сеанс. Первые два используются посетителями (только сессия), а третья для авторов /admin с учетными записями 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;
}
}
Роли
Простой флажок enum для разрешений:
[Flags]
public enum Roles
{
Guest = 0,
Editor = 1,
Author = 2,
Administrator = 4
}
Расширение Enum для перечисления перечислений флагов (wow!).
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 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 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);
}
}
Я украшаю действия в своих контроллерах с этим атрибутом и, как и Microsoft [Authorize]
, никакие параметры не позволяют никому пройти аутентификацию (для меня это Enum = 0, не требуемые роли).
Что касается обертывания фоновой информации (phew)... и написав все это, я ответил на свой первый вопрос. На данный момент мне интересно узнать о целесообразности моей установки:
-
Нужно ли вручную обманывать файл cookie auth и заполнять основную форму FormsIdentity для
HttpContext
или должно быть автоматическим? -
Любые проблемы с проверкой подлинности в атрибуте/фильтре
OnAuthorization()
? -
Что такое компромисс в использовании
Session[]
для хранения моей модели представления или ее сериализации внутри файла cookie auth? -
Является ли это решение, похоже, достаточно хорошо "разделяющим идеалы"? (Бонус, поскольку это более ориентированный на мнение вопрос)