WebSurfer's Home

トップ > Blog 1   |   Login
Filter by APML

MVC5 でのロールによるアクセス制限

by WebSurfer 12. October 2020 15:05

.NET Framework 版の ASP.NET MVC5 アプリで、ユーザーはログインしているが、アクセス権がないコントローラーまたはアクションメソッドにアクセスした場合、以下のようなメッセージを表示する方法を書きます。(Core 3.1 版でのデフォルトです)

AccessDenied

ASP.NET Identity ベースのユーザー認証に、先の記事「ASP.NET Identity のロール管理 (MVC)」のようにしてロールを実装していることが条件です。

.NET Framework 版の ASP.NET MVC5 アプリではコントローラーやアクションメソッドに AuthorizeAttribute 属性を付与し、名前付きパラメータ Roles プロパティにアクセスを許可するロール名を指定することにより、指定されたロールに属するユーザー以外のアクセスを制限することができます。

ただし、デフォルトでは、ユーザーがログイン済みでも指定されたロールに属さない場合、ロールで制限されたページにアクセスすると Login ページにリダイレクトされます。ログイン済なのに Login ページに飛ばされるというのがユーザーフレンドリーではない感じです。

Core 3.1 版の ASP.NET MVC アプリでは、ユーザーがログイン済みだが指定された ロールに属さない場合、Login ページではなくて、上の画像のようなアクセス権がないというメッセージを表示するページにリダイレクトされます。

Core 3.1 版の方がユーザーフレンドリーのように思いますので、.NET Framework 版の MVC5 でも同様なことができないかを考えてみました。方法は以下の 2 つがありそうです。

  1. AuthorizeAttribute クラスを継承したカスタム認証フィルターを定義し、OnAuthorization メソッドを override して使う。
  2. FilterAttribute, IAuthorizationFilter を継承したカスタム認証フィルターを自作してそれを利用する。  

後者の方法で作ったカスタム認証フィルターのコードを以下にアップしておきます。(AccessDenied アクションメソッドとビューの追加も必要ですので忘れないようにしてください。追加しないと 404 エラーになります)

using System;
using System.Web;
using System.Web.Mvc;

namespace Mvc5App2.Filters
{
    public class RoleAuthFilterAttribute : 
        FilterAttribute, IAuthorizationFilter
    {
        private string role;

        public RoleAuthFilterAttribute(string role)
        {
            this.role = role;
        }
        
        public void OnAuthorization(AuthorizationContext filterContext)
        {
            if (filterContext == null)
            {
                throw new ArgumentNullException("filterContext");
            }

            if (!HttpContext.Current.Request.IsAuthenticated)
            {
                string path = "/Account/Login?returnUrl=" + 
                              HttpContext.Current.Request.RawUrl;
                filterContext.Result = new RedirectResult(path);
            }
            else 
            {
                if (!HttpContext.Current.User.IsInRole(this.role))
                {
                    filterContext.Result = 
                        new RedirectResult("/Account/AccessDenied");
                }
            }
        }
    }
}

ASP.NET Identity ベースのユーザー認証にロール管理を実装した場合、ユーザーが認証されているか否か、指定されたロールに属しているか否かはフレームワーク組み込みのプロパティ/メソッドを利用して判定することができます。上のコードの IsAuthenticated プロパティ、IsInRole メソッドがそれです。

独自認証の場合はそのあたりは自力で実装することになります。かなり大変かも。

上のコードの認証フィルターの使い方の例は下の画像の通りです。

認証フィルターの使い方

Visual Studio 2019 のテンプレートを使って作った Home/Contact アクションメソッドに認証フィルター属性を付与して Administrator 以外のアクセスを制限しています。

Tags: , ,

MVC

ASP.NET MVC の非同期プログラミング

by WebSurfer 4. October 2020 15:50

ASP.NET MVC アプリで async / await を利用した非同期プログラミングで (1) 使用されるスレッドがどのようになるか、(2) Task.Result などを使った同期コードと非同期コードを混在させるとデッドロックは起きるのか、(3) ConfigureAwait メソッドでデッドロックは回避できるのかについて書きます。(.NET Framework アプリの話です。CORE は未確認)

ASP.NET MVC の非同期プログラミング

ちなみに ASP.NET Web Forms アプリ用の HTTP ハンドラで async / await を使って非同期呼び出しをする話は先の記事「非同期 HTTP ハンドラ (2)」に書きましたので興味があればそちらを見てください。

(1) 使用されるスレッド

ASP.NET で非同期プログラミングを行う目的はスレッドプールにある限られた数のスレッドを有効利用しスループットを向上するためです。そこが UI の応答性の向上を目的としている Windows Forms のような GUI アプリと違うところです。

ASP.NET アプリでは Web サーバーがクライアントから要求を受けるとスレッドプールからスレッドを確保して要求を処置します。非同期操作をしなければ、要求を受けてから応答を返すまで最初に確保したスレッドを保持し続けます。

Web アプリでは、外部のデータベースや Web API などにアクセスしてデータを取得するということが多いと思いますが、それに時間がかかる場合は一旦使っていたスレッドはスレッドプールに戻し、データ取得後の処理はスレッドプールから新たにスレッドを取得して行うようにすればスレッドプールのスレッドの有効利用が可能です。

そのあたりの詳細は Microsoft のドキュメント「ASP.NET の非同期/待機の概要」に図解入りで説明されているので見てください。

非同期プログラミングを行うと await 前後で実際にスレッドは違うのかを ASP.NET MVC アプリで試した結果が上の画像です。そのコードは以下の通りです。

public async Task<ActionResult> AsyncTest()
{
    ViewBag.Id1 = "開始時, ID=" + 
                  Thread.CurrentThread.ManagedThreadId;

    ViewBag.Id2 = await TimeCosumingMethod();

    ViewBag.Id3 = "終了時, ID=" + 
                  Thread.CurrentThread.ManagedThreadId;

    return View();
}

private async Task<string> TimeCosumingMethod()
{
    int id = Thread.CurrentThread.ManagedThreadId;

    await Task.Delay(3000);

    return "TimeCosumingMethod の戻り値, ID(IN)=" + id + 
           " / ID(OUT)=" + Thread.CurrentThread.ManagedThreadId;
}

上の画像の ID の数字 (ManagedThreadId) を見てください。TimeCosumingMethod メソッドの await 前後で ManagedThreadId が 6 から 7 に変わっています。ちなみに、Windows Forms のような GUI アプリでは await 前後いずれも UI スレッドになり ManagedThreadId ���変わりません。

ASP.NET でも await で待機するときに現在のコンテキストがキャプチャされ、await 完了後はキャプチャしたコンテキストで続きの処理が行われるのは GUI アプリと同様だそうですが、await 前後で同じになるようにしているのはスレッドではなく HttpContext だそうです。それは仕組み上当たり前&そうせざるを得ないと思います。

(2) Task.Result でデッドロック

先の記事「await と Task.Result によるデッドロック」で書いたような Task.Result を使った同期コードと非同期コードを混在させるとデッドロックは起きるでしょうか?

その記事にも少し書きましたが、上のコードの await TimeCosumingMethod() を TimeCosumingMethod().Result に代えるとデッドロックは起きます。そのメカニズムは以下のようなことであろうと思います。

まず、TimeCosumingMethod().Result で 1 つの同期ブロックが待機中となる。呼び出された TimeCosumingMethod メソッドの await で待機する際にキャプチャされた「現在のコンテキスト」で await 完了後の同期処理を実行しようとする。しかし「現在のコンテキスト」では一度に実行するコードは 1 つのチャンクに限定されている。なので、Result プロパティでの待機が終わるまで await 完了後の同期処理は実行できない。結果デッドロックになる。

(3) ConfigureAwait でデッドロック回避

先の記事「ConfigureAwait によるデッドロックの回避」で書いたように、await 完了後の同期処理を実行するのに、await で待機する際にキャプチャした「現在のコンテキスト」ではなく、別のコンテキストで行えばデッドロックにはなりません。

ConfigureAwait メソッドの使用

以下のコードのように ConfigureAwait(false) を追加することにより、await 完了後の残り処理は、キャプチャしたコンテキストではなく、スレッドプールのコンテキストで処理されるのでデッドロックは回避でき、上の画像のとおり実行が完了します。

public async Task<ActionResult> AsyncTest()
{
    ViewBag.Id1 = "開始時, ID=" + 
                  Thread.CurrentThread.ManagedThreadId;

    ViewBag.Id2 = TimeCosumingMethod().Result;

    ViewBag.Id3 = "終了時, ID=" + 
                  Thread.CurrentThread.ManagedThreadId;

    return View();
}

private async Task<string> TimeCosumingMethod()
{
    int id = Thread.CurrentThread.ManagedThreadId;

    // ConfigureAwait(false) を追加するとデッドロックは回避できる
    await Task.Delay(3000).
        ConfigureAwait(continueOnCapturedContext: false);

    return "TimeCosumingMethod の戻り値, ID(IN)=" + id + 
           " / ID(OUT)=" + Thread.CurrentThread.ManagedThreadId;
}

ただし、スレッドは await 前後で同じになります。ということは、要求を受けた時に確保したスレッドを応答を返すまでずっと使い続けていたということで、スレッドの有効利用という ASP.NET の非同期の目的は果たせてないようです。

await 前後でスレッドが異なる場合は、await 前にキャプチャしたコンテキストを await 後でも使わないと HttpContext が渡せないが、continueOnCapturedContext: false ではそれができないので同じスレッドを使い続けざるを得ないということではないかと思います。

Tags: , , ,

MVC

SynchronizationContext とは?

by WebSurfer 30. September 2020 15:53

SynchronizationContext とは何かを調べましたので、十分とは言えないまでも取りあえず分かった(分かった気になっただけかもしれませんが)ことを備忘録として書いておきます。

SynchronizationContext

非同期プログラミングの勉強の際に、Microsoft のドキュメント「非同期プログラミングのベストプラクティス」を読んだのですが、詳しい説明なしでいきなり、

"・・・async void メソッドが開始されたときにアクティブだった SynchronizationContext で直接発生します・・・・・・未完了の Task を待機するときは、現在の "コンテキスト" がキャプチャされ、Task が完了するときのメソッドの再開に使用されます。この "コンテキスト" は現在の SynchronizationContext で・・・"

というように SynchronizationContext という言葉が出てきます。上記の他にも何か所かで出てくるのですが、非同期プログラミングを理解するのに重要なキーワードのようで、それが何かを理解してない自分には、そのドキュメントの内容が半分も理解できませんでした。

Microsoft のドキュメント「SynchronizationContext クラス」によると .NET Framework 2.0 の時代から存在していたようで、以下のように説明されています。

"同期コンテキストをさまざまな同期モデルに反映させるための基本機能を提供します。SynchronizationContext クラスは、同期なしのフリー スレッド コンテキストを提供する基本クラスです。このクラスで実装する同期モデルの目的は、共通言語ランタイムの非同期または同期の内部操作をさまざまな同期モデルで正しく動作させることです。"

残念ながらその説明では自分の頭では全く理解できません。なので、そのドキュメントからリンクが貼ってある MSDN マガジンの記事「並列コンピューティング - SynchronizationContext こそすべて」を読んでみました。

他に「async/await と SynchronizationContext」、「async/awaitと同時実行制御」、「ASP.NET の非同期でありがちな Deadlock を克服する」という記事を参考にさせていただきました。これらは Microsoft のドキュメントに比べれば少し分かりやすかったです。

上の記事を読んで、SynchronizationContext とは何かを理解する上で重要と思った点を以下にまとめておきます。無知ゆえの独断と偏見による個人的解釈も含まれているので注意してください。

  1. マルチスレッドプログラムでは、あるスレッドから別のスレッドに作業単位を受け渡す必要が生じることがよくある。SynchronizationContext クラスはそれを支援するツール。
  2. Windows Forms, WPF, ASP.NET, Silverlight, コンソールアプリなど、すべての .NET プログラムには SynchronizationContext の概念が含まれる。(公式ドキュメントを見ると .NET だけでなく Core なども適用対象に含まれているようです)  
  3. 古くはメッセージキューを使用して作業単位を受け渡していたが、.NET Framework が登場した時に汎用ソリューションとして ISynchronizeInvoke が考案され、その後 .NET Framework 2.0 で ASP.NET の非同期プログラミングをサポートするため SynchronizationContext に置き換えられた。
  4. SynchronizationContext の特徴は (1) 作業単位をコンテキストのキューにする (unit of work is queued to a context rather than a specific thread)、(2) 全てのスレッドは "現在の" コンテキストを持つ (every thread has a “current” contex)、(3) 未完了の非同期操作の数を管理する (it keeps a count of outstanding asynchronous operations)。
  5. Windows Forms, WPF, ASP.NET に使用されている SynchronizationContext はそれぞれ実装が異なっており、順に以下の通りとなる。(Current プロパティで確認できる)

    WindowsFormsSynchronizationContext
    DispatcherSynchronizationContext
    AspNetSynchronizationContext (.NET 4.5 以降)
  6. Windows Forms, WPF などの GUI アプリでは、await で待機するとき現在の SynchronizationContext がキャプチャされ、await が完了するとキャプチャした SynchronizationContext で続きを実行する。その際に使われるスレッドは await 前後で同じ、即ち UI スレッドになる。
  7. ASP.NET でも await 前後での SynchronizationContext のキャプチャと続きの実行は GUI アプリと同様だが、await 前後で HttpContext が同じになるようにしている。await 前までに使っていたスレッドはスレッドプールに戻し、await が完了後の処理はスレッドプールから新たにスレッドを取得して行う。(GUI アプリとは非同期にする目的が違うため。詳しくは「ASP.NET の非同期/待機の概要」を参照)
  8. コンソールアプリでは SynchronizationContext.Current プロパティは null になる(ということは、await で待機する際に「現在の SynchronizationContext をキャプチャ」ということはないはず)。await が完了するとき、スレッドプールを備えた既定の SynchronizationContext を使って、スレッドプールのスレッドで async メソッドの残り処理のスケジュールが設定される。(なので、「await と Task.Result によるデッドロック」に書いたようなデッドロックには陥らない)

async / await を使った非同期プログラミングではプログラマが SynchronizationContext を意識することはほとんどなさそうで、知らなくても済むような気がします。

実際、以下の画像の赤枠部分のコードのように Control.Invoke の代わりに SynchronizationContext.Post メソッドが使えるということぐらいしか使い道は思い当たりません。(自分が知らないだけという可能性は否定できませんが)

SynchronizationContext.Post

MSDN マガジンの記事「並列コンピューティング - SynchronizationContext こそすべて」にも "開発者を支援するために SynchronizationContext クラスが用意されています。残念なことに、多くの開発者はこの便利なツールに気が付いてすらいません" と書いてありましたが、そうだろうなと思いました。

ただ、async / await を使った非同期プログラミングでも内部的には SynchronizationContext が大きく関与しているのは間違いなく、上記の程度は知っておいて損はないかもしれませんね。

Tags: , ,

.NET Framework

About this blog

2010年5月にこのブログを立ち上げました。その後 ブログ2 を追加し、ここは ASP.NET 関係のトピックス、ブログ2はそれ以外のトピックスに分けました。

Calendar

<<  October 2020  >>
MoTuWeThFrSaSu
2829301234
567891011
12131415161718
19202122232425
2627282930311
2345678

View posts in large calendar