2011-12-30 14 views
18

Utilizando Microsoft.Web.DistributedCache.DistributedCacheOutputCacheProvider de Windows Azure como proveedor de salida de cache para una aplicación MVC3. Este es el método de acción relevante:¿Por qué no puedo combinar los atributos [Authorize] y [OutputCache] cuando uso la memoria caché de Azure (aplicación .NET MVC3)?

[ActionName("sample-cached-page")] 
[OutputCache(Duration = 300, VaryByCustom = "User", 
    Location = OutputCacheLocation.Server)] 
[Authorize(Users = "[email protected],[email protected]")] 
public virtual ActionResult SampleCachedPage() 
{ 
    return View(); 
} 

consigo la siguiente excepción al cargar esta vista desde un navegador web:

System.Configuration.Provider.ProviderException: When using a custom output cache provider like 'DistributedCache', only the following expiration policies and cache features are supported: file dependencies, absolute expirations, static validation callbacks and static substitution callbacks. 

System.Configuration.Provider.ProviderException: When using a custom output cache provider like 'DistributedCache', only the following expiration policies and cache features are supported: file dependencies, absolute expirations, static validation callbacks and static substitution callbacks. 
    at System.Web.Caching.OutputCache.InsertResponse(String cachedVaryKey, CachedVary cachedVary, String rawResponseKey, CachedRawResponse rawResponse, CacheDependency dependencies, DateTime absExp, TimeSpan slidingExp) 
    at System.Web.Caching.OutputCacheModule.OnLeave(Object source, EventArgs eventArgs) 
    at System.Web.HttpApplication.SyncEventExecutionStep.System.Web.HttpApplication.IExecutionStep.Execute() 
    at System.Web.HttpApplication.ExecuteStep(IExecutionStep step, Boolean& completedSynchronously) 

Si quito el atributo [Autorizar], la vista cachés como sería esperado. ¿Esto significa que no puedo poner [OutputCache] en un método de acción que debe tener [Authorize]? O bien, ¿debo anular AuthorizeAttribute con una implementación personalizada que utiliza un método de devolución de llamada de validación estática para el caché?

Actualización 1

Después de la respuesta de Evan, he probado el método de acción anteriormente en IIS Express (fuera de Azure). Aquí está mi anulación de la propiedad = VaryByCustom "usuario" en el atributo OutputCache:

public override string GetVaryByCustomString(HttpContext context, string custom) 
{ 
    return "User".Equals(custom, StringComparison.OrdinalIgnoreCase) 
     ? Thread.CurrentPrincipal.Identity.Name 
     : base.GetVaryByCustomString(context, custom); 
} 

Cuando visito la página de muestra en caché como [email protected], se almacena en caché la salida de la página, y la vista pantallas "Esta página fue almacenada en caché el 12/31/2011 11:06: AM (UTC)". Si luego cierro la sesión e inicio sesión como [email protected] y visito la página, aparece "Esta página fue almacenada en caché al 31/12/2011 11:06: AM (UTC)". Al volver a iniciar sesión como [email protected] y al volver a visitar la página, se muestra la caché "Esta página se almacenó en caché el 31/12/2011 11:06: AM (UTC)" nuevamente. Otros intentos de inicio/finalización de sesión muestran que se está almacenando en la memoria caché diferente & según el usuario.

Esto me lleva a creer que la salida se almacena en caché por separado en función del usuario, que es la intención con mi configuración VaryByCustom = "Usuario" & anular. El problema es que no funciona con el proveedor de caché distribuida de Azure. Evan, ¿respondes que solo el contenido público en caché sigue en pie?

Actualización 2

Desenterré la fuente, y se encontró que el AuthorizeAttribute fuera de la caja tiene de hecho una devolución de llamada de validación no estático. He aquí un extracto de OnAuthorization:

if (AuthorizeCore(filterContext.HttpContext)) { 
    // ** IMPORTANT ** 
    // Since we're performing authorization at the action level, the authorization code runs 
    // after the output caching module. In the worst case this could allow an authorized user 
    // to cause the page to be cached, then an unauthorized user would later be served the 
    // cached page. We work around this by telling proxies not to cache the sensitive page, 
    // then we hook our custom authorization code into the caching mechanism so that we have 
    // the final say on whether a page should be served from the cache. 

    HttpCachePolicyBase cachePolicy = filterContext.HttpContext.Response.Cache; 
    cachePolicy.SetProxyMaxAge(new TimeSpan(0)); 
    cachePolicy.AddValidationCallback(CacheValidateHandler, null /* data */); 
} 
else { 
    HandleUnauthorizedRequest(filterContext); 
} 

CacheValidationHandler delegados a la validación caché protected virtual HttpValidationStatus OnCacheAuthorization(HttpContextBase), que por supuesto no es estática. Una razón por la cual no es estática es porque, como se menciona en el comentario IMPORTANTE anterior, invoca protected virtual bool AuthorizeCore(HttpContextBase).

Para realizar cualquiera de las lógicas de AuthorizeCore a partir de un método de devolución de llamada de validación de caché estática, necesitaría conocer las propiedades de Usuarios y Roles de la instancia de AuthorizeAttribute. Sin embargo, no parece ser una forma fácil de conectar. Tendría que anular OnAuthorization para poner estos 2 valores en HttpContext (¿Colección de elementos?) Y luego anular OnCacheAuthorization para que vuelvan a salir. Pero eso huele sucio.

Si tenemos cuidado de utilizar la propiedad VaryByCustom = "User" en el atributo OutputCache, ¿podemos simplemente anular OnCacheAuthorization para que siempre devuelva HttpValidationStatus.Valid?Cuando el método de acción no tiene un atributo OutputCache, no tendríamos que preocuparnos de que esta devolución de llamada se haya invocado alguna vez, ¿correcto? Y si tenemos un atributo OutputCache sin VaryByCustom = "Usuario", entonces debería ser obvio que la página podría devolver cualquier versión almacenada en caché independientemente de la solicitud del usuario que creó la copia en caché. ¿Qué tan arriesgado es esto?

+0

olive - aparte de mi respuesta a continuación, también puede buscar la publicación original de TheCloudlessSky donde obtuve la idea en mi código ... también desechar cualquier cosa innecesaria sobre la inyección de un servicio, o Sesiones ... todo eso es específico para mí Lo que importa es manejar la validación de la caché en esa función OnAuthorization() de la manera en que la necesita para funcionar. :) Cuídate. –

+0

¿Qué ocurre si utiliza "UseSlidingExpiration = False" para imponer la caducidad absoluta? – lalibi

+1

¿Sabes si este problema aún existe? Parece que lo estoy teniendo con MVC5 y, sin embargo, no parece tan común aparte de esta publicación. Parece realmente extraño que simplemente no funciona. No puedo imaginar el uso de caché y el caché de salida azul son tan infrecuentes – GraemeMiller

Respuesta

7

El almacenamiento en caché ocurre antes de la Acción. Es probable que necesite personalizar sus mecanismos de autorización para manejar escenarios de caché.

Mira una pregunta que publiqué hace un tiempo - MVC Custom Authentication, Authorization, and Roles Implementation.

La parte que creo que le ayudaría es un atributo Autorizar personalizado que es el método OnAuthorize() que trata con el almacenamiento en caché.

A continuación se muestra un bloque de código, por ejemplo:

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

/// <summary> 
/// Ensures that authorization is checked on cached pages. 
/// </summary> 
/// <param name="httpContext"></param> 
/// <returns></returns> 
public HttpValidationStatus AuthorizeCache(HttpContext httpContext) 
{ 
    if (httpContext.Session == null) 
     return HttpValidationStatus.Invalid; 
    return _authorizationService.IsAuthorized((UserSessionInfoViewModel) httpContext.Session["user"], _authorizedRoles) 
     ? HttpValidationStatus.Valid 
     : HttpValidationStatus.IgnoreThisRequest; 
} 
+0

Eso es lo que pensé. ¿De dónde viene AuthorizeCache (context)? ¿Es un método estático? Esto actualmente me está dando un error de compilación "No se puede resolver el símbolo 'AuthorizeCache'" – danludwig

+0

compruebe la respuesta nuevamente - Agregué el método de caché. –

+0

Ver Creo que este es el problema con Azure. Creo que el oob MVC3 AuthorizeAttribute tiene un método de devolución de llamada de validación no estático. La excepción en la pregunta dice que "Cuando se utiliza un proveedor de caché de salida personalizado como 'Caché distribuida', solo se admiten las siguientes políticas de caducidad y características de caché: ... devoluciones de llamada de validación estática ...". Su método usa una variable de instancia, por lo que no se puede marcar como estática. Puedo agregar una devolución de llamada de validación estática, pero ¿qué debería hacer? ¿Cuándo debería devolver Valid, Inválido e IgnorarThisRequest, basado únicamente en el argumento HttpContext? – danludwig

2

Estás en lo correcto olive. El almacenamiento en caché funciona almacenando en caché toda la salida de la Acción (incluidos todos los atributos) y luego devuelve el resultado a las llamadas siguientes sin llamar realmente a ninguno de los códigos.

Debido a esto no puede almacenar en caché y verificar la autorización porque al almacenar en caché no va a llamar a ninguno de sus códigos (incluida la autorización). Por lo tanto, todo lo que se almacena en caché debe ser público.

+1

He actualizado mi pregunta. Puedo almacenar con éxito en caché el contenido dependiente del usuario en IIS Express, utilizando la memoria caché incorporada. El problema es hacer esto en Azure. – danludwig

+0

No sabía que pudiera variar según el Usuario, pero tiene razón en que funciona como lo dijo con IIS localmente. Azure usa el almacenamiento en caché de AppFabric frente al almacenamiento en caché local, pero como el código de almacenamiento en caché se ejecuta en el servidor de la aplicación, no estoy seguro de por qué no funciona. ¿Reemplazó el mismo método en DistributedCacheProvider? – Evan

+1

La anulación de VaryByCustom está en global.asax. DistributedOutputCacheProvider está en Microsoft.Web.DistributedCache dll. Creo que uno puede anular AuthorizeAttribute y llamar a 'filterContext.HttpContext.Cache.AddValidationCallback'. Sin embargo, para Azure Cache, el handler arg tiene que ser un método estático. No soy un experto en seguridad ni un experto en caché, así que me pregunto si alguien más ya tiene un código para esto. – danludwig

6

he vuelto a esta cuestión y, después de un poco de bricolaje, han llegado a la conclusión de que no puede utilizar el fuera de la caja System.Web.Mvc.AuthorizeAttribute lo largo con el System.Web.Mvc.OutputCacheAttribute de fábrica cuando utiliza Azure DistributedCache. La razón principal es porque, como indica el mensaje de error en la pregunta original, el método de devolución de llamada de validación debe ser estático para poder usarlo con la Caché distribuida de Azure. El método de devolución de llamada de caché en el atributo MVC Authorize es un método de instancia.

Intenté averiguar cómo hacerlo funcionar haciendo una copia de AuthorizeAttribute desde la fuente MVC, renombrándola, conectándola a una acción con OutputCache conectado a Azure y depurando. La razón por la que el método de devolución de llamada caché no es estático se debe a que, para autorizar, el atributo necesita verificar el Usuario de HttpContext frente a los valores de propiedades de Usuarios y Roles que se establecen cuando se construye el atributo. Aquí está el código correspondiente:

OnAuthorization

public virtual void OnAuthorization(AuthorizationContext filterContext) { 
    //... code to check argument and child action cache 

    if (AuthorizeCore(filterContext.HttpContext)) { 
     // Since we're performing authorization at the action level, 
     // the authorization code runs after the output caching module. 
     // In the worst case this could allow an authorized user 
     // to cause the page to be cached, then an unauthorized user would 
     // later be served the cached page. We work around this by telling 
     // proxies not to cache the sensitive page, then we hook our custom 
     // authorization code into the caching mechanism so that we have 
     // the final say on whether a page should be served from the cache. 

     HttpCachePolicyBase cachePolicy = filterContext 
      .HttpContext.Response.Cache; 
     cachePolicy.SetProxyMaxAge(new TimeSpan(0)); 
     cachePolicy.AddValidationCallback(CacheValidateHandler, null /* data */); 
    } 
    else { 
     HandleUnauthorizedRequest(filterContext); 
    } 
} 

caché de validación de devolución de llamada

private void CacheValidateHandler(HttpContext context, object data, 
    ref HttpValidationStatus validationStatus) { 
    validationStatus = OnCacheAuthorization(new HttpContextWrapper(context)); 
} 

// This method must be thread-safe since it is called by the caching module. 
protected virtual HttpValidationStatus OnCacheAuthorization 
    (HttpContextBase httpContext) { 
    if (httpContext == null) { 
     throw new ArgumentNullException("httpContext"); 
    } 

    bool isAuthorized = AuthorizeCore(httpContext); 
    return (isAuthorized) 
     ? HttpValidationStatus.Valid 
     : HttpValidationStatus.IgnoreThisRequest; 
} 

Como se puede ver, la devolución de llamada de validación caché en última instancia invoca AuthorizeCore, que es otro método de instancia (protegido virtual). AuthorizeCore, que también fue llamado durante OnAuthorization, hace 3 cosas principales:

  1. Comprueba que el HttpContextBase.User.Identity.IsAuthenticated == true

  2. Si el atributo tiene una propiedad de cadena usuarios no vacía , comprueba que HttpContextBase.User.Identity.Name coincide con uno de los valores separados por comas.

  3. Si el atributo tiene una propiedad de cadena Roles no vacía, comprueba que HttpContextBase.User.IsInRole para uno de los valores separados por comas.

AuthorizeCore

// This method must be thread-safe since it is called by the thread-safe 
// OnCacheAuthorization() method. 
protected virtual bool AuthorizeCore(HttpContextBase httpContext) { 
    if (httpContext == null) { 
     throw new ArgumentNullException("httpContext"); 
    } 

    IPrincipal user = httpContext.User; 
    if (!user.Identity.IsAuthenticated) { 
     return false; 
    } 

    if (_usersSplit.Length > 0 && !_usersSplit.Contains 
     (user.Identity.Name, StringComparer.OrdinalIgnoreCase)) { 
     return false; 
    } 

    if (_rolesSplit.Length > 0 && !_rolesSplit.Any(user.IsInRole)) { 
     return false; 
    } 

    return true; 
} 

Cuando simplemente trata de hacer que el método de devolución de llamada de validación estática, el código no se compilará porque necesita el acceso a estos campos y _rolesSplit _usersSplit, que se basan en las propiedades públicas de Usuarios y Roles.

Mi primer intento fue pasar estos valores a la devolución de llamada utilizando el argumento object data del CacheValidateHandler. Incluso después de la introducción de métodos estáticos, esto todavía no funcionaba y daba como resultado la misma excepción. Esperaba que los datos del objeto se serializaran, y luego se devolvieran al manejador de validación durante la devolución de llamada. Aparentemente, este no es el caso, y cuando intenta hacer esto, Azure's DistributedCache aún lo considera una devolución de llamada no estática, lo que da como resultado el mismo mensaje de excepción &.

// this won't work 
cachePolicy.AddValidationCallback(CacheValidateHandler, new object() /* data */); 

Mi segundo intento fue añadir los valores de la colección HttpContext.Items, desde una instancia de HttpContext se pasa automáticamente al controlador. Esto tampoco funcionó. El HttpContext que se pasa al CacheValidateHandlerno es la misma instancia que existía en la propiedad filterContext.HttpContext. De hecho, cuando CacheValidateHandler se ejecuta, tiene una sesión nula y siempre tiene una colección de Elementos vacía.

// this won't work 
private void CacheValidateHandler(HttpContext context, object data, 
    ref HttpValidationStatus validationStatus) { 
    Debug.Assert(!context.Items.Any()); // even after I put items into it 
    validationStatus = OnCacheAuthorization(new HttpContextWrapper(context)); 
} 

Sin embargo ...

A pesar de que parece que no hay manera de pasar los valores de usuarios & Roles de propiedad de vuelta al controlador de devolución de llamada de validación caché, el HttpContext se le pasan de hecho tiene la correcto User Principal. Además, ninguna de las acciones en las que actualmente deseo combinar [Autorizar] y [Caché de resultados] alguna vez pasan una propiedad de Usuarios o Roles al constructor AutorizarAtributo.

Por lo tanto, es posible crear un AuthenticateAttribute personalizado que ignore estas propiedades y solo compruebe para asegurarse de que User.Identity.IsAuthenticated == true. Si necesita autenticarse contra un rol específico, también puede hacerlo y combinarlo con OutputCache ... sin embargo, necesitaría un atributo distinto para cada (conjunto de) Rol (es) a fin de hacer el método de devolución de llamada de validación de caché estático . Volveré y publicaré el código después de haberlo pulido un poco.

Cuestiones relacionadas