ASP.NET Core版 対人対戦できるぷよぷよをつくる(5)の続きです。
これまでは対戦して終わったらなにもしませんでした。勝ち数を記憶させてランクアップを競うようにすれば面白さも倍増するかもしれません。その前提として他のプレイヤーの足を引っ張るために同じ名前でプレイしてわざと負けてランクを下げるようなインチキができないように、プレイヤーの同一性を区別できるようにしておかなければなりません。
ではどうすればいいでしょうか? ログイン機能を追加してログイン状態でプレイすればなりすましは防げるはずです。そこで今回は準備編として簡単なログイン機能を追加する方法を考えます。
Contents
簡単なログイン機能を実装する
まずProgram.csを編集します。プロジェクトを作成したときに生成されるProgram.csに以下の3行を追加します。
Program.cs
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
using Microsoft.AspNetCore.Authentication.Cookies; // この行を追加 var builder = WebApplication.CreateBuilder(args); builder.Services.AddRazorPages(); builder.Services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme).AddCookie(); // この行を追加 var app = builder.Build(); if (!app.Environment.IsDevelopment()) { app.UseExceptionHandler("/Error"); } app.UseStaticFiles(); app.UseRouting(); app.UseAuthorization(); app.MapRazorPages(); app.UseAuthentication(); // この行を追加 app.Run(); |
cshtmlファイル
次にcshtmlファイルとcsファイルを作成します。LoginTest名前空間のなかにFirstModelクラスを定義します。このクラスはPageModelクラスを継承しています。
cshtmlファイルはPagesフォルダの配下であればどこでもかまいません。最初はテキストボックスに文字列を入力してボタンをクリックすればログインできてしまう単純なものをつくります。パスワードを入力しないとログインできないものはそのあと考えます。
Pages\First.cshtml
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
@page @model LoginTest.FirstModel @{ Layout = null; } <!DOCTYPE html> <html lang="ja"> <head> <meta charset="utf-8" /> <title>Test 1</title> </head> <body> <form method="post"> <input type="text" asp-for="InputText" /> <input type="submit" value="Button1" /> </form> <p>@Model.MessageText</p> </body> </html> |
csファイル
インデントが深くなるので名前空間は省略します。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
using Microsoft.AspNetCore.Authentication.Cookies; using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.RazorPages; using System.Security.Claims; namespace LoginTest { public class FirstModel : PageModel { } } |
1 2 3 |
public class FirstModel : PageModel { } |
Pages\First.cshtmlの最初に @model LoginTest.FirstModel と書いてあるのでページにアクセスするとOnGetメソッドが呼び出されます。このなかですでにログインしている場合はどのアカウント名でログインしているか、ログインしていない場合は”ログインしていません”と表示させます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 |
public class FirstModel : PageModel { [BindProperty] public string InputText { get; set; } public string MessageText = ""; public void OnGet() { if (User.IsInRole("Player")) { var list = User.Claims.ToList(); var ret = list.FirstOrDefault(_ => _.Type == "PlayerName"); if (ret != null) MessageText = $"[{ret.Value}] でログイン中" ; else MessageText = ""; } else MessageText = "ログインしていません"; } } ボタンがクリックされたらPostされます。ログインしていないならログインし、すでにログインしているのであればログアウトします。 ログインしていない状態でテキストボックスに文字列が入力されている場合はログインする処理をおこないます。"PlayerName"にテキストボックスに入力されている文字列を指定し、"Player"としてログインします。またブラウザを閉じてもログインした状態を維持できるようにしています。 ログインの処理をしたときもログアウトの処理をしたときも最後に同じページへリダイレクトしています。こうすることでOnGetメソッドが実行され、ログイン状態に適した文字列が表示されるようになります。 public class FirstModel : PageModel { public async Task<RedirectResult> OnPost() { // ログインしていないなら if (InputText != null && !User.IsInRole("Player")) { List<Claim> claims = new List<Claim> { new Claim("PlayerName", InputText), new Claim(ClaimTypes.Role, "Player"), }; var claimsIdentity = new ClaimsIdentity( claims, CookieAuthenticationDefaults.AuthenticationScheme); var authProperties = new AuthenticationProperties { IsPersistent = true // ブラウザを閉じてもログインしたままにする }; await HttpContext.SignInAsync( CookieAuthenticationDefaults.AuthenticationScheme, new ClaimsPrincipal(claimsIdentity), authProperties ); } else { await HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme); } // 自分自身へリダイレクト return Redirect("./First"); } } |
パスワードを入力しないとログインできなくする
次にパスワードを入力しないとログインできないように作りかえます。この場合、アカウント名とパスワードをどこかに保存しなければなりません。本当はMySQLのようなデータベースに保存するべきなのですが、ここでは単純にパスワードを入力しないとログインできないようにしたいだけなので、テキストファイルに保存します。ただしパスワードが流出するのは絶対に避けないといけないので暗号化された状態で保存します。
1 2 3 4 5 |
public string GetHashValue(string str) { System.Security.Cryptography.HashAlgorithm hashAlgorithm = System.Security.Cryptography.SHA256.Create(); return string.Join("", hashAlgorithm.ComputeHash(Encoding.UTF8.GetBytes(str)).Select(x => $"{x:x2}")); } |
cshtmlファイル
ユーザーがログインしていないときはアカウント自体を作っていないかもしれません。そこでログインのためのフォームだけでなくユーザー登録をするためのフォームもいっしょに表示させます。そのうえでログインしているときはユーザー登録のためのフォームは明らかに不要なので非表示にします。
Pages\Second.cshtml
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 |
@page @model LoginTest.SecondModel @{ Layout = null; string loginPlayerName = Model.GetLoginPlayerName(); // ログインしているユーザーの名前 string loginResultText; // ログイン状態か否か? またログインしようとしたときの結果を表示する string registerResultText = @Model.RegisterResultText; // ユーザー登録しようとしたときの結果を表示する string loginButtonText; // ログイン用のボタンに表示させる文字列("ログイン"または"ログアウト") string displayRegisterForm; // ユーザー登録のためのフォームの表示、非表示("block"または"none") string displayLoginTextBox; // ログインフォームのテキストボックスの表示、非表示("inline-block"または"none") // ログインしているかどうかでボタンの文字列やフォーム等の表示と非表示を変更する if (loginPlayerName == "") { loginResultText = "ログインしていません"; loginButtonText = "ログイン"; displayRegisterForm = "block"; displayLoginTextBox = "inline-block"; } else { loginResultText = $"[{loginPlayerName}] でログイン中"; loginButtonText = "ログアウト"; displayRegisterForm = "none"; displayLoginTextBox = "none"; } // ユーザー登録しようとしたときの結果(表示させるべき文字列があれば表示させる) if (Model.LoginResultText != "") loginResultText = Model.LoginResultText; } <!DOCTYPE html> <html lang="ja"> <head> <meta charset="utf-8" /> <title>Test 2</title> </head> <body> <p>ログイン用フォーム</p> <form method="post"> <div style="display:@displayLoginTextBox"> <input type="text" asp-for="InputPlayerName" /><br> <input type="password" asp-for="Password" /> </div> <input type="submit" asp-page-handler="Login" value="@loginButtonText" /> </form> <p>@loginResultText</p> <!-- ログイン状態か否か? またログインしようとしたときの結果を表示する --> <div style="display:@displayRegisterForm"> <p>登録用フォーム</p> <form method="post"> <input type="text" asp-for="InputRegisterName" /> <input type="submit" asp-page-handler="Register" value="登録" /><br> <input type="password" asp-for="Password1" /><br> <input type="password" asp-for="Password2" /><br> </form> <p>@registerResultText</p> <!-- ユーザー登録しようとしたときの結果を表示する --> </div> </body> </html> |
csファイル
次にcsファイル部分ですが同様に名前空間部分は省略します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
using Microsoft.AspNetCore.Authentication.Cookies; using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.RazorPages; using System.Security.Claims; using System.Text; namespace LoginTest { public class SecondModel : PageModel { } } |
1 2 3 |
public class SecondModel : PageModel { } |
ログインユーザー名の取得
GetLoginPlayerNameメソッドはログインしているときにユーザー名を返します。ログインしていないときは空文字列を返します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
public class SecondModel : PageModel { public string GetLoginPlayerName() { if (User.IsInRole("Player")) { List<Claim> list = User.Claims.ToList(); Claim? ret = list.FirstOrDefault(_ => _.Type == "PlayerName"); if (ret != null) return ret.Value; } return ""; } } |
ユーザー登録されているかの確認
GetHashValueメソッドはパスワードをハッシュ値に変換します。ハッシュ化は基本的に不可逆変換であり、元に戻すことができません。そのためデータの流出が起きたときにも解読される心配がありません。そのためセキュリティレベルを引き上げることができるのです。
1 2 3 4 5 6 7 8 |
public class SecondModel : PageModel { public string GetHashValue(string str) { System.Security.Cryptography.HashAlgorithm hashAlgorithm = System.Security.Cryptography.SHA256.Create(); return string.Join("", hashAlgorithm.ComputeHash(Encoding.UTF8.GetBytes(str)).Select(x => $"{x:x2}")); } } |
IsRegisteredメソッドはユーザー登録されているかどうかを調べます。
後述しますが、ユーザー名とパスワードのハッシュ値はテキストファイルにタブ区切りで保存されています。引数が2つあるメソッドはテキストファイルの行のなかにユーザー名があるかどうかを調べるだけでなく、パスワードのハッシュ値を生成してこれが保存されているものと一致するかどうかを調べます。ユーザー名がみつかってもパスワードのハッシュ値が異なっていればログインしようとした人が正しくないパスワードを入力したと判断できます。
引数が1つしかないものはテキストファイルの行のなかにユーザー名があるかどうかだけ調べます。これによってすでに同じユーザー名で登録することを不可能にしています。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 |
public class SecondModel : PageModel { // ユーザー情報が登録されているファイル const string ResistPath = "../registered-data-test2.dat"; bool IsRegistered(string playerName, string password) { bool isRegistered = false; if (System.IO.File.Exists(ResistPath)) { StreamReader sr = new StreamReader(ResistPath, Encoding.UTF8); while (true) { string? str = sr.ReadLine(); if (str == null) break; string[] strings = str.Split('\t'); if (strings[0] == playerName) { if (password == "") isRegistered = true; if (password != "" && strings.Length > 1 && strings[1] == GetHashValue(password)) isRegistered = true; break; } } sr.Close(); } return isRegistered; } bool IsRegistered(string playerName) { return IsRegistered(playerName, ""); } } |
ユーザー登録するための処理
ユーザー登録するための処理を示します。
パスワードは見えないので入力ミスを防ぐため、2回入力させます。そして異なっている場合はユーザー登録できないことにします。
[登録]ボタンがクリックされたら登録しようとしているユーザー名がInputRegisterNameに、パスワードがPassword1とPassword2に格納されます。これらに文字列が格納されているか、Password1とPassword2は同じかを確認したらユーザー名からタブ文字を取り除きます(データを保存するときタブ区切りにするため)。
そのあとIsRegisteredメソッドで同じユーザー名で登録されていないか調べます。もし重複がなければユーザー登録の処理をおこないます。GetHashValueメソッドでパスワードのハッシュ値を生成してサーバー上のファイルに書き込みます。
いずれの場合もユーザー登録の処理が成功したのかどうかわかるように、処理結果をRegisterResultTextに格納してページに表示させます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 |
public class SecondModel : PageModel { [BindProperty] public string InputRegisterName { get; set; } [BindProperty] public string Password1 { get; set; } [BindProperty] public string Password2 { get; set; } public string RegisterResultText = ""; public void OnPostRegister() { if (InputRegisterName == null || Password1 == null || Password1 != Password2) { RegisterResultText = "入力が不正"; return; } string playerName = InputRegisterName.Replace("\t", ""); if (playerName == "") { RegisterResultText = "入力が不正"; return; } if (IsRegistered(playerName)) { RegisterResultText = "登録されている"; } else { StreamWriter sw = new StreamWriter(ResistPath, true, Encoding.UTF8); sw.WriteLine(playerName + "\t" + GetHashValue(Password1)); sw.Close(); RegisterResultText = "登録した"; } } } |
ログインするための処理
ログインしていないならログインの処理をおこないます。ユーザーがアカウント名とパスワードとして入力した文字列がInputPlayerNameとPasswordに格納されているので、サーバー上のファイルのなかにInputPlayerNameからタブ文字を取り除いたものとパスワードのハッシュ値が書き込まれた行がないか調べます。あればログインの処理をおこないます。なければ入力ミスか悪意を持つ者によるアクセスなので、ログインできない旨を表示させます。
すでにログインされている場合はログアウトの処理をおこないます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 |
public class SecondModel : PageModel { [BindProperty] public string InputPlayerName { get; set; } [BindProperty] public string Password { get; set; } public string LoginResultText = ""; public async Task<RedirectResult?> OnPostLogin() { // ログインしていないなら if (!User.IsInRole("Player")) { if (InputPlayerName == null || Password == null || InputPlayerName.Replace("\t", "") == "") { LoginResultText = "名前を入力してください"; return null; } string playerName = InputPlayerName.Replace("\t", ""); if (IsRegistered(playerName, Password)) { List<Claim> claims = new List<Claim> { new Claim("PlayerName", playerName), new Claim(ClaimTypes.Role, "Player"), }; var claimsIdentity = new ClaimsIdentity( claims, CookieAuthenticationDefaults.AuthenticationScheme); var authProperties = new AuthenticationProperties { IsPersistent = true }; await HttpContext.SignInAsync( CookieAuthenticationDefaults.AuthenticationScheme, new ClaimsPrincipal(claimsIdentity), authProperties ); } else { LoginResultText = "登録されていないかパスワードが不正です"; return null; } } else { await HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme); } // 自分自身へリダイレクト return Redirect("./Second"); } } |