アレがアレでアレ

(できれば)プログラミング関係のことを書きたい

ASP.NET Core DIのライフタイムの種類

ASP.NET CoreのDIで設定できるライフタイムについてのメモです。
DIについては特に説明しません。

インスタンスのライフタイム

docs.microsoft.com

公式ドキュメントにはService lifetimesと書かれていますが、注入するインスタンスのライフタイムは以下の3つから選択することができます。

  • Transient
  • Scoped
  • Singleton

Transient

Transientは指定したクラスのインスタンスが要求される度に毎回新しく生成します。
インスタンスはリクエストの終了時に破棄されます。

AddTransient拡張メソッドで設定します。

Scoped

Scopedリクエストごとインスタンスを生成します。
Transientと違い、1リクエスト内では同じオブジェクトを使い回すようです。

Entity FrameworkのDbContextはデフォルトでscopedとなっているみたいですね。
インスタンスが破棄されるタイミングはTransientと同じです。

AddScoped拡張メソッドで設定します。

Singleton

Singletonは最初のリクエスト時にインスタンスを生成し、それ以降はそのインスタンスをずっと使いまわします。
アプリが終了するまでインスタンスは破棄されないまま残り続けます。

AddSingleton拡張メソッドで設定します。

実際に確認してみる

それぞれのライフタイム設定の違いを確認してみます。

確認用に以下のコードを用意しました。

public interface IMyDependency
{
    Guid Guid { get; }
}

public class MyDependency : IMyDependency
{
    private readonly Guid _guid;

    public MyDependency()
    {
        _guid = Guid.NewGuid();
    }

    public Guid Guid => _guid;
}

public class IndexModel : PageModel
{
    private readonly IMyDependency _myDependency;

    public Guid Guid { get; set; }

    public IndexModel(IMyDependency myDependency)
    {
        _myDependency = myDependency;
    }

    public void OnGet()
    {
        Guid = _myDependency.Guid;
    }
}

IndexModelがGuidを持つMyDependencyクラスに依存している状態です。

IndexModelMyDependencyが注入されるように設定します。
まずはSingletonから確認します。

public void ConfigureServices(IServiceCollection services)
{
    services.AddSingleton<IMyDependency, MyDependency>(); // new
    services.AddRazorPages();
}

この状態でアプリを起動して、IndexModelのGuidを表示してみます。

f:id:eiken7kyuu:20201228132221p:plain

上記のような文字列が出てくると思いますが、何度リロードしても同じ値が表示されているかと思います。アプリを再起動しないと値が変化しません。
ちゃんとSingletonになっていますね。

次はTransientとScopedを比較してみます。

比較しやすいようにコードを修正します。

public interface IMyService
{
    Guid Guid { get; }
}

public class MyService : IMyService
{
    private readonly IMyDependency _myDependency;

    public MyService(IMyDependency myDependency)
    {
        _myDependency = myDependency;
    }

    public Guid Guid => _myDependency.Guid;
}

public class IndexModel : PageModel
{
    private readonly IMyService _myService1;
    private readonly IMyService _myService2;

    public Guid Guid1 { get; set; }
    public Guid Guid2 { get; set; }

    public IndexModel(IMyService myService1, IMyService myService2)
    {
        _myService1 = myService1;
        _myService2 = myService2;
    }

    public void OnGet()
    {
        Guid1 = _myService1.Guid;
        Guid2 = _myService2.Guid;
    }
}

新たにMyServiceというクラスを追加し、依存関係を以下のようにしました。

IndexModel -> IMyService -> IMyDependency

この状態でMyDependencyをTransinetで指定した場合とScopedで指定した場合のGuid1, Guid2の違いを見てみます。

まずはTransinet

public void ConfigureServices(IServiceCollection services)
{
    services.AddTransient<IMyDependency, MyDependency>(); // new
    services.AddTransient<IMyService, MyService>(); // new
    services.AddRazorPages();
}

IndexModelのGuid1とGuid2はそれぞれ別の値が表示されると思います。

f:id:eiken7kyuu:20201228133453p:plain

MyDependencyにTransientを指定しているのでmyService1myService2が持つMyDependencyは別のインスタンスであると確認できました。

今度はMyDependencyにScopedを指定してみます。

public void ConfigureServices(IServiceCollection services)
{
    services.AddScoped<IMyDependency, MyDependency>(); // new
    services.AddTransient<IMyService, MyService>();
    services.AddRazorPages();
}

Guid1と2の表示結果 f:id:eiken7kyuu:20201228133918p:plain

どちらも同じ値になりました。
Scopedはリクエスト毎にインスタンスを生成し、リクエスト内で使い回すため、
myService1myService2は同じMyDependencyインスタンスを参照しているってことになりますね。ちゃんとScopedが機能していました!

ざっくりまとめ

ドキュメントだけではTransientとScopedの違いがよくわかりませんでしたが、実際に動作確認することで違いが理解できました。

C#で2分探索木

応用情報の午後問題を解いていたら2分探索木の問題が出てきて実際にC#で書いてみようと思いました。
ノードの検索と挿入は簡単でしたが、削除はちょっと複雑になるんですね。

実装

ノード

namespace BinarySearchTree
{
    public class Node
    {
        public int Value { get; set; }
        public Node Left { get; set; }
        public Node Right { get; set; }

        public Node(int value)
        {
            Value = value;
        }
    }
}

2分探索木

using System;
using System.Collections.Generic;

namespace BinarySearchTree
{
    public class BinarySearchTree
    {
        private Node _rootNode;

        public BinarySearchTree() { }

        public BinarySearchTree(IEnumerable<int> numbers)
        {
            foreach (var num in numbers)
            {
                Insert(num);
            }
        }

        public void Insert(int value)
        {
            _rootNode = InsertRec(value, _rootNode);
        }

        private Node InsertRec(int value, Node node)
        {
            if (node == null) return new Node(value);

            if (node.Value > value)
            {
                node.Left = InsertRec(value, node.Left);
            }
            else if (node.Value < value)
            {
                node.Right = InsertRec(value, node.Right);
            }

            return node;
        }

        public bool Search(int value) => SearchRec(value, _rootNode);

        private bool SearchRec(int value, Node node) => node switch
        {
            null => false,
            Node n when n.Value == value => true,
            Node n when n.Value > value => SearchRec(value, n.Left),
            Node n when n.Value < value => SearchRec(value, n.Right),
            _ => throw new Exception("Search Error")
        };

        public void Remove(int value)
        {
            _rootNode = RemoveRec(value, _rootNode);
        }

        private Node RemoveRec(int value, Node node)
        {
            if (node.Value == value)
            {
                // 削除対象ノードが子を2つ持つ場合
                // 1. 削除対象ノードの右の子から最小値を取得する
                // 2. 1で取得したノードを削除対象ノードと置き換える。その後、削除対象ノードを削除する。
                //    このとき置き換えたノードの右ノードを1で取得したノードの位置に置く
                Func<Node, Node> twoChildren = node =>
                {
                    var rightMinNode = MinNode(node.Right);
                    node.Value = rightMinNode.Value;
                    node.Right = RemoveRec(rightMinNode.Value, node.Right);
                    return node;
                };

                return (node.Left, node.Right) switch
                {
                    // 削除対象ノードの子が1つだけ: 削除対象ノードを子ノードと置き換える
                    ({ }, null) => node.Left,
                    (null, { }) => node.Right,
                    ({ }, { }) => twoChildren(node), // 削除対象ノードが子を2つ持つ
                    (null, null) => null,            // 削除対象ノードが子を持たない: nullを返す(そのまま削除)
                };
            }

            if (node == null) return node;
            if (node.Value > value)
            {
                node.Left = RemoveRec(value, node.Left);
            }
            else if (node.Value < value)
            {
                node.Right = RemoveRec(value, node.Right);
            }

            return node;
        }

        private Node MinNode(Node node)
        {
            return node.Left == null
                ? node
                : MinNode(node.Left);
        }
    }
}

リポジトリ github.com

参考

依存関係逆転の原則

SOLID原則の1つである、依存関係逆転の原則について簡単にまとめたものです。

※依存性の注入に関してここでは解説しません。

依存関係逆転の原則とは

定義

  1. 上位のモジュールは下位のモジュールに依存してはならない。どちらのモジュールも「抽象」に依存すべきである。
  2. 「抽象」は実装の詳細に依存してはならない。実装の詳細が「抽象」に依存すべきである。

引用元: Robert C. Martin. アジャイルソフトウェア開発の奥義

これだけだとちょっとなにを言ってるのかよくわかりませんね。
簡単に言うと、変更されやすく、脆いモジュールへの依存しているものを、変更されにくいものへ依存するようにしなさいという原則です。

変更されやすいものに依存している例

本を扱うシステムがあり、その中に以下のようなモジュールがあるとします。

f:id:eiken7kyuu:20200811170007p:plain

これは、本のタイトルを入力すると本検索APIを呼び出し、本情報を取得、結果をユーザーに返すモジュールです。
上位レベルServiceレイヤ内のBookServiceは検索結果を取得するために、下位レベルInfrastructureレイヤにあるBookSearchApiクラスを利用しています。

システムはこのように制御の流れ依存の流れが同じになってしまいがちです。
アプリケーションの方針を決めている上位レベルのモジュールが、実装の詳細を担当している下位レベルのモジュールに依存してしまっています。

上位レベルと比べて下位レベルのモジュールは脆く、変更されやすいものです。
この依存によって、下位レベルのモジュールの変更が上位レベルのモジュールにまで影響するようになっています。

そこで依存関係逆転の原則を使って、下位レベルのモジュールを上位レベルのモジュールに依存させ、依存関係を逆転させます。

依存を逆転させたあと

依存関係を逆転させた後の図が以下になります。

f:id:eiken7kyuu:20200811172843p:plain

ServiceレイヤでIBookSearchApiインターフェースを宣言し、InfrastructureレイヤのBookSearchApiはこのインターフェースを実装するようにします。

こうすることでBookServiceは同じレイヤにある「抽象」を通してBookSearchApiを利用するようになり、上位モジュールが下位モジュールに依存しなくなりました。
逆に、上位の「抽象」に下位レベルの実装の詳細が依存していることになります。
つまり、下位レベルを変更しても上位レベルがその影響を受けなくなります。

実装の詳細に依存せず、抽象に依存する。これが依存関係逆転の原則です。

所有権も逆転

クライアント側がインターフェースを所有しているので「依存」だけでなく「所有権」も逆転しています。
Infrastructureレイヤの影響を受けないだけでなく、IBookSearchApiを実装するものであれば何でも再利用できるようになっています。

例えば元々、楽天ブックス書籍検索APIを使って実装していたものをGoogle Books APIsへ変更することも楽になりました。

標準ライブラリとかはどうするの?

実装の詳細に依存するな。ですが、例えばC#のstringクラスはどうするのか?

こういったクラスは安定しているので依存しても問題ありません。
依存関係逆転の原則を考えるときはOSやプラットフォーム周りは気にせず、システム内の変化しやすい詳細にのみ気をつければ良いそうです。

まとめ

  • 変化しやすい実装の詳細に依存するな。抽象に依存しよう。

参考

ASP.NET CoreをDocker使ってHerokuにデプロイ & PostgreSQLを使う

devcenter.heroku.com

ASP.NET CoreはHerokuにデプロイできないものだと思っていたのですが、
HerokuのDockerコンテナを使ったデプロイ方法であれば、ASP.NET Coreも動くようなので試してみました。

Dockerを使用したHerokuへのデプロイ方法は2つあるようです。

  1. 自分でビルドしたDockerイメージをHerokuにデプロイする方法
  2. heroku.ymlという設定ファイルを使ってHeroku上でDockerイメージをビルドし、デプロイする方法

1の方法は他の記事でも見ましたが、2の方法を書いているところがなさそうだったのでheroku.ymlを使った方法でデプロイしたいと思います。

環境

  • .NET Core 3.1
  • macOS Catalina 10.15.1

サンプルプロジェクト

Razor Pagesの公式チュートリアルを元に、Herokuデプロイ用のサンプルプロジェクトを用意しました。

github.com

このプロジェクトは、生徒の情報をCRUDできるRazor Pagesアプリです。
データベースはSQLiteが使われています。

このプロジェクトを使って進めていきますので、GitHubからリポジトリをクローンしておいてください。

Nugetパッケージの復元

クローンしたらまずはNugetパッケージを復元します。
ターミナル上でHerokuDeploySample.csprojがあるディレクトリまで移動し、以下のコマンドを実行。

dotnet restore

HerokuのPostgreSQLへ接続するための設定

Heroku上の本番環境ではPostgreSQLを使い、開発環境ではSQLiteを使うようにコードを修正していきます。

EF CoreのPostgreSQLライブラリをNugetから追加します。

dotnet add package Npgsql.EntityFrameworkCore.PostgreSQL

追加できたら、環境によってSQLitePostgreSQLか接続を分けるようにStartup.csを修正します。

public Startup(IConfiguration configuration, IWebHostEnvironment env)
{
    Configuration = configuration;
    Env = env;
}

public IConfiguration Configuration { get; }
public IWebHostEnvironment Env { get; }

StartupコンストラクタにIWebHostEnvironmentインターフェースを追加して、
Envプロパティから環境情報にアクセスできるようにしました。

次に、Envプロパティを使って接続先を分けます。

public void ConfigureServices(IServiceCollection services)
{
    services.AddRazorPages();

    if (Env.IsDevelopment())
    {
        services.AddDbContext<SchoolContext>(options =>
        options.UseSQLite(Configuration.GetConnectionString("SchoolContext")));
    }
    else
    {
        var uri = new Uri(Configuration["DATABASE_URL"]);
        var userInfo = uri.UserInfo.Split(":");
        (var user, var password) = (userInfo[0], userInfo[1]);
        var db = Path.GetFileName(uri.AbsolutePath);

        var connStr = $"Host={uri.Host};Port={uri.Port};Database={db};Username={user};Password={password};Enlist=true";
        services.AddDbContext<SchoolContext>(options => options.UseNpgsql(connStr));
    }
}

ConfigureServicesメソッド内でEnv.IsDevelopmentを使用して接続先を分岐させます。
ifのtureの部分が開発時(SQLite)で、falseの部分がHeroku(PostgreSQL)になっています。

このあと設定しますが、HerokuのPostgresアドオンを追加すると、DATABASE_URLというPostgreSQLの接続先URLが入った環境変数がHerokuに追加されます。

上のConfiguration["DATABASE_URL"]はその環境変数にアクセスし、接続先URLを取得するコードです。
これをそのままUseNpgsqlに渡しても、接続文字列のフォーマットが異なるため接続できません。
そのため接続先URLから情報を抜き出し、ライブラリに渡す用の文字列を作っています。
ロジックが結構書かれちゃってますが、サンプルなのでこのままで。

修正後のStartup.cs全体のコード

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.HttpsPolicy;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.EntityFrameworkCore;
using HerokuDeploySample.Data;
using System.IO;

namespace HerokuDeploySample
{
    public class Startup
    {
        public Startup(IConfiguration configuration, IWebHostEnvironment env)
        {
            Configuration = configuration;
            Env = env;
        }

        public IConfiguration Configuration { get; }
        public IWebHostEnvironment Env { get; }

        public void ConfigureServices(IServiceCollection services)
        {
            services.AddRazorPages();

            if (Env.IsDevelopment())
            {
                services.AddDbContext<SchoolContext>(options =>
                options.UseSqlite(Configuration.GetConnectionString("SchoolContext")));
            }
            else
            {
                var uri = new Uri(Configuration["DATABASE_URL"]);
                var userInfo = uri.UserInfo.Split(":");
                (var user, var password) = (userInfo[0], userInfo[1]);
                var db = Path.GetFileName(uri.AbsolutePath);

                var connStr = $"Host={uri.Host};Port={uri.Port};Database={db};Username={user};Password={password};Enlist=true";
                services.AddDbContext<SchoolContext>(options => options.UseNpgsql(connStr));
            }
        }

        public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }
            else
            {
                app.UseExceptionHandler("/Error");
                app.UseHsts();
            }

            app.UseHttpsRedirection();
            app.UseStaticFiles();

            app.UseRouting();

            app.UseAuthorization();

            app.UseEndpoints(endpoints =>
            {
                endpoints.MapRazorPages();
            });
        }
    }
}

Dockerfileを作成

次に、Heroku上で動かすコンテナの元となるDockerfileを作ります。

Startup.csと同じディレクトリに以下の内容のDockerfileを作成してください。

※2020/11/26 追記

DockerfileにEXPOSEを書いていましたがherokuではサポートしておらず、使用するポートは$PORTに渡されるので不要でした。

また、dotnet publishrestorebuildも暗黙的に実行されるため不要なコマンド行を削除しました。

FROM mcr.microsoft.com/dotnet/core/aspnet:3.1-buster-slim AS base
WORKDIR /app

FROM mcr.microsoft.com/dotnet/core/sdk:3.1-buster AS publish
WORKDIR "/src/HerokuDeploySample"
COPY . .

RUN dotnet publish "HerokuDeploySample.csproj" -c Release -o /app/publish

FROM base AS final
WORKDIR /app
COPY --from=publish /app/publish .
CMD ASPNETCORE_URLS=http://*:$PORT dotnet HerokuDeploySample.dll

このDockerfileはVisual StudioでDockerfile追加したときにデフォルトで書かれている内容を、Heroku用に書き換えたものです。

参考: Deploy ASP.NET Core 3.1 Web API to Heroku with Docker – Jakub Wajs

heroku.yml

heroku.ymlとは

devcenter.heroku.com

Dockerを使ったHerokuアプリを構築するための設定ファイルです。
Heroku上でイメージをビルドでき、コマンドや環境変数なども設定できるみたいです。

heroku.ymlを作成

GitHubからクローンしたサンプルプロジェクトのルートに以下の内容のheroku.ymlを作ります。

ディレクトリ構造

root/
 ├ HerokuDeploySample/
 │ ├  Data/
 │ ├  Models/
 │ ├  ...../
 │ └ ....../
 └ heroku.yml

heroku.yml

build:
  docker:
    web: HerokuDeploySample/Dockerfile

中身は簡単で、Dockerfileの場所を指定してあげるだけでOKです。

Herokuにはリポジトリをプッシュしてデプロイするので、
ここまでの修正をコミットしておいてください。

Herokuへデプロイ

Herokuアプリを構築してデプロイします。

Herokuの操作はすべてHeroku CLIを使って行います。
ターミナルなどではカレントディレクトリをプロジェクトのルート(heroku.ymlを作った場所)にしておきます。

1. Herokuへログイン

heroku login

2. Herokuアプリの作成
デプロイ先が作られ、gitのリモートにherokuが追加されます。

heroku create

3. Postgresアドオンを追加
HerokuアプリでPostgreSQLを使うためアドオンを追加します。
これで環境変数DATABASE_URLが追加され、HerokuでPostgreSQLを使う準備ができました。
環境変数の中身はコマンドで見れます。

heroku addons:create heroku-postgresql:hobby-dev

# DATABASE_URLの中身を表示するコマンド
# heroku config:get DATABASE_URL

4. アプリをDockerコンテナで動かすように設定

heroku stack:set container

5. Herokuへリポジトリをプッシュ

git push heroku master

プッシュするとHeroku上で、追加したDockerfileを元にイメージのビルドが始まります。

しばらく待ち、「remote: Verifying deploy... done.」と表示されたら完了です。
heroku openコマンドでアプリを開いて、動いているか確認しましょう。

成功していれば、/Studentsに生徒情報が一覧表示されています。

f:id:eiken7kyuu:20200805205041p:plain

PCにPostgreSQLがインストールされていれば、Heroku上のデーテベースもコマンドから中身を確認できます。

heroku pg:psql

=> SELECT * FROM "Student";
 ID | LastName  | FirstMidName
----+-----------+--------------
  1 | Alexander | Carson
  2 | Alonso    | Meredith
  3 | Anand     | Arturo
  4 | Barzdukas | Gytis
  5 | Li        | Yan
  6 | Justice   | Peggy
  7 | Norman    | Laura
  8 | Olivetto  | Nino
(8 rows)

修正後のプロジェクト

同じリポジトリのafterブランチに修正後のプロジェクトがあるのでこちらも参照してください。

GitHub - eiken7kyuu/AspNetCoreHeroku at after

その他参考にした記事

ASP.NET Core 3.1のREST-APIをVisualStudio2019とDockerでHerokuにデプロイする手順 - Qiita

Entity Framework Coreが生成するSQLクエリを確認する

ASP.NET CoreでEntity Framework Coreが生成するSQLクエリをログに出力してみます。
ここでは.NET Coreの組み込みのログ機能を使いました。

実行環境

.NET Core 3.1

SQLクエリのパラメーターをログに含める

デフォルトでは、SQLクエリに渡されるパラメーターは表示されないようになっています。
これを表示させるように修正します。

Startup.cs内のConfigureServicesメソッド内にあるEF Coreの設定が、以下のように構成されているとします。

public void ConfigureServices(IServiceCollection services)
{
    services.AddRazorPages();

    services.AddDbContext<SchoolContext>(options =>
        options.UseSqlite(Configuration.GetConnectionString("SchoolContext")));
}

DbContextOptionsBuilderクラスのEnableSensitiveDataLoggingメソッドを追加すると有効化。

public void ConfigureServices(IServiceCollection services)
{
    services.AddRazorPages();

    services.AddDbContext<SchoolContext>(options =>
    {
        options.EnableSensitiveDataLogging(); // new
        options.UseSqlite(Configuration.GetConnectionString("SchoolContext"));
    });
}

appsettings.jsonを修正

EF CoreのSQLがログに表示されるように、
appsettings.jsonLogLevel"Microsoft.EntityFrameworkCore": "Information"を追加します。

開発時のみ表示されればいいので、appsettings.Development.jsonに記述しました。

{
  "Logging": {
    "LogLevel": {
      "Default": "Debug",
      "Microsoft": "Warning",
      "Microsoft.Hosting.Lifetime": "Information",
      "Microsoft.EntityFrameworkCore": "Information"
    }
  }
}

以上で設定は完了です。

SQLを確認

Webアプリを実行して、データベースを使う処理を実行します。
Visual Studio Codeでは「DEBUG CONSOLE」にSQLが出力されています。

info: Microsoft.EntityFrameworkCore.Database.Command[20101]
      Executed DbCommand (2ms) [Parameters=[@__Format_1='%Yan%' (Size = 5)], CommandType='Text', CommandTimeout='30']
      SELECT COUNT(*)
      FROM "Student" AS "s"
      WHERE ("s"."LastName" LIKE @__Format_1) OR ("s"."FirstName" LIKE @__Format_1)

info: Microsoft.EntityFrameworkCore.Database.Command[20101]
Microsoft.EntityFrameworkCore.Database.Command: Information: Executed DbCommand (1ms) [Parameters=[@__Format_1='%Yan%' (Size = 5), @__p_3='3' (DbType = String), @__p_2='0' (DbType = String)], CommandType='Text', CommandTimeout='30']
SELECT "s"."ID", "s"."EnrollmentDate", "s"."FirstName", "s"."LastName"
FROM "Student" AS "s"
WHERE ("s"."LastName" LIKE @__Format_1) OR ("s"."FirstName" LIKE @__Format_1)
ORDER BY "s"."LastName"
LIMIT @__p_3 OFFSET @__p_2
      Executed DbCommand (1ms) [Parameters=[@__Format_1='%Yan%' (Size = 5), @__p_3='3' (DbType = String), @__p_2='0' (DbType = String)], CommandType='Text', CommandTimeout='30']
      SELECT "s"."ID", "s"."EnrollmentDate", "s"."FirstName", "s"."LastName"
      FROM "Student" AS "s"
      WHERE ("s"."LastName" LIKE @__Format_1) OR ("s"."FirstName" LIKE @__Format_1)
      ORDER BY "s"."LastName"
      LIMIT @__p_3 OFFSET @__p_2

デフォルトではパラメータが?になっていますが、 EnableSensitiveDataLoggingを追加することで中身が表示されています。

EnableSensitiveDataLogging なし

[Parameters=[@__Format_1='?' (Size = 5)], CommandType='Text', CommandTimeout='30']

EnableSensitiveDataLogging あり

[Parameters=[@__Format_1='%Yan%' (Size = 5)], CommandType='Text', CommandTimeout='30']

おわりに

ORMはブラックボックスな部分が多いので、開発時はSQLが出力されるようになっているほうがいいですね。
ASP.NET CoreもEntity Framework Coreもデフォルトでログが出力されるようになっているので、設定はかなり楽でした。

参考

パスワードの保存方法について

Webアプリケーションのパスワード保存方法について、どのように保存すれば安全性が高まるのかのメモです。

パスワードをそのまま保存すると?

パスワードを平文のままデータベースに保存し、なんらかの原因により、利用者のパスワードが外部に漏洩した場合、 悪用されて利用者が被害にあうことがあります。

  • 利用者の情報漏洩
  • パスワードリスト攻撃への悪用
  • 漏洩した利用者の権限でアプリの機能を悪用する

従って、万が一パスワードが漏洩しても悪用されないように保護して保存する必要があります。

どうやって保護するか

結論から言うと、パスワードをメッセージダイジェストにしてからデータベースに保存する方法が安全性が高いです。

任意の長さのデータ(ビット列)を固定長のデータに圧縮する関数をハッシュ関数と言います。 このハッシュ関数から生成される固定長の値をメッセージダイジェスト(またはハッシュ値)と言います。

メッセージダイジェストによる保護が安全と言われる理由は、ハッシュ関数が以下の特性を持つためです。

  • ハッシュ値から元データを得ることが困難(一方向性)
  • 異なる文字列から得られるハッシュ値が同じになる確率が非常に低い(衝突耐性)

実際にMD5というハッシュ関数ハッシュ値を出してみるとこんな文字列が出力されます。
このような値にしてからデータベースに保存するわけです。

$ echo -n 'hogehoge' | md5
329435e5e66be809a656af105f42401e

ほんとに安全?

ハッシュ値から元データを得ることが困難」と書きましたが、パスワードの場合は一般的に文字数が短く、文字種も限られるため、総当たりによるオフラインブルートフォース攻撃や、レインボーテーブルといった手法で解析できてしまうようです。 そのため、単純なMD5SHA-1によるハッシュ値でパスワードを保存しても危険です。

ではどうすればいいでしょうか。現状は以下のように対策する必要があります。

  • より強力な暗号学的ハッシュ関数を使う
  • ソルトと呼ばれる文字列を元データに追加することで、見かけのパスワードをある程度長くして、同じパスワードでも異なるハッシュ値が生成されるようにする
  • ハッシュ計算を繰り返し行うストレッチングをして、攻撃にかかる計算時間を長くさせる

これらに対応した、BcryptPBKDF2といったアルゴリズムを使用することで、ハッシュ値の解析を困難にさせ安全性が高まります。

C#でBcryptを使ってみる

Bcryptアルゴリズムを使って、ハッシュ値を計算してみます。
C#ではBCrypt.Net-Nextというライブラリが有名みたいです。

github.com

コードが以下になります。
ハッシュ値計算、パスワードとハッシュ値の検証はどちらも1行で簡単に書けました。

// ハッシュ値を計算
string passwordHash1 = BCrypt.Net.BCrypt.HashPassword("hogehoge");
string passwordHash2 = BCrypt.Net.BCrypt.HashPassword("hogehoge");
Console.WriteLine(passwordHash1);
Console.WriteLine(passwordHash2);

// $2a$11$JMKdIywj9/dHESrcd3581eRx9arbApUVDLPrzkFhlGPmkaPzAVxQ2
// $2a$11$QwNFkmlQRYlegHbdVIfFkOyHP6hSa8TIa7R65Ss94Dzf9DQalqV2i

// パスワードとハッシュ値の検証
bool verified1 = BCrypt.Net.BCrypt.Verify("hogehoge", passwordHash);
bool verified2 = BCrypt.Net.BCrypt.Verify("hogehoge1", passwordHash);
Console.WriteLine(verified1); // true
Console.WriteLine(verified2); // false

HashPasswordにパスワードを渡すと、ハッシュ値が得られます。
ソルトを与えて計算しているので、同じhogehogeでも異なるハッシュ値になることが確認できます。
HashPasswordは独自のソルトを渡して計算できるようですが、自分でソルトを作るのは危険なのでライブラリに任せます。

先頭4文字目からの$11がストレッチング回数を表しています。11回ではなく、2^n乗なので、2^11 2048回です。

// ハッシュ値を計算
string passwordHash1 = BCrypt.Net.BCrypt.HashPassword("hogehoge");
string passwordHash2 = BCrypt.Net.BCrypt.HashPassword("hogehoge");
Console.WriteLine(passwordHash1);
Console.WriteLine(passwordHash2);

// $2a$11$JMKdIywj9/dHESrcd3581eRx9arbApUVDLPrzkFhlGPmkaPzAVxQ2
// $2a$11$QwNFkmlQRYlegHbdVIfFkOyHP6hSa8TIa7R65Ss94Dzf9DQalqV2i

Verifyにパスワードとハッシュ値を渡すと、一致しているか検証します。
hogehoge1はパスワードが異なるので当然falseが返ります。

// パスワードとハッシュ値の検証
bool verified1 = BCrypt.Net.BCrypt.Verify("hogehoge", passwordHash);
bool verified2 = BCrypt.Net.BCrypt.Verify("hogehoge1", passwordHash);
Console.WriteLine(verified1); // true
Console.WriteLine(verified2); // false

まとめ

  • パスワードは平文のまま保存しない
  • メッセージダイジェストにしてから保存する
  • ソルトとストレッチングで対策しているBcryptやPBKDF2を使ってメッセージダイジェストを生成する

参考: 体系的に学ぶ 安全なWebアプリケーションの作り方 第2版 脆弱性が生まれる原理と対策の実践 | 徳丸 浩 |本 | 通販 | Amazon

GitHubに草が生えない問題

GitHubにpushしてもcontributionsに反映されていなかったり、コミットユーザーのアイコンが正しく表示されない現象が起こってた。
調べたら単純にGitHubとgitクライアントのuser.emailの設定が異なるせいでした。

user.nameとuser.emailをどっちも合わせたところ、草が生えるのを確認できた。 この時、gitクライアントのメールアドレスはnoreplyのメールアドレスを設定する。