ASP.NET Core DIのライフタイムの種類
ASP.NET CoreのDIで設定できるライフタイムについてのメモです。
DIについては特に説明しません。
インスタンスのライフタイム
公式ドキュメントには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
クラスに依存している状態です。
IndexModel
にMyDependency
が注入されるように設定します。
まずはSingletonから確認します。
public void ConfigureServices(IServiceCollection services) { services.AddSingleton<IMyDependency, MyDependency>(); // new services.AddRazorPages(); }
この状態でアプリを起動して、IndexModel
のGuidを表示してみます。
上記のような文字列が出てくると思いますが、何度リロードしても同じ値が表示されているかと思います。アプリを再起動しないと値が変化しません。
ちゃんと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はそれぞれ別の値が表示されると思います。
MyDependency
にTransientを指定しているのでmyService1
とmyService2
が持つMyDependency
は別のインスタンスであると確認できました。
今度はMyDependency
にScopedを指定してみます。
public void ConfigureServices(IServiceCollection services) { services.AddScoped<IMyDependency, MyDependency>(); // new services.AddTransient<IMyService, MyService>(); services.AddRazorPages(); }
Guid1と2の表示結果
どちらも同じ値になりました。
Scopedはリクエスト毎にインスタンスを生成し、リクエスト内で使い回すため、
myService1
とmyService2
は同じMyDependency
インスタンスを参照しているってことになりますね。ちゃんとScopedが機能していました!
ざっくりまとめ
- Transient: 毎回新しくインスタンスを生成する。リクエスト終了時に破棄。
- Scoped: 1リクエストで1つインスタンスを生成し、同じリクエスト内で使い回す。リクエスト終了時に破棄。
- Singleton: 最初のリクエストでインスタンスを生成し、それ以降ずっと同じものを使い回す。アプリ終了まで破棄されない。
ドキュメントだけでは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); } } }
参考
依存関係逆転の原則
SOLID原則の1つである、依存関係逆転の原則について簡単にまとめたものです。
※依存性の注入に関してここでは解説しません。
依存関係逆転の原則とは
定義
- 上位のモジュールは下位のモジュールに依存してはならない。どちらのモジュールも「抽象」に依存すべきである。
- 「抽象」は実装の詳細に依存してはならない。実装の詳細が「抽象」に依存すべきである。
引用元: Robert C. Martin. アジャイルソフトウェア開発の奥義
これだけだとちょっとなにを言ってるのかよくわかりませんね。
簡単に言うと、変更されやすく、脆いモジュールへの依存しているものを、変更されにくいものへ依存するようにしなさいという原則です。
変更されやすいものに依存している例
本を扱うシステムがあり、その中に以下のようなモジュールがあるとします。
これは、本のタイトルを入力すると本検索APIを呼び出し、本情報を取得、結果をユーザーに返すモジュールです。
上位レベルService
レイヤ内のBookService
は検索結果を取得するために、下位レベルInfrastructure
レイヤにあるBookSearchApi
クラスを利用しています。
システムはこのように制御の流れと依存の流れが同じになってしまいがちです。
アプリケーションの方針を決めている上位レベルのモジュールが、実装の詳細を担当している下位レベルのモジュールに依存してしまっています。
上位レベルと比べて下位レベルのモジュールは脆く、変更されやすいものです。
この依存によって、下位レベルのモジュールの変更が上位レベルのモジュールにまで影響するようになっています。
そこで依存関係逆転の原則を使って、下位レベルのモジュールを上位レベルのモジュールに依存させ、依存関係を逆転させます。
依存を逆転させたあと
依存関係を逆転させた後の図が以下になります。
Service
レイヤでIBookSearchApi
インターフェースを宣言し、Infrastructure
レイヤのBookSearchApi
はこのインターフェースを実装するようにします。
こうすることでBookService
は同じレイヤにある「抽象」を通してBookSearchApi
を利用するようになり、上位モジュールが下位モジュールに依存しなくなりました。
逆に、上位の「抽象」に下位レベルの実装の詳細が依存していることになります。
つまり、下位レベルを変更しても上位レベルがその影響を受けなくなります。
実装の詳細に依存せず、抽象に依存する。これが依存関係逆転の原則です。
所有権も逆転
クライアント側がインターフェースを所有しているので「依存」だけでなく「所有権」も逆転しています。
Infrastructure
レイヤの影響を受けないだけでなく、IBookSearchApi
を実装するものであれば何でも再利用できるようになっています。
例えば元々、楽天ブックス書籍検索APIを使って実装していたものをGoogle Books APIsへ変更することも楽になりました。
標準ライブラリとかはどうするの?
実装の詳細に依存するな。ですが、例えばC#のstringクラスはどうするのか?
こういったクラスは安定しているので依存しても問題ありません。
依存関係逆転の原則を考えるときはOSやプラットフォーム周りは気にせず、システム内の変化しやすい詳細にのみ気をつければ良いそうです。
まとめ
- 変化しやすい実装の詳細に依存するな。抽象に依存しよう。
参考
ASP.NET CoreをDocker使ってHerokuにデプロイ & PostgreSQLを使う
ASP.NET CoreはHerokuにデプロイできないものだと思っていたのですが、
HerokuのDockerコンテナを使ったデプロイ方法であれば、ASP.NET Coreも動くようなので試してみました。
Dockerを使用したHerokuへのデプロイ方法は2つあるようです。
- 自分でビルドしたDockerイメージをHerokuにデプロイする方法
heroku.yml
という設定ファイルを使ってHeroku上でDockerイメージをビルドし、デプロイする方法
1の方法は他の記事でも見ましたが、2の方法を書いているところがなさそうだったのでheroku.yml
を使った方法でデプロイしたいと思います。
環境
- .NET Core 3.1
- macOS Catalina 10.15.1
サンプルプロジェクト
Razor Pagesの公式チュートリアルを元に、Herokuデプロイ用のサンプルプロジェクトを用意しました。
このプロジェクトは、生徒の情報を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
追加できたら、環境によってSQLiteかPostgreSQLか接続を分けるように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 publish
でrestore
もbuild
も暗黙的に実行されるため不要なコマンド行を削除しました。
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とは
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
に生徒情報が一覧表示されています。
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.json
のLogLevel
に
"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
ほんとに安全?
「ハッシュ値から元データを得ることが困難」と書きましたが、パスワードの場合は一般的に文字数が短く、文字種も限られるため、総当たりによるオフラインブルートフォース攻撃や、レインボーテーブルといった手法で解析できてしまうようです。 そのため、単純なMD5やSHA-1によるハッシュ値でパスワードを保存しても危険です。
ではどうすればいいでしょうか。現状は以下のように対策する必要があります。
- より強力な暗号学的ハッシュ関数を使う
- ソルトと呼ばれる文字列を元データに追加することで、見かけのパスワードをある程度長くして、同じパスワードでも異なるハッシュ値が生成されるようにする
- ハッシュ計算を繰り返し行うストレッチングをして、攻撃にかかる計算時間を長くさせる
これらに対応した、BcryptやPBKDF2といったアルゴリズムを使用することで、ハッシュ値の解析を困難にさせ安全性が高まります。
C#でBcryptを使ってみる
Bcryptアルゴリズムを使って、ハッシュ値を計算してみます。
C#ではBCrypt.Net-Next
というライブラリが有名みたいです。
コードが以下になります。
ハッシュ値計算、パスワードとハッシュ値の検証はどちらも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