概要
ASP.NET MVC アプリケーションが起動した際に最初に実行されるメソッド Application_Start の役割と、そこで行う主な処理についてまとめてみました。
目次
Webアプリケーションプロジェクトについて
Webアプリケーションプロジェクトは、Webサーバー上で動作するアプリケーションを作成するためのプロジェクトのことです。ビルドして出力されるのはクラスライブラリと同様にDLLですが、IIS のワーカープロセス(w3wp.exe)から読み込まれて実行されます(コンソールアプリのように exe で自己ホストはしません)。また、通常w3wp.exeはWebアプリケーションを実行するサーバーに存在します。
クラスライブラリは再利用可能なロジックをまとめた DLL を作るのがメインで、UIやWebページは持ちません。逆にWebアプリケーションはHTMLを返したりルーティングの処理や、HTTPリクエストを受ける仕組みを持ちます。また、Web.configというWebアプリケーションプロジェクトの設定を構成するxmlファイルもあるのがWebアプリケーションプロジェクトの特徴です。
Webアプリケーションプロジェクトはアプリケーションの「実行主体」になるので、他のライブラリを参照して機能を利用する側になり、他のライブラリから参照される側になることはほとんどありません。
コード
以下のように、WebアプリケーションプロジェクトのSystem.Web.HttpApplication を継承したクラスの Application_Start がアプリケーションが起動したときに最初に呼ばれるメソッドになります。
今回は以下のように、この中で行っているMVCのルーティングやバンドル、フィルタなどの設定について調べました。
using System.Web.Helpers;
using System.Web.Mvc;
using System.Web.Optimization;
using System.Web.Routing;
namespace WebApplication
{
public class MvcApplication : System.Web.HttpApplication
{
protected void Application_Start()
{
MvcHandler.DisableMvcResponseHeader = true;
AntiForgeryConfig.SuppressXFrameOptionsHeader = true;
AreaRegistration.RegisterAllAreas();
FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);
RouteConfig.RegisterRoutes(RouteTable.Routes);
BundleConfig.RegisterBundles(BundleTable.Bundles);
}
}
}
MvcHandler.DisableMvcResponseHeader = true;
レスポンスヘッダの一つである X-AspNetMvc-Version を消去できます。
このヘッダにはMVCのバージョン (例:5.2) が入っていますが、falseに設定すると攻撃者に「このサイトは ASP.NET MVC 5.2 を使っている」と教えてしまう形になってしまいます。なのでtrueにすることでセキュリティ的に余計な情報を減らして攻撃のヒントを減らしています。
デフォルトはfalseです。
X-AspNetMvc-Versionの確認方法
- 該当のWebページを開く
- F12 キーで開発者ツールを起動
- Network(ネットワーク)タブを選択
- ページを再読み込み
- 一覧から Type が Document の行をクリック
- Response Headersを確認
- X-AspNetMvc-Version: 5.2 のような行があればX-AspNetMvc-Versionが付いている(なければ付いていない)
レスポンスヘッダとは、HTTP通信でサーバーからクライアント(ブラウザなど)に返される「追加情報」のことです。
HTTPレスポンスは以下の3つの要素で構成されますが、このうちの一つにレスポンスヘッダがあります。
- ステータスライン(例:HTTP/1.1 200 OK)
- レスポンスヘッダ(メタ情報)
- ボディ(HTMLやCSS、JSなどの実データ)
レスポンスヘッダには X-AspNetMvc-Version や X-Powered-By などなくてもWebページの表示には影響がないものもありますが、 Content-Type や Content-Length など、ないと表示が崩れたり問題につながるものもあります。
また、他のHTTPレスポンスであるステータスラインやボディも必須の情報になります。ステータスラインが無いとHTTPプロトコルとして不正なので、ブラウザはレスポンスを解釈できず画面は表示されません(通信エラー)。同じくボディもないと、HTMLがないので画面に何も表示されなくなります。
AntiForgeryConfig.SuppressXFrameOptionsHeader = true;
レスポンスヘッダのひとつ、セキュリティヘッダ X-Frame-Options の追加を抑止する設定です。web.config などで明示的に設定している場合に二重に付くのを防ぐ目的があります。デフォルトはfalseです。
X-Frame-Optionsの確認方法
- 該当のWebページを開く
- F12 キーで開発者ツールを起動
- Network(ネットワーク)タブを選択
- ページを再読み込み
- 一覧から Type が Document の行をクリック
- Response Headersを確認
- X-Frame-Options: DENY のような行があればX-Frame-Optionsが付いている(なければ付いていない)
HTTPレスポンスに同じヘッダが複数回出ると、ブラウザやセキュリティツールが解釈に迷ってしまいます。そのため、trueに設定して、代わりにweb.configなどで設定するという定義です。
<httpProtocol>
<customHeaders>
<add name="X-Frame-Options" value="DENY" />
</customHeaders>
</httpProtocol>
<customHeaders>はレスポンスヘッダをカスタマイズするためのWeb.configの仕組みです。
DENY は iframe を使用しての埋め込みを完全に禁止する設定で、セキュリティ的には最も強力なオプションです。
iframeとは、HTMLタグ<iframe>を使用して、別のWebページを自分のページの一部として表示できる機能です。広告バナーやYouTube動画の埋め込みなど、様々なものを埋め込みできます。
この定義は他のWebページがあなたのページを<iframe>で埋め込むことを禁止する定義で、自分のページ内で<iframe>を使って他サイトを表示することは制限していません。
攻撃者のWebページに埋め込まれることで、クリックジャッキング攻撃のリスクがあります。攻撃者のWebページに埋め込まれるだけで「攻撃に加担している」ことにはならないですが、埋め込まれたページは「攻撃の舞台」として利用される形になるので、埋め込まれた側のサイトの信頼性も損なわれてしまいます。
AreaRegistration.RegisterAllAreas();
「エリア」機能を初期化するためのメソッドです。
エリアとは、大規模なアプリで機能ごとにコントローラやビューを整理、分割するために使います。
例えば、/Admin と /User で画面やコントローラを分けたい場合、
Areas/Admin/Controllers/...
Areas/User/Controllers/...
のようにフォルダ構成を分けて管理できます。
RegisterAllAreas()のメソッドを呼ぶと、アプリ起動時に、Areas フォルダをスキャンして、各エリアの AreaRegistration クラスを見つけ、そのルートを登録します。これを呼ばないと、/Admin/Home/Index のような エリア付きURLが404になります。逆にエリアを使わないアプリなら影響はありません。
ASP.NET MVC の エリアはAreaRegistration を継承したクラスを作成し、その中で RegisterArea() を実装することで有効になります。AreaRegistration.RegisterAllAreas(); は、アプリ起動時に アセンブリ内のすべての AreaRegistration 派生クラスを探して登録しています。なので、AreaRegistration を継承しているクラスの有無でそのプロジェクトがエリアを使用しているかどうかが判断できます。
FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);
RegisterGlobalFilters() の中は以下のようになっているとします。
using System.Web;
using System.Web.Mvc;
namespace WebApplication
{
public class FilterConfig
{
public static void RegisterGlobalFilters(GlobalFilterCollection filters)
{
filters.Add(new HandleErrorAttribute());
}
}
}
これはグローバルフィルタを登録する処理で、アプリ全体に共通で適用するフィルタを登録します。フィルタとは、コントローラやアクションの前後で共通の処理を追加できる仕組みです。グローバルフィルタに登録すると、
- エラーが起きたときに共通の画面を表示する
- 全ページでログを取る
- 認証やアクセス制御をする
のような「どの画面でも必要な処理」をすべてのコントローラ・アクションに自動適用することができます。
ここで登録しているHandleErrorAttribute は、MVC標準のエラーハンドリングフィルタで、コントローラやアクションで未処理の例外が発生したときにキャッチを行います。グローバルに登録することで、全アクションで例外が発生した場合に共通のエラーページを表示できるイメージです。
HandleErrorAttribute は、未処理例外が発生したときに 既定で Views/Shared/Error.cshtml を探して表示しする挙動になっています。
ちなみに、ASP.NET MVC には以下の4種類のフィルタがあります。
- 認証フィルタ(Authentication):ユーザーが誰かを確認
- 認可フィルタ(Authorization):アクセス権限を確認([Authorize])
- アクションフィルタ(Action):アクション実行の前後で処理(ログ、計測など)
- 例外フィルタ(Exception):エラー発生時に処理(HandleErrorAttribute)
RouteConfig.RegisterRoutes(RouteTable.Routes);
これは「URLとコントローラ/アクションの対応ルール(ルーティング)」を登録する処理です。MVCでは、URLを見て「どのコントローラのどのアクションを呼ぶか」を決める必要があります(ルーティング)。そのルールを RouteTable.Routes に登録するのが RegisterRoutesです。
RouteConfig.RegisterRoutes(RouteTable.Routes);は以下のようになっているとします。
using System.Web.Mvc;
using System.Web.Routing;
namespace WebApplication
{
public class RouteConfig
{
public static void RegisterRoutes(RouteCollection routes)
{
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
routes.MapRoute(
name: "Default",
url: "{controller}/{action}/{id}",
defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional }
);
}
}
}
これは、「URL を見て、どの Controller / Action を呼ぶか」を決めるルール(ルーティング)を登録しています。
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
WebResource.axd や ScriptResource.axd、trace.axd などの .axd リソースへのリクエストを MVC のルーティング対象から外しています(IIS/ASP.NET の既定ハンドラに処理を任せる)。これがないと、/trace.axd のような URL が 既定ルートに誤まってマッチし、ErrorController などにいって 404 になったり、余計な処理コストがかかることがあります。
.axdファイルとは、ASP.NET が内部で使う特殊なハンドラ(WebResource.axd、ScriptResource.axdなど)のことで、埋め込みリソース(JavaScriptや画像)を配信するために使われます。
{*pathInfo}はCatch-Allパラメータといい、* が付くことで、「残り全部」を1つの文字列として受け取ります。
ここでは「.axd の後ろに何があっても無視する」という意味になります。
URL /WebResource.axd/foo/bar/baz → {pathInfo} = foo/bar/baz
routes.IgnoreRoute("{resource}.axd/{*pathInfo}") は「.axd がいつ・どこで使われても MVC が邪魔しないようにするための安全策」のようなイメージです。
ASP.NET MVCのリクエスト処理はざっくりこうなります。
[ブラウザ] → IIS → (ASP.NET Modules: Routing など) → ハンドラの選定 → 実行
MVC では、UrlRoutingModule というモジュールがあり、このモジュールが受け取ったリクエスト URL を ルートテーブル と突き合わせます。例えば「/Home/Index」なら {controller}/{action}/{id} という既定ルートに当てはめて、HomeController.Index() を呼び出す、という流れです。
また、既存の物理ファイルは通常ルーティングの対象外になっているのですが、.axd で終わるリクエストは 物理ファイルではなく専用ハンドラで処理される仮想URL です。そのため「既存ファイル」と判定されず、ルーティングまで回ってきてしまいます。
例えば/trace.axd は既定ルート "{controller}/{action}/{id}" に マッチしてしまうことがあります(controller = "trace.axd", action = "Index" などに解釈されうる)。
当然「trace.axd というコントローラ」は存在しないので、MVC 側で 404 になり、本来の trace.axd ハンドラに処理が渡らず トレースページが開けない、といった事態になります。
上記の解消方法としてIgnoreRoute を入れておくと、そのパターンに合致した URL は StopRoutingHandler で処理が打ち切られ、正しく .axd 専用のハンドラ(TraceHandler や WebResourceHandler など)に処理が流れるようになります。
routes.MapRoute(...)
MapRoute は、「URL パターン(ルート)」を RouteTable.Routes に登録するためのメソッドです。これにより、特定の URL から コントローラ/アクション/パラメータ の対応付けルールをアプリに追加できます。
例えば以下のようなURLの場合、
http://example.com/Products/Details/5
ドメイン部分がhttp://example.com(IIS やホスティング環境が担当する領域)
パス部分がProducts/Details/5になります。このパス部分を MVC の ルーティングが解析します。
ルートを定義することで、「このURLパターンにマッチしたら、このコントローラのこのアクションメソッドを呼ぶ」という対応付けができます。
ここでは"{controller}/{action}/{id}" という形の URL を Controller / Action / id に割り当てています。
また、defaults で 未指定時の既定値を設定しています(controller="Home", action="Index", id は任意)。例えば http://example.com のようにパスが空の場合、defaults の値のルートが適用されます。
{controller} や {action} は 既定の MVC ルートハンドラが参照するキー名になります。Controller は、クラス名の末尾が Controller の型を探します(例:ProductsController)。
コントローラクラスは、一般的には Controller という名前のクラスを継承しています。
Action は「パブリックなインスタンス メソッド」で、[NonAction] 属性が付いていないものが アクション メソッドとして認識されます。また、戻り値は ActionResult(またはその派生・Task)が推奨ですが、string など他の型でも動きます。また、HTTP メソッド属性([HttpGet], [HttpPost] など)が付いていれば、その条件に一致する場合のみ候補になります。
BundleConfig.RegisterBundles(BundleTable.Bundles);
バンドルとは、複数の JavaScript や CSS ファイルを「まとめて1つのリクエストで配信する仕組み」のことです。さらに、圧縮(minify)してファイルサイズを小さくします。
メソッドの中は以下のようになっているとします。
using System.Web.Optimization;
namespace WebApplication
{
public class BundleConfig
{
public static void RegisterBundles(BundleCollection bundles)
{
bundles.Add(new ScriptBundle("~/bundles/jquery").Include(
"~/Scripts/jquery-{version}.js"));
bundles.Add(new ScriptBundle("~/bundles/jqueryval").Include(
"~/Scripts/jquery.validate*"));
bundles.Add(new ScriptBundle("~/bundles/modernizr").Include(
"~/Scripts/modernizr-*"));
bundles.Add(new Bundle("~/bundles/bootstrap").Include(
"~/Scripts/bootstrap.js"));
bundles.Add(new StyleBundle("~/Content/css").Include(
"~/Content/bootstrap.css",
"~/Content/site.css"));
}
}
}
例えば、Webページは、JS や CSS をたくさん読み込みます。
<!-- JavaScript -->
<script src="/Scripts/jquery.js"></script>
<script src="/Scripts/bootstrap.js"></script>
<script src="/Scripts/site.js"></script>
<!-- CSS -->
<link rel="stylesheet" href="/Content/bootstrap.css">
<link rel="stylesheet" href="/Content/site.css">
上記の場合、 HTTPリクエストが5回発生します。HTTPリクエストは数が多いほど遅くなるので、これをまとめたい場合にバンドルを使用します。
ここで、上記の複数ファイルを 1つの論理パスにまとめます。
@Scripts.Render("~/bundles/jquery")
@Scripts.Render("~/bundles/bootstrap")
@Styles.Render("~/Content/css")
これをすることで、1つの圧縮済みファイルとして配信されます。
-
バンドル使用前:3つのJS + 2つのCSS → 5回リクエスト
-
バンドル使用後:2つのJSバンドル + 1つのCSSバンドル → 3回リクエスト
JS と CSS を同じバンドルに「まとめる」ことはできません。
バンドルを使用するには、以下のように「どの論理パスに、どのファイルをまとめるか」を定義して、
bundles.Add(new ScriptBundle("~/bundles/jquery").Include(
"~/Scripts/jquery-{version}.js"));
cshtmlでどのバンドルを読み込むか」を指定する必要があります。
@Scripts.Render("~/bundles/jquery")
BundleConfig は「まとめ方の定義」、.cshtml は「まとめたものを使う場所」のイメージです。
バンドルを呼び出すには、@Scripts.Render や @Styles.Render という Razor ヘルパーを使います。これは サーバーサイドで実行されるコードなので、.cshtml(Razorビュー)でしか使えません。そのため、htmlでは直接バンドルは使用できません。
※バンドルの出力URL(例:/bundles/jquery?v=abc123)を <script> タグで書けば、HTML でも読み込めますが、この URL は ハッシュ値が変わるので、手書き管理は現実的ではありません。
用語
コントローラ
MVCパターン(Model-View-Controller)のCにあたります。MVCパターンではそれぞれ以下のような役割があります。
- Model:データやビジネスロジック
- View:画面表示
- Controller:両者をつなぐ「制御役」
上記の中で Controller とは、ユーザーのリクエストを受け取り、必要な処理をしてレスポンスを返す役割を持っています。「リクエストとレスポンスの司令塔」のようなイメージで、URLを見てどのControllerのどのアクションを呼ぶかを決定します。MVCパターンだとViewは直接Modelにアクセスしないのが原則なので、レスポンスもリクエストも一度Controllerを介します。
以下がコントローラクラスのサンプルです。
public class ProductsController : Controller
{
// 標準的:ActionResult を返す
public ActionResult Details(int id)
{
var product = repository.Get(id);
if (product == null)
{
return HttpNotFound();
}
return View(product);
}
// 文字列も可能(ContentResult として出力)
public string Ping()
{
return "OK";
}
// 非同期アクション
public async Task<ActionResult> Index()
{
var list = await repository.GetAllAsync();
return View(list);
}
// 別名で公開(/Products/Info/5 で Details を呼ぶ)
[ActionName("Info")]
public ActionResult DetailsAlias(int id)
{
return Details(id);
}
// アクションではない([NonAction]属性を付与)
[NonAction]
public void Helper()
{
// ヘルパー処理
}
}
ルーティング
「受け取ったURLを解析して、どのコントローラとアクションに処理を渡すかを決定する仕組み」です。
例えば以下のようなURLがあったとします。
URL:http://example.com/Products/Details/5
既定ルート "{controller}/{action}/{id}" に当てはめると:
- controller = Products
- action = Details
- id = 5
この情報を使って、ProductsController.Details(5) が呼ばれます。
既定ルートとは
ASP.NET MVC で最初から設定されている「URLパターンとコントローラ・アクションの対応ルール」です。
テンプレートでは RouteConfig.RegisterRoutes に次のように書かれています。
routes.MapRoute(
name: "Default",
url: "{controller}/{action}/{id}",
defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional }
);
ProductsController.Details(5) とは
これは MVC のルーティングによって呼び出されるメソッドを指しています。ProductsController というコントローラクラスにDetailsというメソッドがあり、引数として5を渡しているイメージです。
ルートテーブル
アプリ全体で使用する「ルーティング規則(ルート)」の一覧です。
型は RouteCollection で、アプリからは RouteTable.Routes で参照します。
通常はアプリ起動時(Application_Start)に、RouteConfig.RegisterRoutes(RouteTable.Routes) で 規則を追加します。
追加した順に評価され、最初にマッチしたルートが採用されます。なので追加順が重要なポイントになってきます。
終わりに
基本的なことを身に着けておくとしっかりした素地ができ、今後の理解や応用にもつながると思いました。
