しばやん雑記

Azure とメイドさんが大好きなフリーランスのプログラマーのブログ

ASP.NET MVC で reCAPTCHA を使ってみた

今更感が半端ないですが、ASP.NET MVC で開発したアプリケーションに reCAPTCHA の version 2 を組み込む方法を調べました。何番煎じか分かりませんが、小ネタ程度にまとめておきます。

reCAPTCHA を使うためには Google のサイトから URL を登録して、トークンを貰う必要があります。

reCAPTCHA

ドキュメントも数ページしかないので、これを読めば誰でも実装は出来るはずです。

Developer's Guide  |  reCAPTCHA  |  Google for Developers

流れとしてはビューにタグを追加しておき、POST されたデータを API に投げて正しいかチェックするだけです。この流れは ValidateAntiForgery と似てるので、例によって Action Filter として実装します。

とりあえず作成した ValidateReCaptcha 属性のコードを丸ごと載せておきます。

public class ValidateReCaptchaAttribute : ActionFilterAttribute
{
    private const string SiteVerifyEndpoint = "https://www.google.com/recaptcha/api/siteverify";
    private const string ResponseFieldKey = "g-recaptcha-response";

    private static readonly string SecretKey = ConfigurationManager.AppSettings["ReCaptcha:SecretKey"];

    public override void OnActionExecuting(ActionExecutingContext filterContext)
    {
        var data = new NameValueCollection
        {
            { "secret", SecretKey },
            { "response", filterContext.HttpContext.Request.Form[ResponseFieldKey] },
            { "remoteip", filterContext.HttpContext.Request.UserHostAddress }
        };

        var json = new WebClient().UploadValues(SiteVerifyEndpoint, data);

        var result = JsonConvert.DeserializeObject<ReCaptchaVerifyResponse>(Encoding.UTF8.GetString(json));

        if (!result.Success)
        {
            throw new ReCaptchaValidationException(result.ErrorCodes[0]);
        }
    }
}

public class ReCaptchaVerifyResponse
{
    public bool Success { get; set; }

    [JsonProperty("error-codes")]
    public string[] ErrorCodes { get; set; }
}

使い方の説明は要らない気がしますが、検証したいアクションに属性を付けるだけです。

public class HomeController : Controller
{
    public ActionResult Index()
    {
        return View();
    }

    [HttpPost]
    [ValidateReCaptcha]
    public ActionResult Index(FormCollection collection)
    {
        return View("Success");
    }
}

用意したビューは以下のような感じです。特に言うことはありません。

<script src="https://www.google.com/recaptcha/api.js"></script>

<h2>Index</h2>

@using (Html.BeginForm())
{
    <div class="g-recaptcha" data-sitekey="..."></div>
    <br/>
    <button type="submit">Submit</button>
}

これで準備が出来たので、実際に実行して動作を確認しておきます。ブラウザでページを表示すると、誰もが 1 回は見たことあると思われる CAPTCHA が表示されます。

チェックを入れて Submit を行うと、検証が成功するのでページが表示されます。

チェックを入れずに Submit を行うと、検証に失敗して例外が投げられるので、そこで処理が中断されます。

思ったより簡単な仕組みだったので、昼休みの暇潰し程度で試せました。ASP.NET MVC 6 だと Action Filter が Task ベースになるので、HttpClient を使いたいところです。

FatAntelope を使って XML Document Transform (XDT) を自動で生成する

主に ASP.NET アプリケーションの Web.config 変換で使われている XML Document Transform (XDT) ですが、個人的にはあれを手書きするのはかなり嫌いでした。

XML の差分を持っているだけなので、自動生成できるのではないかと長い間思ってましたが、最近 FatAntelope というまさに待ち望んでいたライブラリが公開されました。

2 つの XML を指定すると、自動的に差分を取って XDT として書き出してくれます。最高ですね。コマンドラインツールやライブラリの形で公開されているので、簡単に使い方の紹介をしておきます。

XDT を生成する

FatAntelope のコマンドラインツールは GitHub で公開されているので最新版をダウンロードしてきます。

今回はサンプルとして Web.config っぽいものを適当に用意しました。開発時に使うものになります。

<?xml version="1.0" encoding="utf-8"?>
<configuration>
  <connectionStrings>
    <add name="DefaultConnection"
         connectionString="Data Source=.\SQLEXPRESS;Initial Catalog=DevelopmentDB;Integrated Security=True"
         providerName="System.Data.SqlClient" />
  </connectionStrings>
  <system.web>
    <compilation debug="true" targetFramework="4.6" />
    <httpRuntime targetFramework="4.6" />
  </system.web>
</configuration>

そしてこっちが本番用の Web.config になります。内容としては接続文字列を本番向けにして、リリースビルドを行うというものです。

<?xml version="1.0" encoding="utf-8"?>
<configuration>
  <connectionStrings>
    <add name="DefaultConnection"
         connectionString="Server=tcp:***.database.windows.net,1433;Database=ProductionDB;User ID=***;Password=***;Trusted_Connection=False;Encrypt=True;Connection Timeout=30;"
         providerName="System.Data.SqlClient" />
  </connectionStrings>
  <system.web>
    <compilation targetFramework="4.6" />
    <httpRuntime targetFramework="4.6" />
  </system.web>
</configuration>

この 2 つのファイルを FatAntelope に与えると、XDT が生成されます。生成された XDT はひとまず Web.Release.config とでも名前を付けて保存するようにします。

FatAntelope Web.config Production.config Web.Release.config

コマンドを実行すると、コンソールにメッセージが出るのでエラーにならなければ成功です。

そして出力された XDT が以下のようになります。ちゃんと差がある部分だけを抽出してくれています。

<configuration xmlns:xdt="http://schemas.microsoft.com/XML-Document-Transform">
  <connectionStrings>
    <add connectionString="Server=tcp:***.database.windows.net,1433;Database=ProductionDB;User ID=***;Password=***;Trusted_Connection=False;Encrypt=True;Connection Timeout=30;" xdt:Transform="SetAttributes(connectionString)" />
  </connectionStrings>
  <system.web>
    <compilation xdt:Transform="RemoveAttributes(debug)" />
  </system.web>
</configuration>

手書きの場合と比較すると xdt:Locator が無かったりと、少し違和感があるかもしれませんが、XDT の仕様としてはこれで問題はありません。XML 宣言は何故か付かないので、後で追加しましょう。

C# から XDT を生成する

FatAntelope は NuGet でライブラリが公開されているので、API には多少癖がありますが、自分のアプリケーションなどに簡単に組み込むことが可能です。

当然ながら NuGet を使って FatAntelope をインストールする必要があります。

Install-Package FatAntelope

FatAntelope の API は多少癖があるので、最低限の XDT 生成を行うコードを紹介しておきます。

using System;
using System.Xml;

using FatAntelope;
using FatAntelope.Writers;

namespace ConsoleApplication17
{
    class Program
    {
        static void Main(string[] args)
        {
            // 元になる XML
            var baseXml = new XmlDocument();

            baseXml.Load(@"C:\Users\shibayan\Downloads\FatAntelope.v0.2.3\Web.config");

            // 新しい XML
            var newXml = new XmlDocument();

            newXml.Load(@"C:\Users\shibayan\Downloads\FatAntelope.v0.2.3\Production.config");

            // XmlDocument から XTree を作成しておく
            var baseTree = new XTree(baseXml);
            var newTree = new XTree(newXml);

            // XDiff を使って差分を計算し、newTree に格納
            XDiff.Diff(baseTree, newTree);

            // 差分が格納された XTree を元に XDT として書き出し
            var patch = new XdtDiffWriter().GetDiff(newTree);

            patch.Save(Console.Out);

            Console.WriteLine();
        }
    }
}

XDiff.Diff を呼び出した結果がイマイチな感じですね。これを実行すると XDT がコンソールに出力されます。

生成された XDT をどのように使うかはお任せします。MSBuildTask などにしてしまうのも手だと思います。

おまけ:XDT を XML に反映させる

完全におまけですが、元になる XML と XDT を使って、差分が反映された XML を生成する方法も紹介しておきます。ちなみにこちらは Microsoft 純正のライブラリが提供されています。

http://blogs.msdn.com/b/webdev/archive/2013/04/23/xdt-xml-document-transform-released-on-codeplex-com.aspx

ライブラリは NuGet で公開されているので、FatAntelope と同じようにサクッと使えます。

基本的に XmlDocument を継承したクラスを使って XML に対して XDT を適用する形になります。こちらもサンプルコードを紹介しておきます。

using System;

using Microsoft.Web.XmlTransform;

namespace ConsoleApplication17
{
    class Program
    {
        static void Main(string[] args)
        {
            // 変換元の XML を読み込む
            var source = new XmlTransformableDocument();

            source.Load(@"C:\Users\shibayan\Downloads\FatAntelope.v0.2.3\Web.config");

            // XDT を読み込んで Apply で反映させる
            var xdt = new XmlTransformation(@"C:\Users\shibayan\Downloads\FatAntelope.v0.2.3\Web.Release.config");

            xdt.Apply(source);

            source.Save(Console.Out);

            Console.WriteLine();
        }
    }
}

実行すると XDT の内容が反映された XML がコンソールに出力されます。

これまでは XDT を適用するという一方向のみの対応でしたが、FatAntelope のリリースによって XML から XDT を作成するという、双方向の変換が可能になりました。

個人的には Web.config 変換を書きやすくするための Visual Studio 拡張を作りたいですね。

ASP.NET MVC 5 アプリケーションを .NET Framework 4.6 と C# 6.0 にアップデートしてみた

リリースされたばかりの .NET Framework 4.6 が Azure Web Apps で使えるようになったらしいので、動かしていた ASP.NET MVC 5 なアプリケーションを ASP.NET 4.6 と C# 6.0 にアップデートしてみました。

8 月中にリリースという話でしたが、思ったよりも早かったです。

ポータルの表示はまだ 4.5 なので注意が必要です。Azure 公式ブログもアップデートされています。

http://azure.microsoft.com/blog/2015/08/11/update-on-net-framework-4-6-and-azure/

Azure 界の抱かれたい男 No.1 こと @kosmosebi、通称ぶちぞうさんも既にブログで Web Apps の .NET 4.6 対応について書いてます。要チェックです。

Azure App Service に.NET Framework 4.6がロールアウトされました | ブチザッキ

現時点では RyuJIT は無効化されているのと、Web Apps は Windows Server 2012 で動いていて HTTP/2 は使えないので、Web Forms を使っている人ぐらいにしか大きな恩恵は無さそうです。

これだけだとアレなので、C# 6.0 を使う方法を紹介することにします。

ASP.NET と C# 6.0 の関係

C# 6.0 の機能はコンパイラが提供するので .NET Framework 4.6 とは別扱いです。このあたりの関係がどうにもわかりにくく感じたので、表にしてみました。

ASP.NET 4.5 ASP.NET 4.6 ASP.NET 5
cs ファイル △ (Roslyn が必要) ○ (MSBuild のパスを変更)
ASPX / Razor △ (CodeDOM Provider が必要) △ (CodeDOM Provider が必要)

.NET 4.6 をインストールすると Roslyn 込みの MSBuild がインストールされているので、C# のコードは MSBuild のパスを変えるだけでビルド可能になりますが、実際には CodeDOM Provider が Roslyn もインストールするので、特に設定しなくても使えるようになります。

結局のところ CodeDOM Provider をインストールすれば C# 6.0 が使えるようになるってことです。

例外的存在の ASP.NET 5 は Roslyn が組み込まれているので、デフォルトで C# 6.0 に対応しています。ASP.NET 5 に関しては RC1 が出てから本気を出すつもりでいます。

ASP.NET MVC 5 で C# 6.0 を有効にする

ASPX や Razor で C# 6.0 の機能を使うためには CodeDOM Provider をインストールする必要があります。

http://blogs.msdn.com/b/webdev/archive/2015/07/20/announcing-asp-net-4-6-and-asp-net-5-beta-5-in-visual-studio-2015-release.aspx

NuGet パッケージを調べてみると、このパッケージは .NET 4.6 だけではなく .NET 4.5 でも動くみたいなので、C# 6.0 の機能はすぐに使え始めることが出来そうです。

ちなみに Visual Studio 2015 を使って ASP.NET MVC プロジェクトを作成すると、最初から CodeDOM Provider がインストールされています。なので、特に何も考えることなく C# 6.0 を使って開発が行えます。

一緒にインストールされている Microsoft.Net.Compiler は Roslyn の本体になります。

展開されたパッケージを見ると csc や vbc が存在しているのが確認できます。

MSBuild 用の targets ファイルも含まれているので、ASP.NET アプリケーション以外に WebJobs などのコンソールアプリケーションでも C# 6.0 の機能が利用可能です。

話が逸れましたが、この CodeDOM Provider をインストールすると Web.config には system.codedom 要素が追加されているので、この設定を変えることで C# のバージョンを変更したりできます。

実際に行ったことは CodeDOM Provider のインストールだけですが、これだけで GitHub などのソース管理からのデプロイも問題なくできるようになります。もちろん Web Deploy も利用可能です。

以前に紹介した aspnet_compiler.exe を使ってプリコンパイルを行う方法もそのまま使えました。

コンパイル時間が前よりも大幅に遅くなった気がしますが、C# 6.0 が使えるので我慢しようかと思います。

ASP.NET アプリケーションを Azure Web Apps にソース管理からデプロイする時にプリコンパイルさせる方法

Azure Web Apps に ASP.NET アプリケーションを GitHub などからデプロイする場合に、ビューのプリコンパイルを行いたいと思っていましたが、カスタムデプロイスクリプトを書くことで対応できたので紹介します。

やはり同じことを考える人は居たようですが、スクリプト例が見つかりませんでした。

aspnet_compiler.exe について色々書かれていましたが、現行の Web Apps にインストールされているもので問題なくプリコンパイル出来ました。

スクリプトの重要な部分というか、修正が必要な部分を順に解説していきます。

プリコンパイルを行う準備

最初にプリコンパイル前のビルド結果を一時的に格納させるためのディレクトリと、aspnet_compiler.exe のパスを用意しておきます。基本的に存在しないはずなので常に新しく作ります。

SET PRECOMPILE_TEMP=%temp%\___precompileTemp%random%
mkdir "%PRECOMPILE_TEMP%"

SET ASPNET_COMPILER_PATH=%WINDIR%\Microsoft.NET\Framework\v4.0.30319\aspnet_compiler.exe

これでランダムなディレクトリがデプロイごとに作成されます。

MSBuild の出力ディレクトリを変更

通常のデプロイスクリプトでは MSBuild の実行結果を DEPLOYMENT_TEMP に書き出すのですが、プリコンパイルするために今回作成した PRECOMPILE_TEMP に書き出すように変更します。

call :ExecuteCmd "%MSBUILD_PATH%" /p:_PackageTempDir="%PRECOMPILE_TEMP%" %SCM_BUILD_ARGS%

このコマンドは長いの不要な部分は削除しました。オリジナルのスクリプトから _PackageTempDir を指定してる部分を探して、そこを PRECOMPILE_TEMP に設定します。

aspnet_compiler でプリコンパイル

PRECOMPILE_TEMP に MSBuild でビルドされたアプリケーションのファイル一式が入っているので、それをソースにして DEPLOYMENT_TEMP にプリコンパイルした結果を出力します。

call :ExecuteCmd "%ASPNET_COMPILER_PATH%" -v / -p "%PRECOMPILE_TEMP%" -c "%DEPLOYMENT_TEMP%"

仮想ディレクトリを表す -v オプションの値は / のままで基本は良いと思います。DEPLOYMENT_TEMP に結果を出力すると、後は KuduSync が wwwroot に差分をコピーしてくれます。

プリコンパイルに使ったディレクトリを削除

追加したプリコンパイル用のディレクトリは Kudu によって自動的に削除されないので、スクリプトの最後の方で削除するようにしておきます。

IF EXIST "%PRECOMPILE_TEMP%" rd /s /q "%PRECOMPILE_TEMP%"

一応存在チェックを行ってから削除するようにします。

完成したスクリプト例

ここまで紹介したスクリプトを全て組み込んだ例を最後に張り付けておきます。

当然ですが、そのままコピペで使えるというわけではないです。これで処理の全体像を掴んでください。

::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
:: Deployment
:: ----------

SET PRECOMPILE_TEMP=%temp%\___precompileTemp%random%
mkdir "%PRECOMPILE_TEMP%"

SET ASPNET_COMPILER_PATH=%WINDIR%\Microsoft.NET\Framework\v4.0.30319\aspnet_compiler.exe

echo Handling .NET Web Application deployment.

:: 1. Restore NuGet packages
IF /I "AspNet.sln" NEQ "" (
  call :ExecuteCmd nuget restore "%DEPLOYMENT_SOURCE%\AspNet.sln"
  IF !ERRORLEVEL! NEQ 0 goto error
)

:: 2. Build to the temporary path
IF /I "%IN_PLACE_DEPLOYMENT%" NEQ "1" (
  call :ExecuteCmd "%MSBUILD_PATH%" "%DEPLOYMENT_SOURCE%\AspNet\AspNet.csproj" /nologo /verbosity:m /t:Build /t:pipelinePreDeployCopyAllFilesToOneFolder /p:_PackageTempDir="%PRECOMPILE_TEMP%";AutoParameterizationWebConfigConnectionStrings=false;Configuration=Release /p:SolutionDir="%DEPLOYMENT_SOURCE%\.\\" %SCM_BUILD_ARGS%
) ELSE (
  call :ExecuteCmd "%MSBUILD_PATH%" "%DEPLOYMENT_SOURCE%\AspNet\AspNet.csproj" /nologo /verbosity:m /t:Build /p:AutoParameterizationWebConfigConnectionStrings=false;Configuration=Release /p:SolutionDir="%DEPLOYMENT_SOURCE%\.\\" %SCM_BUILD_ARGS%
)

IF !ERRORLEVEL! NEQ 0 goto error

:: 3. Precompile Razor
IF /I "%IN_PLACE_DEPLOYMENT%" NEQ "1" (
  call :ExecuteCmd "%ASPNET_COMPILER_PATH%" -v / -p "%PRECOMPILE_TEMP%" -c "%DEPLOYMENT_TEMP%"
  IF !ERRORLEVEL! NEQ 0 goto error
)

:: 4. KuduSync
IF /I "%IN_PLACE_DEPLOYMENT%" NEQ "1" (
  call :ExecuteCmd "%KUDU_SYNC_CMD%" -v 50 -f "%DEPLOYMENT_TEMP%" -t "%DEPLOYMENT_TARGET%" -n "%NEXT_MANIFEST_PATH%" -p "%PREVIOUS_MANIFEST_PATH%" -i ".git;.hg;.deployment;deploy.cmd"
  IF !ERRORLEVEL! NEQ 0 goto error
)

:: 5. Clean Precompile Dir
IF EXIST "%PRECOMPILE_TEMP%" rd /s /q "%PRECOMPILE_TEMP%"

デプロイスクリプトのひな形は Azure CLI を使うと簡単に作成できます。

Azure Web Apps (Websites) - Custom Deployment Scripts Generator

実際にこのスクリプトを組み込んだアプリケーションを Web Apps へ GitHub からデプロイすると、プリコンパイルが行われていることが確認できます。

ビューをプリコンパイルしておくことで、アプリケーションの起動パフォーマンスを改善出来ます。

特に Azure Web Apps はアクセスが無いとインスタンスが落とされる関係上、仮想マシンなどに置いてあるアプリケーションよりも初回起動が多くなる傾向にあるので、プリコンパイルは有効な手段になります。

インストール時に MachineKey を自動生成する NuGet パッケージを作った

大昔に紹介した MachineKey をオンラインで生成できるサイトが、いつの間にかドメインごと消滅していてとても不便だったので、代わりになるものとして MachineKey を自動的に生成し、Web.config に設定まで行う NuGet パッケージを作りました。

このパッケージをインストールすると、自動的に Web.config に machineKey 要素が追加されます。今のところ AES256 と HMACSHA256 で固定にしています。

MachineKey に関してもう少し知りたいという場合には、下の記事を参考にしてください。


1 つのアプリケーションを複数の仮想マシンで動作させる場合、問題となることが多いです。

例としてパッケージマネージャーコンソールから、MachineKeyGenerator をインストールしてみました。

インストールするとテキストファイルも追加されますが、これは NuGet の仕様でパッケージに content か lib が含まれていないと install.ps1 / uninstall.ps1 が実行されないので、仕方なく入れています。

Web.config を開くと machineKey 要素が追加されているのが確認できます。本当は 1 行で追加されますが、分かりにくいので改行してあります。

パッケージをアンインストールすると、MachineKey も同時に削除するようにしています。*1

既に MachineKey を設定済み、もしくは NuGet パッケージの復元を行った場合には処理をスキップするようにしています。一度設定した MachineKey が変わることはありません。

実際に NuGet パッケージの復元を行いましたが、MachineKey は変わっていません。

PowerShell 力は相変わらずですが、少しコードを整理した後に GitHub にでも置いておこうかと思います。

追記

GitHub にスクリプト一式を置いておきました。build.cmd を叩けば nupkg が生成されます。

*1:この仕様は後で変更する可能性ありそう

ASP.NET MVC の Display Mode を使っている場合には Vary HTTP ヘッダーを出力するべきだった

ASP.NET MVC の Display Mode を使えば、ビューを用意するだけで PC 版とスマートフォン版のページを同じ URL で公開することができます。

既に何回か紹介しているので、Display Mode については以下の記事を参考にしてください。

機能としては上の記事の通りにすれば問題ないですが、Google のドキュメントに動的な配信を行う場合には Vary HTTP ヘッダーを使って、User-Agent で異なるビューが返されることを伝えるべきとありました。

動的な配信の場合、ページをリクエストするユーザー エージェントに応じて、同じ URL で異なる HTML(および CSS)が配信されます。

この設定では、PC 用ユーザー エージェントのクロール時にはモバイル コンテンツが隠されているため、モバイル用ユーザー エージェント向けにサイトの HTML が変更されることはすぐにはわからない状態になっています。サーバーからヒント送信し、スマートフォン用 Googlebot がページをクロールしてモバイル コンテンツを検出するようリクエストすることをおすすめします。このヒントは Vary HTTP ヘッダーを使用して実装します。

モバイルファースト インデックスに関するおすすめの方法 | Google 検索セントラル  |  ドキュメント  |  Google for Developers

適切にヒントをクローラーに与えていない場合には、検出されるまでに時間がかかることがありそうです。

Vary HTTP ヘッダーを出力する

結局のところ Vary HTTP ヘッダーで User-Agent を返せば良いだけなんですが、IIS の圧縮モジュールを使っている場合には簡単にはいきません。モジュールに既知の不具合があるからです。

IIS で gzip 圧縮が有効になっている場合、強制的に Vary HTTP ヘッダーの値が Accept-Encoding に上書きされてしまいます。残念ながら直る気配がありません。

この時は gzip 圧縮をオフにして対応しましたが、それだと効率が悪化するので URL Rewrite を使いました。

<system.webServer>
  <rewrite>
    <outboundRules>
      <rule name="Append Vary" preCondition="IsHTML">
        <match serverVariable="RESPONSE_Vary" pattern="^.*$" />
        <action type="Rewrite" value="{R:0}, User-Agent" />
      </rule>
      <preConditions>
        <preCondition name="IsHTML">
          <add input="{RESPONSE_CONTENT_TYPE}" pattern="^text/html" />
        </preCondition>
      </preConditions>
    </outboundRules>
  </rewrite>
</system.webServer>

URL Rewrite Module はサーバー変数を書き換えると、レスポンスヘッダーにも反映されます。

この定義を Web.config に張り付けるだけで、HTML を返した時だけ Vary HTTP ヘッダーに User-Agent が追加されます。実際にブラウザからアクセスして確認してみました。

Content-Encoding は gzip のままで、Vary には Accept-Encoding と User-Agent が追加されています。

URL Rewrite の Outbound Rule 処理は HTTP 圧縮が行われた後に実行されるので、影響を受けずに HTTP ヘッダーを操作することが出来る、というからくりでした。

おまけ:Vary HTTP ヘッダーが存在しない場合

ついでに圧縮を無効にした場合の挙動も一緒に確認しておきました。

HTTP 圧縮モジュールが Vary を出力しないため、User-Agent のみ出力されています。理想的な挙動です。

ASP.NET MVC 5 アプリケーションの開発を始める時のテンプレート的なものを作った話

昔 ASP.NET MVC 5 で開発を始める上で、自分が定型的に追加している設定をまとめた記事を書きました。

完全に自分用として書いた記事なので、頻繁に読むようになってしまいました。

ぶっちゃけ、自分で MVC 5 アプリケーションを作り始める時に毎回参照しているのも馬鹿らしくなってきたので、GitHub にて色々と設定を追加したテンプレート的な MVC 5 プロジェクトを公開しました。

テンプレートと言いつつ、VSIX 形式にまでは出来ていないので参考にするとかコピペ元にするとか、そういった使い方をしようかと思ってます。

標準のテンプレートとの違い

具体的にこのテンプレートで、どのような設定を追加で行っているかまとめておきます。

  • 最新の ASP.NET MVC 5.2.3 にアップデート
  • RazorViewEngine のみを有効化
  • 認証クッキー名を .ASPXAUTH から auth に変更
  • セッションクッキー名を ASP.NET_SessionId から session に変更
  • CSRF トークンのクッキー名を __RequestVerificationToken から token に変更
  • Attribute Routing のみを使うように
  • URL ルーティングで生成される URL を小文字に
  • レスポンスヘッダに ASP.NET / ASP.NET MVC のバージョン、X-Powered-By を出力しない
  • クライアントサイド検証を無効化
  • エラー画面の表示を IIS 側の設定に統一
  • HTML ヘルパーを少し追加

カスタム HTTP レスポンスヘッダは地味に非表示にしろと言われるので、最初から非表示にしました。

追加した HTML ヘルパー

バリデーションエラーを表示する ValidationSummary ヘルパーの拡張性が全くなく、実際にデザインを当てる場合に不便しかなかったので、部分ビューを使って自由に表示できるようにしています。

ヘルパー名が微妙ですけど、良いのが思いついたらたぶん変更します。

@using (Html.BeginForm())
{
    @Html.ValidationSummaryView("_ErrorView")
    
    @Html.TextBoxFor(m => m.Name)

    <button type="submit">Submit</button>
}

引数にレンダリングする部分ビュー名を指定しておけば、エラーがある場合にはそのビューを使って表示してくれます。エラーがない場合はレンダリング自体を行いません。

エラー表示用のビューはモデルとして ModelError を取ります。

@model IEnumerable<ModelError>

<p>エラーがあります</p>
<ul>
    @foreach (var error in Model)
    {
        <li>@error.ErrorMessage</li>
    }
</ul>

このヘルパーを使ってエラー表示を行った場合には、以下のようになります。

最近行っている開発では、HTML ヘルパー自体をカスタマイズすることが多すぎるので、ある程度の自由度を持たせて楽できるようにしたいですね。

テンプレートは暇なときに更新していきたいと思います。

ASP.NET の HTTP エラー画面の表示処理を IIS 側に統一する

以前に ASP.NET の customErrors と IIS の httpErrors の違いについて書きました。

結局、この時には customErrors と httpErrors の両方を Web.config に追加するという結論に達しましたが、やはり二つを同時に設定するのは手間なので IIS 側に統一してみました。

元々の Web.config はこのように記述していました。これをベースに変更します。

<system.web>
  <customErrors mode="On">
    <error statusCode="404" redirect="/home/error"/>
  </customErrors>
</system.web>
<system.webServer>
  <httpErrors errorMode="Custom">
    <remove statusCode="404" />
    <error statusCode="404" path="/home/error" responseMode="Redirect" />
  </httpErrors>
</system.webServer>

今回は customErrors での処理を行いたくないので、出来るだけ使わないように設定します。

<system.web>
  <customErrors mode="RemoteOnly" />
</system.web>

mode は適当な感じですが、開発中はエラーが見えた方が良いので RemoteOnly にします。ステータスコードによってエラーページのリダイレクトを行う設定は、要素ごと削除しました。

このままだと ASP.NET 側でエラーが発生した場合には IIS の設定がスキップされて、いつものカスタムエラーページが表示されてしまいますが、httpErrors の existingResponse を Replace に設定すると、関係なく IIS の設定に従って画面を表示することが出来ます。

<system.webServer>
  <httpErrors errorMode="Custom" existingResponse="Replace">
    <remove statusCode="404" />
    <error statusCode="404" path="/home/error" responseMode="ExecuteURL" />
  </httpErrors>
</system.webServer>

responseMode は ExecuteURL に設定してあります。この場合にはサーバー側で path に指定した URL を実行してくれるので、リダイレクトすることなくエラー画面を返すことが出来るようになります。

設定の詳細については IIS 公式サイトのリファレンスを参照しておいてください。

Adding HTTP Errors <error> | Microsoft Learn

これでカスタムなエラーページを URL そのままで表示できるようになりました。ASP.NET と IIS で発生した 404 エラーは両方この設定通りに処理が行われます。

しかし、このままだと 200 が返ってしまうので、ステータスコードを 404 に変更してビューを返します。

public class HomeController : Controller
{
    public ActionResult Error()
    {
        Response.StatusCode = 404;

        return View();
    }
}

余談になりますが、responseMode が Redirect を設定している場合には、TrySkipIisCustomErrors を true に設定しないと無限ループになります。

全て設定が終わったので、実際に開発者ツールを使って確認してみました。

URL はアクセスしたそのままで、カスタムエラー画面と 404 が返っていることが確認出来ました。

ASP.NET MVC で HTTP 401 を返した時にログインページへリダイレクトさせない方法

割と有名な ASP.NET のフォーム認証モジュールのおせっかい機能として、HTTP ステータスコードで 401 を返すと自動的にログインページへの 302 リダイレクトに変換するというのがあります。

認証が必要な Web サイトを作る場合には割と便利なんですが、API を MVC でサクッと作りたい場合には厄介です。API としては 401 を返したいのに、勝手にログインページへのリダイレクトになるからです。

f:id:shiba-yan:20150612140747p:plain

ちなみに ASP.NET Web API の Nightly 版でも同じ問題がありましたが、製品版ではアプリケーション設定に特殊なキーを追加することで回避可能になりました。

最初から Web API で作っとけよという感じですが、今回久しぶりにはまったので書きます。

ASP.NET 4.5 から HttpResponse に SuppressFormsAuthenticationRedirect というそのまんまな名前のプロパティが追加されています。

HttpResponse.SuppressFormsAuthenticationRedirect プロパティ (System.Web)

このプロパティを true にすればリダイレクトは回避出来るんですが、このプロパティに値をセットするタイミングで少し悩みました。

最初は OnActionExecuting をオーバーライドして設定すればいいだろと思って試しました。

public class ApiController : Controller
{
    protected override void OnActionExecuting(ActionExecutingContext filterContext)
    {
        Response.SuppressFormsAuthenticationRedirect = true;

        base.OnActionExecuting(filterContext);
    }

    [Authorize]
    public ActionResult Auth()
    {
        return Json(true, JsonRequestBehavior.AllowGet);
    }
}

ところが実際に試すとリダイレクトされたままです。

少し考えると、そもそも Authorize 属性を付けた場合には OnActionExecuting が実行される前に処理が打ち切られてしまうので、そもそも呼び出されないことに気が付きました。

public class ApiController : Controller
{
    protected override void Initialize(System.Web.Routing.RequestContext requestContext)
    {
        requestContext.HttpContext.Response.SuppressFormsAuthenticationRedirect = true;

        base.Initialize(requestContext);
    }

    [Authorize]
    public ActionResult Auth()
    {
        return Json(true, JsonRequestBehavior.AllowGet);
    }
}

必ず呼び出されるメソッドを考えた結果、今回は Initialize をオーバーライドすることにしました。

f:id:shiba-yan:20150612140759p:plain

これで 401 を返してもリダイレクトされることが無くなりました。

なぞなぞ認証を ASP.NET MVC で実装して遊んでみた

特に意味はないのですが、はてなの各サービスで使えるようになっているなぞなぞ認証を、ASP.NET MVC の機能を使って実装してみました。なぞなぞ認証の詳細ははてなキーワードを見てください。

単純になぞなぞに回答して、それが正解であればページを表示できる機能です。最近は新機能の紹介的なことばかりやってきて疲れてきたので、たまにはこんな実装をしてみるのもいいでしょう。

ASP.NET MVC で実装するので MVC 5 で追加された IAuthenticationFilter を使ってみようと思ったのですが、なぞなぞ認証の場合はユーザーの特定は出来ないので ActionFilter として素直に実装しました。

実装して試した結果

今回はまず実装したらどのような挙動になるのかを紹介しておきます。実際にルートに対してなぞなぞ認証を有効にした場合、専用のフォームが表示されるようになります。

この答えは Azure を使っている方なら誰でも分かると思います。当然ながら答えは kosmosebi ですね。

正しい答えを入力して送信ボタンを押すと、実際のページが表示されるようになります。

これがなぞなぞ認証の大雑把な流れになります。

実際には一度正解した場合には再度入力することの無いように、クッキーに何らかの情報を入れておくべきだと思ったので、適当にクッキーに保存するような処理を実装しました。

コードの解説など

なぞなぞ認証は ActionFilter として実装するので、継承した RiddleAuthenticationAttribute というクラスを作成しました。実装としては簡単で、問題文と答えをコンストラクタで受け取って、ユーザーから答えが入力された場合にはチェックしているだけです。

初回アクセスや、回答が間違っていた場合にはフォームを強制的に表示するようにしています。

public class RiddleAuthenticationAttribute : ActionFilterAttribute
{
    public RiddleAuthenticationAttribute(string question, string answer)
    {
        _question = question;
        _answer = answer;

        var hash = SHA1.Create().ComputeHash(Encoding.UTF8.GetBytes(_question + ":" + _answer));

        _riddleHash = BitConverter.ToString(hash).Replace("-", "").ToLower();
    }

    private readonly string _question;
    private readonly string _answer;

    private readonly string _riddleHash;

    private const char Separator = '|';
    private const string RiddleCookieName = "riddle";

    private const string DefaultAnswerName = "riddle-answer";
    private const string DefaultViewName = "_RiddleForm";

    public override void OnActionExecuting(ActionExecutingContext filterContext)
    {
        // 既に回答済みか確認
        var cookie = filterContext.HttpContext.Request.Cookies[RiddleCookieName] ?? new HttpCookie(RiddleCookieName);

        var values = (cookie.Value ?? "").Split(new[] { Separator }, StringSplitOptions.RemoveEmptyEntries);

        if (values.Contains(_riddleHash))
        {
            return;
        }

        // 回答が入力されたか確認
        var postAnswer = filterContext.HttpContext.Request.Form[DefaultAnswerName];

        if (postAnswer == _answer)
        {
            cookie.Value = string.Join(Separator.ToString(), values.Concat(new[] { _riddleHash }));

            filterContext.HttpContext.Response.SetCookie(cookie);

            return;
        }

        // なぞなぞ認証フォームを表示
        filterContext.Result = new ViewResult
        {
            ViewData = new ViewDataDictionary
            {
                { "Question", _question }
            },
            ViewName = DefaultViewName
        };
    }
}

多少、フォームの項目は決め打ちが多いですが、眠たいのと遊びなのでこのように実装しました。

この属性を使った例は以下のようになります。単純にアクションへ追加するだけです。

public class HomeController : Controller
{
    [RiddleAuthentication("Azure 界の抱かれたい男 No.1 とは?", "kosmosebi")]
    public ActionResult Index()
    {
        return View();
    }
}

ちなみになぞなぞ認証のフォームは以下のような形です。特にいうこともないシンプルなフォームです。

<h2>なぞなぞ認証</h2>

@using (Html.BeginForm())
{
    <div class="form-group">
        <label>@ViewBag.Question</label>
        <input type="text" name="riddle-answer" class="form-control" />
    </div>
    <button type="submit" class="btn btn-primary">送信</button>
}

実際に使うかと言われると微妙ですが、ActionFilter を使う例としては分かりやすいサンプルになったのではないかと思います。IAuthenticationFilter は Basic 認証の例をどこかでアップしておきたいと思います。