Zavedení kešování direktivou @OutputCache je velmi snadné, avšak nevhodným použitím si můžeme přivodit nechtěné zobrazení citlivých údajů třetí straně. V tomto článku si ukážeme, jak i nadále @OutputCache využívat a přitom vyzrazení citlivých údajů zabránit.

Snadnost deklarativního nastavení kešování stránky nebo uživatelských ovládacích prvků lehce svádí k jeho použití všude, kam to jen lze napsat. Je proto důležité si uvědomit, že kešování může být dobrým sluhou, ale špatným pánem, pokud je živelné a nerozumně rozvržené.

Nejprve bychom si tedy měli rozvrhnout hierarchii kešování. K nevhodnému rozvržení patří například i situace, kdy jsou do sebe zanořeny prvky, které všechny mají nějakým způsobem deklarovanou @OutputCache – díky zanoření kešovaných prvků může docházet k nedefinovaným efektům a závislostem, mějme proto na paměti, že v nejnepříznivějším případě se mohou doby všech do sebe vnořených prvků sečíst. Dobrým pravidlem proto je nevnořovat do sebe více kešovaných uživatelských prvků – kešujme pouze ty vložené jednotlivě, nebo hromadně celý nadřízený prvek. V celé stránce, která samozřejmě může také být kešovaná, pak budou pouze uživatelské ovládací prvky, které ponesou pouze v jedné úrovni zanoření direktivu @OutputCache.

Jiný pohled na kešování se ukazuje v případě, že webová aplikace pracuje s citlivými údaji. Výchozí chování je takové, že stránky doplněné HTTP autentizací jsou chápány jako soukromé (private) a nebudou sdílenými kešemi ukládány. Smutným faktem je, že HTTP (basic) autentizaci dnes pro reálné aplikace prakticky nepoužíváme – ASP.NET nabízí řadu modernějších autentizačních mechanismů, takže toto chování se v našich aplikacích neprojeví, pokud nevyužíváme „basic“ autentizaci (například pomocí basic autentizačního modulu). Je tedy jasné, že se musíme sami postarat o to, aby server naší aplikace korektně zajišťoval strategii kešování odpovědí, aby se zamezilo nechtěnému úniku citlivých údajů. Klient potom tyto direktivy serveru (odesílané formou HTTP hlaviček odpovědi) respektuje, tak jak definuje standard HTTP komunikace.

Stránky doručené do prohlížeče přes HTTPS obvykle obsahují mnohem citlivější údaje, které vyžadují také ochranu před různými zdroji útoků (může jimi být například spyware na uživatelově PC nebo další uživatelé používající daný stroj například v internetové kavárně). Přesto, že samo použití HTTPS zamezí ukládání citlivých údajů ve sdílených keších (jako jsou třeba proxy), i tak bychom měli zajistit odesílání odpovídajících direktiv pro kešování pro důkladné zajištění bezpečnosti. A to zvláště proto, že chování prohlížečů s ohledem na ukládání takových stránek v lokální keši, kde mohou být zneužitelné neautorizovaným přístupem, není jasně definované – běžně totiž prohlížeče i tyto stránky ukládají do lokální keše, kde jsou zranitelné. Vývojář by měl takové chování brát v potaz a zamezit také lokální keši v ukládání citlivých informací.

Potřebné chování zajistíme odesláním patřičných HTTP hlaviček Pragma, Cache-Control, Expires a Last-Modified:

Cache-Control
  • private: Označuje odpověď jako soukromou, určenou pouze jedné osobě. Taková odpověď nesmí být uložena v žádné sdílené keši. V soukromé (nesdílené) keši uložena být může.
  • no-store: Zabraňuje nechtěnému úniku a zadržení citlivých informací. Platí obousměrně, tedy ani odpověď, ani požadavek, který ji vyvolal, nesmí být uloženy. Keš nesmí informace ukládat do nevolatilní paměti a musí se pokusit je co nejdokonaleji odstranit z volatilní paměti.
  • no-cache: Zakazuje, aby odpověď byla použita pro uspokojování dalších požadavků bez úspěšné revalidace na cílovém serveru. Tím je cílovému serveru umožněno, aby zabránil ukládání odpovědi dokonce i v keších, které jsou nakonfigurovány pro poskytování „nečerstvých“ odpovědí.
  • must-revalidate: Keš nesmí odpověď použít poté, co se stane „nečerstvou“, musí ji nejdříve validovat na cílovém serveru, tedy provést revalidaci pokaždé, když se odpověď dle hlavičky Expires nebo max-age stane „nečerstvou“). Servery by měly začleňovat direktivu must revalidate jedině tehdy, pokud by výsledkem neprovedené revalidace byly chybné operace, jako například skryté nevyřízení finanční transakce.
  • no-transform: Proxy nesmí ve zprávě ani v žádném požadavku modifikovat ani přidávat žádnou z hlaviček Content-Encoding, Content-Range a Content-Type. (Proxy totiž může například převádět formáty obrázků, aby snížila nároky na přenosovou linku. Toto ovšem může způsobovat vážné problémy například při autentizaci.) Direktiva zakazuje změnu hlaviček.
Pragma
no-cache: Nekešovat v žádných keších – ani místních, ani sdílených (například v proxy a podobně), platí to samé jako pro hlavičku Cache-Control popsanou výše. Navíc se tvrdí, že řada klientů tuto hlavičku ignoruje, její použití je tedy pouze doplňkové.
Expires
Říká všem keším, po jakou dobu je objekt „čerstvý“, po tomto čase se eše vždy zeptají původního serveru, zda byl dokument změněn.
Last-Modified
Datum poslední modifikace – jednoduše řečeno, záznam uložený v keši je platný, pokud od data Last-Modified nebyla odpověď serveru změněna.

Ačkoli se nabízí i možnost použít metaelementy jako <meta http-equiv="Pragma" content="no-cache" />, toto řešení není úplně nejvhodnější a je třeba ho chápat pouze jako doplňkové. A to zejména proto, že sdílené keše obsah přenášeného dokumentu nijak nezkoumají a tudíž i veškeré metaelementy kompletně ignorují. Přesto jsem dále v článku uvedl ukázku, jak můžeme tyto doplňkové elementy v naší aplikaci ovládat programově v souladu s programovým řízením HTTP hlaviček – nikdy nevíme, jak přesně se ten který klient zachová (zvláště v dnešní záplavě mobilních zařízení) a zda mu metaelementy „nepřijdou k duhu“.

Shrneme-li výše popsané zásady do požadavku na praktické řešení, pak se mi jako výhodné řešení jeví použití @OutputCache s nastavením Location="DownStream" a také použití této direktivy v jednotlivých uživatelských prvcích v jedné úrovni zanoření. Tím vyhovíme pravidlu nezanořovat více částí kešovaných na serveru – na serveru se tedy budou kešovat pouze jednotlivé uživatelské prvky, celá stránka se na serveru kešovat nebude a bude pouze odesílat HTTP hlavičky informující, že všechny následné prvky „po trase“ mohou obsah kešovat – tedy prohlížeč klienta, proxy servery a další.

Příklad takového nastavení může vypadat takto:

<a href=’mailto:%25@OutputCache‘>%@OutputCache</a> Location=“DownStream“ Duration=“3600″ VaryByHeader=“Accept“ VaryByParam=“*“ VaryByCustom=“ContentUrl;Culture;Roles;DeviceType“%

Nyní se může naskytnout otázka, co v případě, že se nám do naší aplikace přihlásí uživatel a aplikace v důsledku toho bude zobrazovat citlivé údaje. V takovém případě samozřejmě potřebuje „zlikvidovat“ i jakékoli kešování „po trase“, tedy jakoby odebrat direktivu @OutputCache ze stránky. Tady se nám nabízí programová práce s objektem HttpResponse.Cache naší stránky.

Připravil jsem proto vhodnou metodu, která zajistí odeslání „nekešujících“ hlaviček, jak jsme si je už dříve popsali. Ve skutečné aplikaci by pak metoda měla být volána v případě, že uživatel přistupující na stránku je ověřen (autorizován). Pokud je tomu tak, kešovat se bude pouze na serveru (pouze uživatelské ovládací prvky, kešování celé stránky je již zrušeno nastavením Location v direktivě stránky) a ostatní kešování se explicitně zakáže.

private void SetServerOnlyCacheLocation()
{
  Response.Cache.SetNoTransforms(); // Sets the Cache-Control: no-transform HTTP header.
  Response.Cache.SetNoStore(); // Sets the Cache-Control: no-store HTTP header.
  Response.Cache.SetCacheability(HttpCacheability.ServerAndNoCache); // indicates that the content is cached at the server but all others are explicitly denied the ability to cache the response.
  Response.Cache.SetLastModified(DateTime.Now);
  Response.Cache.SetRevalidation(HttpCacheRevalidation.AllCaches);
  Response.Cache.SetValidUntilExpires(true); // if true, ASP.NET ignores cache invalidation headers and the page remains in the cache until it expires
}

A nyní zpět k řízení kešování – v naší metodě pro vrácení hodnoty použijeme jednoduše čas posledního zápisu do souboru prostřednictvím statické metody GetLastWriteTime():

private void Page_Load(object sender, System.EventArgs e)
{
  if (Page.User.Identity.IsAuthenticated)
    SetServerOnlyCacheLocation();
  ..
  ..
}

V případě, že je uživatel přihlášen, můžeme ještě zdůraznit vliv HTTP hlaviček doplněním metaelementů. Mají vliv skutečně pouze na klienta, nicméně uvedení těchto elementů je aplikaci každopádně ku prospěchu. Do hlavičky stránky je můžeme opět umístit jako serverové prvky, díky čemuž je pak můžeme ovládat programově:

<meta id=“MetaCacheControl“ http-equiv=“Cache-Control“ RunAt=“Server“ Visible=“False“ />
<meta id=“MetaPragma“ http-equiv=“Pragma“ RunAt=“Server“ Visible=“False“ />
<meta id=“MetaExpires“ http-equiv=’Last-Modified‘ RunAt=“Server“ Visible=“False“ />

V případě, že je uživatel přihlášen a nemá se tedy kešovat „po trase“, zviditelníme dané elementy s nastavením vhodných hodnot pro zamezení kešování:

 if (Page.User.Identity.IsAuthenticated)
{
  MetaCacheControl.Attributes.Add(„content“,“No-Cache“);
  MetaCacheControl.Visible = true;
  MetaPragma.Attributes.Add(„content“,“No-Cache“);
  MetaPragma.Visible = true;
  MetaExpires.Attributes.Add(„content“,“-1″);
  MetaExpires.Visible = true;
}

Velmi agresivního zamezení kešování dosáhneme přidáním náhodně generovaného parametru do URL požadavku. Toto řešení je často jedinou skutečně funkční možností, jak zabránit kešování na různých mobilních zařízeních a wap branách. V běžných aplikacích se ovšem může hodit pro případ, kdy chceme ve stránce zavádět klientský skript, který je generován dynamicky a jehož obsah by se měl kešovat zhruba stejně, jako obsah nějakého ve stránce vloženého uživatelského ovládacího prvku. Uživatelský ovládací prvek zajistí kešování hodnoty náhodně generovaného parametru, takže s přegenerováním obsahu onoho ovládacího prvku se vynutí i stažení čerstvé verze klientského skriptu, protože tento parametr se změní.

Příklad volání u klienta kešovaného klientského skriptu z uživatelského ovládacího prvku kešovaného na serveru – při občerstvení obsahu prvku je klient „donucen“ stáhnout klientský skript znovu:

<script src=“ClientFunction.PopUpMenu.js.aspx?__ufps=<%=DateTime.Now.Ticks.ToString()%>“ type=“text/javascript“ charset=“utf-8″></script>

Do parametru s názvem __ufps (název je inspirován z chování mobilních ASP.NET stránek, které jej využívají také) prostě přidáváme Ticks (počet „úderů“ aktuálního data a času).

Závěrem bych zdůraznil, že při omezení kešování je potřeba také uvážit nejen zvýšení zátěže na naše systémy, ale také zvětšení objemu přenesených dat. Ještě více se toto projeví při použití HTTPS. Pokud tedy ještě nevyužíváte HTTP kompresi, kdy jsou data klientovi odesílána ve sbalené podobě, vyzkoušejte její použití ve své aplikaci. Servery IIS6 a IIS7 ji podporují nativně, stačí ji pouze zapnout, u starší verze IIS5.x si můžeme vypomoci kompresním modulem (sám mám velmi dobré zkušenosti s HttpCompress).

Soubor s ukázkami zdrojových kódů si můžete stáhnout a volně použít (viz zdrojový kód).

Starší komentáře ke článku

Pokud máte zájem o starší komentáře k tomuto článku, naleznete je zde.

Žádný příspěvek v diskuzi

Odpovědět