Почему я не могу комбинировать атрибуты [Authorize] и [OutputCache] при использовании кеша Azure (приложение .NET MVC3)?
Использование Windows Azure Microsoft.Web.DistributedCache.DistributedCacheOutputCacheProvider
в качестве поставщика outputCache для приложения 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();
}
При загрузке этого представления из веб-браузера я получаю следующее исключение:
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)
Если я удалю атрибут [Авторизовать], кэширует представление, как и следовало ожидать. Означает ли это, что я не могу поместить [OutputCache] в метод действия, который должен иметь [Авторизовать]? Или мне нужно переопределить AuthorizeAttribute с помощью специальной реализации, которая использует метод обратного вызова статической проверки для кеша?
Обновление 1
После ответа Эвана я протестировал вышеупомянутый метод действий в IIS Express (за пределами Azure). Вот мое переопределение для свойства VaryByCustom = "User" в атрибуте OutputCache:
public override string GetVaryByCustomString(HttpContext context, string custom)
{
return "User".Equals(custom, StringComparison.OrdinalIgnoreCase)
? Thread.CurrentPrincipal.Identity.Name
: base.GetVaryByCustomString(context, custom);
}
Когда я посещаю образец кэшированной страницы как [email protected], вывод страницы кэшируется, и на экране отображается "Эта страница была кэширована в 12/31/2011 11:06: 12 AM (UTC)". Если я затем выйду из системы и зарегистрирую его как [email protected] и перейдя на страницу, он отобразит "Эта страница была кеширована в 12/31/2011 11:06: 38 AM (UTC)". Подпись в виде [email protected] и пересмотр страницы приводит к отображению кеша. "Эта страница была снова сохранена в кэше в 12/31/2011 11:06: 12 AM (UTC)". Дальнейшие попытки входа/выхода показывают, что разные выходные данные кэшируются и возвращаются в зависимости от пользователя.
Это заставляет меня думать, что вывод кэшируется отдельно на основе пользователя, что является намерением с настройкой и переопределением VaryByCustom = "Пользователь". Проблема в том, что он не работает с поставщиком распределенных кешей Azure. Эван, вы отвечаете только о кешировании общедоступного контента?
Обновление 2
Я выкопал источник и обнаружил, что атрибут AuthorizeAttribute из готового кода действительно имеет обратный вызов нестатического подтверждения. Вот выдержка из 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
делегирует проверку кэша на protected virtual HttpValidationStatus OnCacheAuthorization(HttpContextBase)
, что, конечно, не является статичным. Одна из причин, почему он не является статичным, заключается в том, что, как отмечено в ВАЖНОМ комментарии выше, он вызывает protected virtual bool AuthorizeCore(HttpContextBase)
.
Чтобы выполнить любую логику AuthorizeCore из метода обратного вызова проверки статического кеша, ему необходимо знать свойства "Пользователи и роли" экземпляра AuthorizeAttribute. Однако, кажется, нет простого способа подключиться. Мне пришлось бы переопределить OnAuthorization, чтобы поместить эти 2 значения в HttpContext (коллекция Items?), А затем переопределить OnCacheAuthorization, чтобы вернуть их. Но это пахнет грязным.
Если мы стараемся использовать свойство VaryByCustom = "User" в атрибуте OutputCache, можем ли мы просто переопределить OnCacheAuthorization, чтобы всегда возвращать HttpValidationStatus.Valid? Когда у метода действия нет атрибута OutputCache, нам не нужно беспокоиться об этом обратном вызове, когда-либо вызываемом, правильно? И если у нас есть атрибут OutputCache без VaryByCustom = "Пользователь", тогда должно быть очевидно, что страница может вернуть любую кешированную версию, независимо от того, какой пользовательский запрос создал кешированную копию. Насколько это рискованно?
Ответы
Ответ 1
Кэширование происходит до действия. Вероятно, вам придется настроить свою механизацию авторизации для обработки сценариев кэширования.
Проверьте вопрос, который я опубликовал некоторое время назад - Внедрение MVA, аутентификация, авторизация и роли.
Часть, которая, я думаю, поможет вам, является специальным атрибутом авторизации, который OnAuthorize()
имеет дело с кешированием.
Ниже приведен блок кода, например:
/// <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;
}
Ответ 2
Я вернусь к этой проблеме и, после небольшого переделания, пришел к выводу, что вы не можете использовать из коробки System.Web.Mvc.AuthorizeAttribute
вместе с полем System.Web.Mvc.OutputCacheAttribute
при использовании Azure DistributedCache. Основная причина заключается в том, что, как говорится в сообщении об ошибке в исходном вопросе, метод обратного вызова валидации должен быть статическим, чтобы использовать его с Azure DistributedCache. Метод обратного вызова кеша в атрибуте Authorize MVC является методом экземпляра.
Я попытался выяснить, как заставить его работать, сделав копию AuthorizeAttribute из источника MVC, переименовав его, подключив его к действию с OutputCache, подключенному к Azure, и отлаживанию. Причина, по которой метод обратного вызова кеша не является статичным, заключается в том, что для авторизации атрибут должен проверять пользователя HttpContext на значения свойств пользователей и ролей, которые задаются при построении атрибута. Вот соответствующий код:
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);
}
}
Обратный вызов проверки кэша
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;
}
Как вы можете видеть, обратный вызов проверки кэша в конечном счете вызывает AuthorizeCore, который является другим методом экземпляра (защищенный виртуальный). AuthorizeCore, также называемый во время OnAuthorization, выполняет 3 основные функции:
-
Проверяет, что HttpContextBase.User.Identity.IsAuthenticated == true
-
Если атрибут имеет непустое свойство строки Users, проверяет, что HttpContextBase.User.Identity.Name соответствует одному из значений, разделенных запятыми.
-
Если атрибут имеет непустое свойство строки Roles, проверяет, что HttpContextBase.User.IsInRole для одного из значений, разделенных запятой.
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;
}
Когда вы просто пытаетесь сделать метод обратного вызова валидации статическим, код не будет компилироваться, потому что ему нужен доступ к этим полям _rolesSplit и _usersSplit, которые основаны на свойствах общих пользователей и ролей.
Моя первая попытка состояла в том, чтобы передать эти значения в обратный вызов, используя аргумент object data
CacheValidateHandler
. Даже после введения статических методов это все еще не сработало и привело к тому же исключению. Я надеялся, что данные объекта будут сериализованы, а затем возвращены к обработчику проверки в ходе обратного вызова. По-видимому, это не так, и когда вы пытаетесь это сделать, Azure DistributedCache по-прежнему считает его нестатистическим обратным вызовом, что приводит к тому же исключению и сообщению.
// this won't work
cachePolicy.AddValidationCallback(CacheValidateHandler, new object() /* data */);
Моя вторая попытка состояла в том, чтобы добавить значения в коллекцию HttpContext.Items
, так как экземпляр HttpContext
автоматически передается обработчику. Это тоже не сработало. HttpContext
, который передается в CacheValidateHandler
, не является тем же экземпляром, который существовал в свойстве filterContext.HttpContext
. Фактически, когда выполняется CacheValidateHandler, он имеет нулевой сеанс и всегда имеет пустую коллекцию Items.
// 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));
}
Однако...
Несмотря на то, что, кажется, нет способа передать значения свойств "Пользователи и Роли" обратно обработчику обратного вызова проверки кэша, HttpContext
, переданный ему , действительно имеет правильный пользовательский принцип. Кроме того, ни одно из действий, в которых я сейчас хочу объединить [Authorize] и [OutputCache], никогда не передаёт свойство "Пользователи" или "Роли" в конструктор AuthorizeAttribute.
Таким образом, можно создать настраиваемый атрибут AuthenticateAttribute, который игнорирует эти свойства и проверяет только, чтобы User.Identity.IsAuthenticated == true
. Если вам нужно пройти аутентификацию против определенной роли, вы также можете сделать это и объединиться с OutputCache... однако вам понадобится отдельный атрибут для каждого (набора) ролей (ов), чтобы сделать метод обратного вызова проверки кеша статическим, Я вернусь и отправлю код после того, как немного отполировал его.
Ответ 3
Вы правильные оливковые. Кэширование работает путем кэширования всего вывода Action (включая все атрибуты), а затем возвращает результат для последующих вызовов без фактического вызова какого-либо вашего кода.
Из-за этого вы не можете кэшировать и проверять авторизацию, потому что при кэшировании вы не будете называть какой-либо из ваших кодов (включая авторизацию). Поэтому все, что кэшируется, должно быть общедоступным.