しばやん雑記

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

開発環境でも ASP.NET Core MVC の RequireHttps 属性を使えるようにする

ASP.NET MVC の RequireHttps 属性を使うと、HTTP で来た場合には HTTPS にリダイレクトしてくれるのですが、開発環境で 443 以外のポートを使っている場合、正しく動作しないという欠点がありました。

[RequireHttps]
public ActionResult Index()
{
    return View();
}

本番環境などの 80 / 443 で動作している場合には正しく動作しますが、IIS Express ではポートが動的に割り当てられるので URL のスキーマを変えるだけだとアクセス出来なくなります。

aspnetwebstack/RequireHttpsAttribute.cs at master · ASP-NET-MVC/aspnetwebstack · GitHub

実装を見てもスキーマを HTTPS に変えているだけなので、MVC 5 までは対応方法が 80 / 443 で動かすしかありませんでしたが、ASP.NET Core MVC ではオプションで SSL のポートを渡せるようになりました。

Core MVC のオプションは AddMvc の引数に指定したデリゲートの中で行います。

public void ConfigureServices(IServiceCollection services)
{
    // Add framework services.
    services.AddMvc(options =>
    {
        options.SslPort = 44357;
    });
}

SslPort というプロパティが用意されているので、開発環境で使用する SSL のポートを指定します。

ちなみに ASP.NET Core アプリケーションで SSL を有効にするには、アプリケーションのプロパティを開いて Web サーバーの設定から行います。IIS Express の場合のみ設定可能です。

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

これで終わればいいのですが、ConfigureService の中に SSL のポート番号を直接書くのは、開発環境以外で問題になります。そして ConfigureService では IHostingEnvironment を受け取れないので、環境ごとに設定を切り替えることはこのままでは無理です。

方法はいくつかあると思いますが、分かりやすそうな 2 つの方法を紹介しておきます。

appsettings.Development.json に定義して読み込む

SSL のポート番号は基本的に開発環境でしか指定することはないと思うので、appsettings.Development.json を追加してその中にポート番号を追加しておきます。

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

開発環境のみでしか読み込まれないファイルなので、他の環境には影響しません。

そして ConfigureServices では Configuration を使ってポート番号を読み込み、SslPort に設定します。

public void ConfigureServices(IServiceCollection services)
{
    // Add framework services.
    services.AddMvc(options =>
    {
        options.SslPort = Configuration.GetValue<int?>("SslPort", null);
    });
}

キーが存在しない場合には null が返されるので、デフォルトの 443 が使われます。

launchSettings.json を直接読み込む

Visual Studio で設定した Web サーバー周りの設定は Properties 以下に保存されている launchSettings.json に書き込まれているので、このファイルを読み込んでしまえば、SSL のポート番号を直接取得できます。

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

開発環境のみで読み込む必要があるので、IsDevelopment を使って環境を判別する必要があります。

public Startup(IHostingEnvironment env)
{
    var builder = new ConfigurationBuilder()
        .SetBasePath(env.ContentRootPath)
        .AddJsonFile("appsettings.json", optional: true, reloadOnChange: true)
        .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true)
        .AddEnvironmentVariables();

    if (env.IsDevelopment())
    {
        builder.AddJsonFile("Properties/launchSettings.json", optional: true);
    }

    Configuration = builder.Build();
}

public void ConfigureServices(IServiceCollection services)
{
    // Add framework services.
    services.AddMvc(options =>
    {
        options.SslPort = Configuration.GetValue<int?>("iisSettings:iisExpress:sslPort", null);
    });
}

読み込んでしまえば、あとは同じように SslPort に設定するだけです。GetSection や GetValue は : を使うとネストされていても 1 回で読み込めるので結構便利です。

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

リダイレクトを確認してみると、SSL のポート番号がちゃんと反映されていることが確認できます。

そして今回の話とは関係ないですが、MVC 5 の RequireHttps には GET 以外で来た場合に例外を投げるので 500 エラーになるという、非常にいやらしい問題もあります。

ASP.NET Core MVC では、この問題も例外を投げるのではなく 403 を返すことで対応されました。

Mvc/RequireHttpsAttribute.cs at dev · aspnet/Mvc · GitHub

全体的に Core MVC では MVC 5 までの問題のある挙動が、大幅に改善されているように感じます。存在しないルーティングで 500 を返すのではなく、404 を返すのもその一つです。

とりあえず意図しない入力があった場合に、無条件で例外を投げていた MVC 5 が悪いだけですが。