2011年10月29日土曜日

URLRewriteのoutboundRulesでセッションIDを含んだHTML内のURLを普通のURLに戻す、リベンジ

タイトル長っ!

無聊を託つ: Controllerを名前から生成するしHTMLを書き換えたりもしてみる

ちょっと前にエントリしました。が、間違えてました。思いっきり。これを信じてくれた人すいません。リベンジです!今度はこないだのよりだいぶマシ。

目的は、OutputCacheを利用する際にCookieless URL(セッションIDとかを含んだURL)をHTMLに保持してると、他の人とセッション共有しちゃうから、それを防ぐ!です。セキュリティ的に守らなきゃ、という理由だけでページ全体をno-cacheにするのはあまりにも富豪。

さらに、gzipで動的コンテンツを圧縮することで、CPUは多めに使うことがあるけど、OutputCacheで相殺。もちろんレスポンス性能が劇的に向上するので、ユーザーにとってはいいことづくし。

<rewrite>
  <outboundRules>
    <rule name="Sessionless" preCondition="html" enabled="true">
      <match filterByTags="A, Area, Base, Form, Frame, Head, IFrame, Img, Input, Link, Script, CustomTags" 
				customTags="All" 
				pattern="(.*)/\([SFA]\([^/]+\)\)/(.*)"/>
      <action type="Rewrite" value="{R:1}/{R:2}"/>
      <conditions>
        <add input="{REQUEST_URI}" matchType="Pattern" pattern="\)/mobile" ignoreCase="true" negate="true" />
      </conditions>
    </rule>
    <customTags>
      <tags name="All" />
    </customTags>
    <preConditions>
      <preCondition name="html">
        <add input="{RESPONSE_CONTENT_TYPE}" pattern="text/html" />
        <add input="{REQUEST_URI}" pattern="/mobile" negate="true" />
      </preCondition>
    </preConditions>
  </outboundRules>
</rewrite>

これが、たぶん正解のRewrite用のconfig。ちゃんと確認してみます。

せっかくなので、MVC4DPを利用してみましょう(意味なくはない)。プロジェクト作って実行すると表示される画面は↓こうですね。

rewrite1

この時のURLは http://localhost:61972/ です。

このままではやりにくいので、sessionStateでcookieless="UseUri"にします。

rewrite2

見た目が変わるわけじゃないです。URLを見てくださいね。今度は http://localhost:61972/(S(l2ulbdgxdrbdoyrozczknp1p))/ となっていますね。この状態でソースを確認します。

rewrite3

バッチリセッションIDがURLに含まれてますね。先ほどのconfigをsystem.webServer内に追記するとどうなるか。

rewrite4

セッションIDが消えました!概ね...。部分的に残ってる部分があるんですけど、それは多分仕様。と、いうのもAタグのhref属性の前にdata-dialog-title属性が入ってますよね。これがあるとURLRewriteが対象だと判断してくれないみたいです。試しに、_LogOnPartial.cshtmlを変更してみます。

@if (Request.IsAuthenticated) {
    <p>
        Hello, @Html.ActionLink(User.Identity.Name, "ChangePassword", "Account", null, new { @class = "username" })!
        @Html.ActionLink("Log Off", "LogOff", "Account")
    </p>
} else {
    <ul>
        <li>@Html.ActionLink("Register", "Register", "Account", routeValues: null, htmlAttributes: new { id = "registerLink", data_dialog_title = "Registration" })</li>
        <li>@Html.ActionLink("Log on", "LogOn", "Account", routeValues: null, htmlAttributes: new { id = "logonLink", data_dialog_title = "Identification" })</li>
    </ul>
}

↑こっちがオリジナル。で、↓こっちが修正版。違いはhtmlAttributesのdata_dialog_titleの有無。

@if (Request.IsAuthenticated) {
    <p>
        Hello, @Html.ActionLink(User.Identity.Name, "ChangePassword", "Account", null, new { @class = "username" })!
        @Html.ActionLink("Log Off", "LogOff", "Account")
    </p>
} else {
    <ul>
        <li>@Html.ActionLink("Register", "Register", "Account", routeValues: null, htmlAttributes: new { id = "registerLink" })</li>
        <li>@Html.ActionLink("Log on", "LogOn", "Account", routeValues: null, htmlAttributes: new { id = "logonLink" })</li>
    </ul>
}

そうするとレンダリングされるHTMLは↓こうなります。

rewrite5

RegisterとLog onのURLからもSessionIDが消えて正しくなりました。htmlAttributesで追加した属性はTagBuilderで展開されるときにアルファベット順に出力(SortedDictionary)されるんですね。なので、hrefより先にdata属性が展開される、と。この辺、どうするんでしょうね。正しくはURLRewriteのOutboud Providerが対応することなんだと思うけど...。

とりあえず、今のところスルー。さーせん。

ちなみに前回はこれを仮想ディレクトリ配下にデプロイせずに「出来たできた~」と浮かれてて、実はちゃんと消えないっていうダメっぷり。あと、SessionIDだけじゃなくCookielessの場合は認証チケット(F)も匿名ID(A)もURLに含まれるのにSだけ見てて、これまたちゃんと消えないっていうダメダメっぷり。

今回はちゃんと確認。

rewrite6

ローカルIISのmvc4dpにデプロイ。URLは view-source:http://localhost/mvc4dp/(S(cow2znspocwggst1mah50myt))/ です。これもちゃんとHTML中のURLからはちゃんとSessionIDが消えました!

でー。この状態で今度はアウトプットキャッシュをOnにします。そのためにHomeControllerに以下の追記。

    [OutputCache(Duration = 60)]
    public class HomeController : Controller
    {
        public ActionResult Index()
        {

これをVS実行環境で見てみる。

rewrite7

ちゃんと出力キャッシュが効いてる証に、max-ageとExpiresが出てますね。

ローカル環境でもキャッシュの有無で少しだけ、結果が違いますね。

rewrite8 rewrite9

少しだけね。で、このキャッシュされてるHTMLにはもちろんSessionIDは含まれてません。

ココからさらに動的圧縮をOnにするために以下の記述を追加。

    <urlCompression
      doStaticCompression="true"
      doDynamicCompression="true"
      dynamicCompressionBeforeCache="false" />

と、いいたいところですが、残念ながらこれはこのままでは機能しないんです。

URL Rewrite Outbound Rules w/ Compression : The Official Microsoft IIS Site

↑ココに書かれてる通り、 レジストリに項目追加が必要です。切ないですね。でも、まぁ、いいでしょう。

Windows Registry Editor Version 5.00

[HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\InetStp\Rewrite]
"LogRewrittenUrlEnabled"=dword:00000000

これをレジストリに追加。

で、いざ!と思いきや...。

rewrite10

ざんねーん。IIS Expressではどうもこのレジストリの値を見てくれないみたいです。対策も特に見つけられなかったです。なのでローカルのIISにデプロイしたほうで試します。

rewrite11 rewrite12

たたーん!出力キャッシュも効いてるし、gzipも効いてます。さらにHTML中のSession IDを持ってるURLもなくなってます!Content-Lengthが5.77KBから2.61KB。

標準(追加モジュールですけどMS謹製)でもココまで出来ました。

これ以上は独自のResponse Filterを書いてHTML中のURLを操作する方法になるでしょう(もちろんURL RewriteのProviderを実装という手もあるけど、それ書くくらいならFilterのほうが低コストじゃないですかねー)。

大規模サイトもコレで安心ですね。

ちなみに、ですけど、すべてのサイトでコレを設定すれば早くなるわけじゃないので用法・用量を守って正しくお使いください。

dotnetConf2015 Japan

https://github.com/takepara/MvcVpl ↑こちらにいろいろ置いときました。 参加してくださった方々の温かい対応に感謝感謝です。