LoginSignup
7
11

More than 3 years have passed since last update.

【Blazor入門】Blazor初心者がログインからチャット機能まで付けてデプロイしてみた ~その4~

Last updated at Posted at 2020-08-10

前回

【Blazor入門】Blazor初心者がログインからチャット機能まで付けてデプロイしてみた ~その1~
【Blazor入門】Blazor初心者がログインからチャット機能まで付けてデプロイしてみた ~その2~
【Blazor入門】Blazor初心者がログインからチャット機能まで付けてデプロイしてみた ~その3~

の続きです。

前回は、Cognitoを使用して登録されているユーザーでないとログインできないようにしました。
これだとまだ、とある問題を抱えていますが、その前に今回はログイン情報の保持をさせるようにしたいと思います。

ログイン情報の保持

多くのサービスは以下の様な処理でログイン情報を保持しているかと思います。
今回はこの方式に則って、機能追加をしていこうと思います。
image.png

出典:セキュリティ対策はばっちり? セッションとCookieの違いとPHPでの使い方をご紹介!

Blazorによるクッキーへの読み書き

結構調べてはみたんですが、今の所クッキーを扱う情報について見当たりませんでした。

BlazorではHTTPContextは扱えない(?)

.NET MVC CoreなどではHTTPContextを通すことによりセッションやクッキーを扱う事が出来ましたが、Controllerクラスからでないと扱う事が出来ません。それはBlazorでも同じなようですが、今のソリューションにControllerクラスを追加するというのはいささか変(というか、それってBlazorにとってどういう意味があるの?)と思ったので別の方法を考えてみました。

JavaScriptでクッキーを扱う

別の方法としては、JavaScriptでクッキーを扱うしかないと思いました。Blazorのよさを損なうような感じがしますがここはしょうがない。実装してみましょう。

呼び出したいJSの処理を追加する

_Host.cshtmlファイルWriteCookieメソッドReadCookieメソッドを追加します。

_Host.cshtml
<script>
    window.blazorExtensions = {
        WriteCookie: function(name, value) {
            var maxAge = "max-age=3600; ";
            var path = "path=/; ";
            document.cookie = name + "=" + value + "; " + maxAge + path;
        },

        ReadCookie: function () {
            return document.cookie;
        },
    }
</script>

ハマりポイント

ここでReadCookieメソッドはクッキー全体ではなく連想配列などで返したかったんですが、配列を生成するだけでUnHundled Errorとなってしまったので泣く泣くこうしています。配列を返す方法があれば教えてください。

C#からJavaScriptの処理を呼ぶ

新しく、ServiceフォルダCookieServiceクラスを追加しました。
というのも、クッキー操作をしたいのは特定のページだけでない可能性があると考えたからです。複数回インスタンス生成するようなクラスでもないですし、DIでどのページでも使いまわせるようにしましょう、という粋な計らいです。
image.png

コードは以下の通りです。

CookieService.cs
using System.Collections.Generic;
using System.Threading.Tasks;
using Microsoft.JSInterop;

namespace LoginTest.Service
{
    public class CookieService
    {
        #region フィールド
        private readonly IJSRuntime _jsRuntime;
        #endregion

        #region コンストラクタ
        public CookieService(IJSRuntime jsRuntime)
        {
            _jsRuntime = jsRuntime;
        }
        #endregion

        #region メソッド
        /// <summary>
        /// クッキーへの書き込み
        /// </summary>
        /// <param name="key"></param>
        /// <param name="value"></param>
        /// <returns></returns>
        public async Task WriteCookieAsync(string key, string value)
        {
            await _jsRuntime.InvokeVoidAsync("blazorExtensions.WriteCookie", key, value).ConfigureAwait(false);
        }
        /// <summary>
        /// クッキーの読み込み
        /// </summary>
        /// <returns></returns>
        public async Task<Dictionary<string, string>> ReadCookieAsync()
        {
            var cookieDictionary = new Dictionary<string, string>();
            var cookie = await _jsRuntime.InvokeAsync<string>("blazorExtensions.ReadCookie").ConfigureAwait(false);

            var cookieSplit = cookie.Split(";");
            for (var i = 0; i < cookieSplit.Length; ++i)
            {
                var cookieKeyValue = cookie.Split("=");
                cookieDictionary.TryAdd(cookieKeyValue[0], cookieKeyValue[1]);
            }

            return cookieDictionary;
        }
        #endregion
    }
}

IJSRuntimeクラスInvokeAsyncメソッドに対して、先ほど宣言したJavaScriptのメソッド名や引数を渡してあげるだけで実行ができます。かんたんかんたん。

あとはDIできるように、StartupクラスConfigureServicesメソッドCookieServiceクラスScopedで追加してあげるだけでおkです。

Startup.cs
public void ConfigureServices(IServiceCollection services)
{
    services.AddSignalR();
    services.AddRazorPages();
    services.AddServerSideBlazor();
    services.AddResponseCompression(opts =>
    {
        opts.MimeTypes = ResponseCompressionDefaults.MimeTypes.Concat(
            new[] { "application/octet-stream" });
    });
    services.AddBlazoredLocalStorage();
    services.AddScoped<CookieService>(); //★この行を追加★
}

ハマりポイント

と、実装は非常に簡単に見えますが、この処理に辿り着くまでにかなり時間が掛かりました。ハマりポイントは2点あります。

①サービスクラスへのDIはコンストラクタインジェクションしかできない
これまでは何となくInject属性を付けていればサービスに対してDI出来ていましたが、それはrazorファイルrazor.csファイルだったから、だそうです。

それについては以下のページに記載があります。
サービスでDIを使用する

なのでこのCookieServiceクラスではコンストラクタインジェクションを行ってIJSRuntimeクラスへの注入をしています。仮に、プロパティでIJSRuntimeクラスを宣言しInject属性を付けたとしてもnullのままで値は何も入ってきません。


②サービスクラスでデフォルトサービスをDIさせたい場合はデフォルトサービスの有効期間に合わせてサービスを追加する
ちょっと何を言っているか分からない人が多くなってきてそうなので丁寧めに説明します。

サービスクラスというのは、今回作ろうとしているCookieServiceクラスのことです。
デフォルトサービスというのは、以下の3つのサービスの事を指します。最初から使えるサービス達ですね。

image.png

今回、私はサーバーサイドでソリューションを作成しています。
また、デフォルトサービスであるIJSRuntimeクラスを使用したいと考えています。
上記の画像を見てみればわかりますが、サーバーサイドではIJSRuntimeクラスの有効期間は「スコープ」となっています。
なので以下の様に、CookieServiceクラスを「スコープ」で追加しています。

Startup.cs
services.AddScoped<CookieService>(); //★この行を追加★

ここを合わせる必要があると分かっていなかったので最初はシングルトンで追加していました。そうすると以下のようなエラーが出力されました。

キャプチャ.JPG

InvalidOperationException: Cannot consume scoped service 'Microsoft.JSInterop.IJSRuntime' from singleton 'LoginTest.Service.CookieService'.
(InvalidOperationException:シングルトン「LoginTest.Service.CookieService」からスコープサービス「Microsoft.JSInterop.IJSRuntime」を使用できません。)

サービスの有効期間についての説明はいかの通りです。スコープはサーバーサイドにしかない有効期間なのですね。
image.png

ログイン情報を使いまわす

クッキーやローカルストレージを利用して、ログイン情報を保持します。また。ログイン情報が保存されているのであれば自動的にチャットページへルーティングしてあげましょう。

まずはNuGetからBlazored.LocalStorageをインストールします。
image.png

このライブラリを使用することにより、非常に簡単にローカルストレージを使用することができます。ローカルストレージであればブラウザを閉じたり別タブでも保存している情報が引き継げるので今回はローカルストレージを使用します。
image.png
出典:JavaScriptのsessionStorageの使い方を現役エンジニアが解説【初心者向け】


改めて、以下の図を思い出してみましょう。
image.png

ユーザーがページに訪れた際に行うべき処理は次の通りです。

  1. ログインページへアクセス
  2. ユーザーのクッキーに保存されているセッションIDを取得
  3. クッキーのセッションIDと一致するローカルストレージに保持されているセッションIDを見つける
  4. ローカルストレージ内に保持している前回ログイン情報を取得
  5. チャットページへルーティングする

クッキーにセッションIDが保存されていなければ作成し、ログイン成功時にローカルストレージへセッションIDをキーとしてログイン情報を保持させるようにすればよい、ということになります。


HTTPContextが使えないので、セッションIDNewGuidで代用しています。
以下の処理で上記の1~5の処理が実装出来ました。

Index.razor.cs
protected override async Task OnAfterRenderAsync(bool firstRender)
{
    //IsPostbackプロパティのような感じ?
    if (firstRender)
    {
        //セッションIDを持っていなければクッキーに埋め込む
        var newSessionID = Guid.NewGuid().ToString();
        var cookie = await CookieService.ReadCookieAsync().ConfigureAwait(false);
        if (cookie == null)
        {
            await CookieService.WriteCookieAsync(SessinID, newSessionID).ConfigureAwait(false);
        }
        else
        {
            var sessionID = cookie.FirstOrDefault(x => x.Key == SessinID);
            if (sessionID.Key == null)
            {
                //セッションIDを持っていなければクッキーに埋め込む
                await CookieService.WriteCookieAsync(SessinID, newSessionID).ConfigureAwait(false);
            }
            else
            {
                //セッションIDを持っておりローカルストレージにもログイン情報がある場合
                var loginData = await LocalStorage.GetItemAsync<LoginData>(sessionID.Value).ConfigureAwait(false);
                if (loginData != null)
                {
                    Navigation.NavigateTo("Chat", false);
                }
            }
        }
    }
}

また、Validationのチェックにかからず正しいユーザーでログインできた場合にはローカルストレージへログイン情報を追加してあげれば完了です。この間にクッキーが削除されている可能性があるので、念の為にクッキーへの書き込み処理を追加しています。

Index.razor.cs
public async Task OnValidSubmit(EditContext context)
{
    Console.WriteLine($"OnValidSubmit()");

    var errorMessage = await SignInUserAsync().ConfigureAwait(false);
    if (string.IsNullOrEmpty(errorMessage))
    {
        var cookie = await CookieService.ReadCookieAsync();
        var sessionID = cookie.FirstOrDefault(x => x.Key == SessinID);
        if (sessionID.Key == null)
        {
            //セッションIDを持っていなければクッキーに埋め込む
            var newSessionID = Guid.NewGuid().ToString();
            await CookieService.WriteCookieAsync(SessinID, newSessionID).ConfigureAwait(false);
            sessionID = cookie.Single(x => x.Key == SessinID);
        }
        await LocalStorage.SetItemAsync(sessionID.Value, LoginData).ConfigureAwait(false);

        Navigation.NavigateTo("Chat", false);
    }
    else
    {
        LoginErrorMessage = errorMessage;
    }
}

実行してみる

ログイン画面がちらっと見えてしまいますが、すぐにチャットページへルーティングされることが確認できます。
Counter.gif

ログイン画面が見えてしまうのは恐らく、OnAfterRenderAsyncメソッドでルーティングしているせいだと考えています。しかし、OnBeforeRenderメソッドはありません。

ただプルリクエストがあったので、今後は使えるようになることでしょう。
OnBeforeRender #1716

まとめ

これまでの記事ではフルC#でしたが、クッキーを扱う為にJavaScriptを使うハメになってしまいました。恐らく今後は、こういったC#以外の部分はライブラリが出てくると思うのでそれに期待したいと思います。

次回は、認証をしなくてもチャットページへルーティング出来てしまう問題を解決したいと思います。

参考にさせていただいたページ

7
11
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
7
11