Почему я не могу объединить атрибуты [Authorize] и [OutputCache] при использовании кэша Azure (приложение .NET MVC3)? - PullRequest
19 голосов
/ 30 декабря 2011

Использование Windows Azure Microsoft.Web.DistributedCache.DistributedCacheOutputCacheProvider в качестве поставщика outputCache для приложения MVC3.Вот соответствующий метод действия:

[ActionName("sample-cached-page")]
[OutputCache(Duration = 300, VaryByCustom = "User", 
    Location = OutputCacheLocation.Server)]
[Authorize(Users = "me@mydomain.tld,another@otherdomain.tld")]
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)

Если я удаляю атрибут [Authorize], представлениекэши, как и следовало ожидать.Означает ли это, что я не могу поместить [OutputCache] в метод действия, который должен иметь [Authorize]?Или мне нужно переопределить 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);
}

Когда я захожу на пример кэшированной страницы как me@mydomain.tld, выходные данные страницы кэшируются, иView отображает «Эта страница была кэширована в 31.12.2011 11: 06: 12 AM (UTC)».Если я затем выйду из системы и войду в систему как other@otherdomain.tld и зайду на страницу, на ней отобразится «Эта страница была кэширована 31.12.2011 11: 06: 38 AM (UTC)».При повторном входе в систему как me@mydomain.tld и повторном посещении страницы в кеше снова отображается «Эта страница была кэширована 31.12.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 из метода обратного вызова проверки статического кэша, ему необходимо знать свойства Users и Roles экземпляра AuthorizeAttribute.Однако, кажется, нет простого способа подключить его. Мне пришлось бы переопределить OnAuthorization, чтобы поместить эти 2 значения в HttpContext (коллекция Items?), А затем переопределить OnCacheAuthorization, чтобы вернуть их обратно.Но это пахнет грязно.

Если мы осторожно используем свойство VaryByCustom = "User" в атрибуте OutputCache, можем ли мы просто переопределить OnCacheAuthorization, чтобы всегда возвращать HttpValidationStatus.Valid?Если у метода действия нет атрибута OutputCache, нам не нужно беспокоиться о том, что этот обратный вызов когда-либо будет вызван, верно?И если у нас есть атрибут OutputCache без VaryByCustom = "User", то должно быть очевидно, что страница может вернуть любую кэшированную версию независимо от того, какой запрос пользователя создал кэшированную копию.Насколько это рискованно?

Ответы [ 3 ]

9 голосов
/ 06 января 2012

Кэширование происходит перед действием.Скорее всего, вам потребуется настроить механизм авторизации для обработки сценариев кэширования.

Проверьте вопрос, который я опубликовал некоторое время назад - Пользовательская аутентификация, авторизация и реализация ролей MVC .

Часть, которая, как мне кажется, поможет вам, - это пользовательский атрибут авторизации, метод 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;
}
7 голосов
/ 12 мая 2012

Я вернулся к этой проблеме и, немного поработав, пришел к выводу, что нельзя использовать "из коробки" System.Web.Mvc.AuthorizeAttribute вместе с "* из коробки" System.Web.Mvc.OutputCacheAttribute при использовании Azure DistributedCache . Основная причина в том, что, как говорится в сообщении об ошибке в исходном вопросе, метод обратного вызова проверки должен быть статическим, чтобы использовать его с DistributedCache Azure. Метод обратного вызова кэша в атрибуте MVC Authorize является методом экземпляра.

Я пытался выяснить, как заставить его работать, создав копию AuthorizeAttribute из источника MVC, переименовав его, подключив его к действию с OutputCache, подключенным к Azure, и отладив. Причина, по которой метод обратного вызова кэша не является статическим, заключается в том, что для авторизации атрибут должен проверить пользователя HttpContext по значениям свойств Users и Roles, которые устанавливаются при создании атрибута. Вот соответствующий код:

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 основных действия:

  1. Проверяет, что HttpContextBase.User.Identity.IsAuthenticated == true

  2. Если атрибут имеет непустое строковое свойство Users, проверяет соответствие HttpContextBase.User.Identity.Name одному из значений, разделенных запятыми.

  3. Если атрибут имеет непустое строковое свойство 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, которые основаны на открытых свойствах Users и Roles.

Моя первая попытка состояла в том, чтобы передать эти значения в функцию обратного вызова, используя аргумент object data в CacheValidateHandler. Даже после введения статических методов это все равно не сработало и привело к тому же исключению. Я надеялся, что данные объекта будут сериализованы, а затем переданы обратно обработчику проверки во время обратного вызова. По-видимому, это не тот случай, и при попытке сделать это DistributedCache Azure по-прежнему считает его нестатическим обратным вызовом, что приводит к тому же исключению и сообщению.

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

Моя вторая попытка состояла в добавлении значений в коллекцию HttpContext.Items, поскольку экземпляр HttpContext автоматически передается в обработчик. Это тоже не сработало. HttpContext, который передается в CacheValidateHandler , не является тем же экземпляром , который существовал в свойстве filterContext.HttpContext. Фактически, когда CacheValidateHandler выполняется, он имеет нулевой сеанс и всегда имеет пустую коллекцию элементов.

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

Однако ...

Несмотря на то, что, похоже, нет никакого способа передать значения свойства Users & Roles обратно в обработчик обратного вызова проверки кэша, HttpContext, переданный ему , фактически имеет правильного принципала пользователя . Кроме того, ни одно из действий, в которых я в данный момент хочу объединить [Authorize] и [OutputCache], никогда не передает свойство Users или Roles в конструктор AuthorizeAttribute.

Таким образом, можно создать пользовательский атрибут AuthenticateAttribute, который игнорирует эти свойства и проверяет только User.Identity.IsAuthenticated == true. Если вам нужно пройти аутентификацию для определенной роли, вы также можете сделать это и объединить с OutputCache ... однако вам потребуется отдельный атрибут для каждой (набора) ролей, чтобы сделать статический метод обратного вызова проверки кэша статическим. , Я вернусь и выложу код после того, как немного его отполирую.

2 голосов
/ 31 декабря 2011

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

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

...