Подтвердить что ты не робот

Почему я не могу комбинировать атрибуты [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 = "Пользователь", тогда должно быть очевидно, что страница может вернуть любую кешированную версию, независимо от того, какой пользовательский запрос создал кешированную копию. Насколько это рискованно?

4b9b3361

Ответ 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 (включая все атрибуты), а затем возвращает результат для последующих вызовов без фактического вызова какого-либо вашего кода.

Из-за этого вы не можете кэшировать и проверять авторизацию, потому что при кэшировании вы не будете называть какой-либо из ваших кодов (включая авторизацию). Поэтому все, что кэшируется, должно быть общедоступным.