vista tutorial paso net mvc modelo form ejemplo controlador cache asp asp.net-mvc-3 azure outputcache authorize-attribute

asp.net mvc 3 - tutorial - ¿Por qué no puedo combinar los atributos[Authorize] y[OutputCache] cuando uso la memoria caché de Azure(aplicación.NET MVC3)?



mvc c# windows forms (3)

Estás correcto oliva. El almacenamiento en caché funciona guardando en caché todo el resultado de la Acción (incluidos todos los atributos) y luego devolviendo el resultado a llamadas posteriores sin llamar realmente a ninguno de sus 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.

Utilizando Microsoft.Web.DistributedCache.DistributedCacheOutputCacheProvider Windows Azure como el proveedor de salida de caché para una aplicación MVC3. Aquí está el método de acción relevante:

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

Obtengo 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 elimino el atributo [Autorizar], la vista se almacena en caché como se esperaría. ¿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, probé el método de acción anterior en IIS Express (fuera de Azure). Aquí está mi anulación para la propiedad VaryByCustom = "User" 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], el resultado de la página se almacena en caché y la vista muestra "Esta página fue almacenada en caché el 31/12/2011 11:06: 12 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é el 31/12/2011 11:06: 38 AM (UTC)". Al volver a iniciar sesión como [email protected] y volver a visitar la página, la caché muestra "Esta página se almacenó en caché al 31/12/2011 11:06: 12 AM (UTC)" nuevamente. Otros intentos de inicio / finalización de sesión muestran que se están almacenando y devolviendo diferentes resultados 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 = "User" y anulación. 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 descubrí que el AuthorizeAttribute listo para usar tiene, de hecho, una devolución de llamada de validación no estática. Aquí hay 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 delega la validación de la memoria caché a 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 señala en el comentario IMPORTANTE anterior, invoca protected virtual bool AuthorizeCore(HttpContextBase) .

Para poder hacer 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 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?


Volví a este tema y, después de algunos retoques, he llegado a la conclusión de que no se puede utilizar el System.Web.Mvc.AuthorizeAttribute fuera de la caja junto con el System.Web.Mvc.AuthorizeAttribute fuera de la caja cuando se usa 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 de 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 relevante:

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

Retrollamada de validación de caché

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 puede ver, la devolución de llamada de validación de caché invoca finalmente a AuthorizeCore, que es otro método de instancia (protegido virtual). AuthorizeCore, que también se llamó durante OnAuthorization, hace 3 cosas principales:

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

  2. Si el atributo tiene una propiedad de cadena de Usuarios no vacía, comprueba que HttpContextBase.User.Identity.Name coincida 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.

AutorizarCore

// 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 intenta hacer que el método de devolución de llamada de validación sea estático, el código no se compilará porque necesita acceso a estos campos _rolesSplit y _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 de object data de 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 la misma excepción y mensaje.

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

Mi segundo intento fue agregar los valores a la colección HttpContext.Items , ya que una instancia de HttpContext se pasa automáticamente al controlador. Esto tampoco funcionó. El HttpContext que se pasa al CacheValidateHandler no 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...

Aunque parece que no hay forma de volver a pasar los valores de las propiedades de usuarios y roles al manejador de devolución de llamada de validación de caché, el HttpContext que se le HttpContext tiene, de hecho, el principal de usuario correcto . 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.


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 Autenticación personalizada, autorización e implementación de roles .

La parte que creo que te ayudaría es un atributo Autorizar personalizado OnAuthorize() método OnAuthorize() trata 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 . /// </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; }