WebSurfer's Home

トップ > Blog 1   |   ログイン
APMLフィルター

Link Tag Helper と Script Tag Helper (CORE)

by WebSurfer 2021年8月9日 11:22

ASP.NET Core アプリで Content Delivery Network (CDN) から css や JavaScript のリソースを取得する際、CDN からの取得に失敗した場合にフォールバック(代替えリソース)を取得するのに便利な Link Tag Helper, Script Tag Helper があります。

その概要は Mocrosoft のドキュメント「ASP.NET Core のリンク タグ ヘルパー」と「ASP.NET Core のスクリプト タグ ヘルパー」に書いてあるのを見つけました・・・が、それを読んだだけでは理解できませんでした。(汗)

なので、実際にコードを書いて動かしてどういう動きになるのかを調べて、その結果分かったことを以下に備忘録として残しておきます。

まず、上に紹介したドキュメントのサンプルコードにある integrity 属性と crossorigin 属性とは何かを書きます。それらは ASP.NET Core の Tag Helper の機能ではなく、html に備わっている改ざん防止機能です。詳しい説明は MDN のドキュメント「サブリソース完全性」と「HTML crossorigin 属性」にあります。

ユーザーがあらかじめ CDN から取得したリソースのハッシュ値を計算してそれを integrity 属性に設定しておくと、ブラウザが CDN に要求をかけて応答として取得したリソースのハッシュ値を計算して比較し、一致しない場合はブラウザはそのリソースをロードしないという動きになるようです。それにより悪意のある第三者による改ざん攻撃のリスクを軽減するものだそうです。

integrity 属性に設定するハッシュ値の取得方法は、MDN の記事に書いてあるように、オンラインで SRI Hash Generator というサービスから取得できます。自分も試してみましたが、期待通りの結果が得られました。

crossorigin 属性の方は、MDN のドキュメントでは自分には意味不明でした。(涙) いろいろ調べてみると、サブリソース完全性に以下のように書いてあり、CORS と関係があるようです。

"サブリソース完全性の検証において、サブリソースが埋め込まれる文書のオリジン以外から提供されたリソースについては、ブラウザーはオリジン間リソース共有 (CORS) を使用してリソースに追加のチェックを行い、オリジンがリソースがリクエストしたオリジンに共有されることを許可しているかどうかを確認します。"

実際に検証してみると、integrity 属性を付与した場合は crossorigin="anonymous" 属性も一緒に付与しないと、CDN から応答が返ってきてもブラウザはそれを取り込むことはないという結果になりました。

crossorigin="anonymous" 属性を付与した場合は要求に origin ヘッダが含まれるようになります。それにより CORS によるチェックが行われ、応答ヘッダの Access-Control-Allow-Origin: * を確認してスクリプトを読み込むという動作になります。

ちなみに、crossorigin="use-credentials" の場合は要求の credential mode が 'include' になり、そのモードでは Access-Control-Allow-Origin にワイルドカード '*' は許されてないので CORS ポリシーによりブロックされスクリプトは読み込まれません。

要するに、integrity 属性にハッシュ値を設定をして改ざん防止の効果を期待するなら、同時に crossorigin="anonymous" 属性の付与も必須ということのようです。

さらに、要求に Origin ヘッダが含まる場合はサーバー側で応答ヘッダに Access-Control-Allow-Origin を含めるということも必須になります。CDN がそれに対応してないと「サブリソース完全性」による検証はできないということになるので、使用する CDN が対応しているかどうか調べる必要がありそうです。

なお、ASP.NET Core の Link Tag Helper と Script Tag Helper にとって integrity 属性と crossorigin 属性の設定は必須ではありません。無くてもフォールバック機能は動きます。

次に、ASP.NET Core の Tag Helper 独自の asp-fallback-* という属性の説明をします。名前に fallback とあるように、それらの属性はすべてフォールバックを行うためのものです。

(1) Link Tag Helper の場合

asp-fallback-href: CDN の css ファイルがロードできなかった場合のフォールバック(代替え)css ファイルの URL を指定します。

asp-fallback-test-*: CDN の css ファイルに含まれる特定のクラス名、プロパティ名とその値を指定します。例えば、以下のクラスが含まれる場合、

.sr-only {
    position: absolute;
    width: 1px;
    height: 1px;
    padding: 0;
    overflow: hidden;
    clip: rect(0, 0, 0, 0);
    white-space: nowrap;
    border: 0;
}

asp-fallback-test-* は以下のように設定します。

<link rel="stylesheet"
  href="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/4.1.3/css/bootstrap.min.css"
  asp-fallback-href="~/lib/bootstrap/dist/css/bootstrap.min.css"
  asp-fallback-test-class="sr-only" 
  asp-fallback-test-property="position"
  asp-fallback-test-value="absolute"
  crossorigin="anonymous"
  integrity="sha384-MCw98/SFnGE8fJT3GXwEOngsV7Zt27NXFoaoApmYm81iuXoPkFOJwJ8ERdknLPMO" />

上の Link Tag Helper が html にレンダリングされると以下のようになります (一部略)。

<link rel="stylesheet" 
  href="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/4.1.3/css/bootstrap.min.css"
   crossorigin="anonymous" 
  integrity="sha384-MCw98/SFnGE8fJT3GXwEOngsV7Zt27NXFoaoApmYm81iuXoPkFOJwJ8ERdknLPMO" />

<meta name="x-stylesheet-fallback-test" content="" class="sr-only" />

<script>
  !function (a,b,c,d) {
    var e, f=document,
        g = f.getElementsByTagName("SCRIPT"),
        h = g[g.length - 1].previousElementSibling,
        i = f.defaultView && f.defaultView.getComputedStyle ? 
              f.defaultView.getComputedStyle(h) : h.currentStyle;

    if ( i && i[a] !== b) 
      for (e = 0; e < c.length; e++)
        f.write('<link href="'+c[e]+'" '+d+"/>")}
  ("position","absolute",["/lib/bootstrap/dist/css/bootstrap.min.css"], 
   "rel=\u0022stylesheet\u0022 crossorigin=\u0022anonymous\u0022 ... ");
</script>

meta タグとスクリプトを見てください。それらによって CDN の css が asp-fallback-test-* に設定したクラス名、プロパティ名とその値を含んでいるかがテストされ、テスト結果 NG と判断された場合は asp-fallback-href に指定される URL のフォールバック css ファイルを取得するよう link 要素を document に書き込みます。

なお、integrity 属性を使っての検証 NG の場合は CDN から送られてきた css はロードされませんので、 テスト結果は NG と判断され、フォールバック css ファイルを取得するようになります。

(2) Script Tag Helper の場合

asp-fallback-src: CDN の js ファイルがロードできなかった場合のフォールバック(代替え)js ファイルの URL を指定します。

asp-fallback-test: CDN の js ファイルに含まれる特定の JavaScript オブジェクト名を指定します。例えば、window.jQuery という名前の JavaScript オブジェクトが含まれる場合、asp-fallback-test="window.jQuery" とします。以下の例を見てください。

<script src="https://ajax.aspnetcdn.com/ajax/jquery/jquery-3.5.1.min.js"
  asp-fallback-src="~/lib/jquery/dist/jquery.min.js"
  asp-fallback-test="window.jQuery"
  crossorigin="anonymous"
  integrity="sha384-ZvpUoO/+PpLXR1lu4jmpXWu80pZlYUAfxl5NsBMWOEPSjUn/6Z/hRTt8+pR6L4N2">
</script>

上の Script Tag Helper が html にレンダリングされると以下のようになります (一部略)。

<script src="https://ajax.aspnetcdn.com/ajax/jquery/jquery-3.5.1.min.js"
  crossorigin="anonymous" 
  integrity="sha384-ZvpUoO/+PpLXR1lu4jmpXWu80pZlYUAfxl5NsBMWOEPSjUn/6Z/hRTt8+pR6L4N2">
</script>

<script>
  (window.jQuery || 
   document.write("\u003Cscript src=\u0022/lib/jquery/dist/jquery.min.js\u0022 ..."));
</script>

asp-fallback-test に設定した window.jQuery が定義されていない場合は CDN から送られてきた js がロードできなかったということなので、document.write(...) が実行されて、asp-fallback-src に指定される URL の js ファイルを取得するよう script 要素を document に書き込みます。

Link Tag Helper の場合と同様に、integrity 属性を使っての検証 NG の場合は CDN から送られてきた js はロードされませんので、上のスクリプトで window.jQuery は未定義となり、フォールバック js ファイルを取得するようになります。

Tags: , , ,

CORE

HttpClient のキャンセルは要求の中断に相当? (CORE)

by WebSurfer 2021年7月13日 12:31

下の画像のシステムで、クライアントがブラウザを操作して要求を中断した場合、Web API でのサーバーで実行中の処理をキャンセルできるでしょうか? 自分が検証した限りではできるようです。その詳細を以下に書きます。

システム構成

クライアントがブラウザを使って MVC にアクセスすると、MVC のサーバーは HttpClient クラスを使って Web API にアクセスして必要な情報を取得し、ブラウザに応答として返すというシステムです。

クライアントが要求を送信した後待ちきれなくなって、サーバーによる処理が終わって応答が返ってくる前に要求を中断した場合、それ以上サーバーのリソースを消費しないで済むよう、サーバー側の処理を MVC でも Web API でも中断できるかがポイントです。

なお、クライアントによる要求の中断とは、下の画像の赤丸印の中のブラウザの ✕ ボタンをクリックするとか Esc キーを押す、Ajax を使っての要求の場合は XMLHttpRequest.abort() メソッドを実行することを意味します。

要求の中断

先の記事「要求の中断による処理のキャンセル (CORE)」に書きましたように、処理のキャンセルには HttpContext.RequestAborted プロパティで取得できる CancellationToken を利用します。クライアントが要求を中断すると、取得した CancellationToken がキャンセル通知を配信しますので、それをリッスンして処理の中断を行います。

ブラウザ ⇔ MVC の間は、先の記事に書いたように、MVC のアクションメソッドの引数に渡された CancellationToken によるキャンセル通知を利用して MVC のサーバー内での処理を中断できます。

その先の MVC ⇔ Web API の間は MVC のサーバーから HttpClient クラスを利用して Web API にアクセスするというシステムですが、そこがどうできるかをこの記事の下の方に載せた検証用のコードを使って調べてみました。

MVC のアクションメソッドでは次のようにします。HttpClient クラスの SendAsync メソッドや PostAsync メソッドには引数に CancellationToken を取るオーバーロードがあるので、それに HttpContext.RequestAborted プロパティで取得できる CancellationToken を渡します。そうすることで、クライアントによる要求の中断で SendAsync メソッドや PostAsync メソッドの実行をキャンセルできます。

Web API のアクションメソッドの引数にも Web API のサーバー内で HttpContext.RequestAborted プロパティで取得できる CancellationToken を渡します。そのキャンセル通知をリッスンして処理を中断します。

そうした場合、MVC のサーバー内で SendAsync メソッドや PostAsync メソッドの実行がキャンセルされると、Web API のアクションメソッドの引数に渡した CancellationToken はキャンセル通知を配信してくれるかが問題です。

下に載せたコードで検証した結果 Web API の CancellationToken もキャンセル通知を配信してくれることが分かりました。

なので、ブラウザ ⇔ MVC ⇔ Web API という構成でも、適切に CancellationToken を渡してキャンセル通知で処理の中断を行う実装をしておけば、ブラウザで要求を中断しても、MVC でも Web API でもサーバー内の処理を中断できるようです。

参考に検証に使った MVC および Web API のコードを以下に載せておきます。.NET 5.0 の ASP.NET Core アプリで MVC と Web API のプロジェクトは異なります (検証の際のホストが異なりますので、HttpContext.RequestAborted プロパティで取得できる CancellationToken は MVC と Web API で違うものになります)。

MVC(MvcCore5App4)

using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
using MvcCore5App4.Models;
using System.Diagnostics;
using System.Threading;
using System.Threading.Tasks;
using System.Net.Http;

namespace MvcCore5App4.Controllers
{
    public class HomeController : Controller
    {        
        private readonly ILogger<HomeController> _logger;
        private readonly IHttpClientFactory _clientFactory;

        public HomeController(ILogger<HomeController> logger,
                              IHttpClientFactory clientFactory)
        {
            _logger = logger;
            _clientFactory = clientFactory;
        }

        // ・・・中略・・・

        public async Task<IActionResult> Cancel(CancellationToken token)
        {
            HttpClient client = _clientFactory.CreateClient();
            var url = "https://localhost:44398/api/values";

            // GET 要求する場合はこちら
            //var request = new HttpRequestMessage(HttpMethod.Get, url);
            //HttpResponseMessage response = 
            //                await client.SendAsync(request, token);

            // POST 要求する場合はこちら
            HttpResponseMessage response =
                            await client.PostAsync(url, null, token);

            if (response.IsSuccessStatusCode)
            {
                string result = 
                    await response.Content.ReadAsStringAsync(token);
                ViewBag.Result = result;
            }

            return View();
        }
    }
}

Web API (MvcCore5App2)

using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
using System.Threading;
using System.Threading.Tasks;
using System;

namespace MvcCore5App2.Controllers
{
    [Route("api/[controller]")]
    [ApiController]
    public class ValuesController : ControllerBase
    {
        private readonly ILogger<ValuesController> _logger;

        public ValuesController(ILogger<ValuesController> logger)
        {
            _logger = logger;
        }

        [HttpGet]
        public async Task<IActionResult> Get(CancellationToken token)
        {
            _logger.LogInformation($"start: {DateTime.Now:ss.fff}");
            await Task.Delay(5000, token);
            _logger.LogInformation($"end: {DateTime.Now:ss.fff}");
            return Ok("GET 処理完了");
        }

        [HttpPost]
        public async Task<IActionResult> Post(CancellationToken token)
        {
            _logger.LogInformation($"start: {DateTime.Now:ss.fff}");
            await Task.Delay(5000, token);
            _logger.LogInformation($"end: {DateTime.Now:ss.fff}");
            return Ok("POST 処理完了");
        }
    }
}

なお、上記のように キャンセルができるのは、IIS を使ったインプロセス ホスティング モデルに限った話ですので注意してください。

インプロセス ホスティング モデル

先の記事「要求の中断による処理のキャンセル (CORE)」に書きましたように、アウトプロセス ホスティング モデルや Linux 系の OS で Nginx とか Apache をリバースプロキシに使う場合は CancellationToken のキャンセル通知を配信できませんので、サーバーでの処理の中断はできません。

それから、データベースサーバーを相手にする場合、Entity Framework で使う ToListAsync とか SaveChangesAsync などではどうなるかですが、そこはまだ調べ切れていません。走り出したらキャンセルは効かないということもあるかもしれません。今後の検討課題にしたいと思います。

Tags: , , ,

CORE

要求の中断による処理のキャンセル (CORE)

by WebSurfer 2021年7月11日 13:07

IIS を使ってのインプロセス ホスティング モデル(この記事の下の方の図参照)でホストされる ASP.NET Core Web アプリは、クライアントによる要求の中断を検出してサーバー側の処理をキャンセルすることができます。(.NET Framework 版の ASP.NET の場合は別の記事「要求の中断による処理のキャンセル (MVC5)」を見てください)

要求の中断

クライアントによる要求の中断とは、上の画像の赤丸印の中のブラウザの ✕ ボタンをクリックするとか Esc キーを押す、Ajax を使っての要求の場合は XMLHttpRequest.abort() メソッドを実行することを意味します。

処理のキャンセルには HttpContext.RequestAborted プロパティで取得できる CancellationToken を利用します。クライアントが上に書いた要求の中断操作を行うと、取得した CancellationToken は操作を取り消す通知を配信します。

キャンセル処理は基本的に先の記事「非同期タスクのキャンセル」に書いたことと同様で、以下のようになると思います。

  1. HttpContext.RequestAborted プロパティで CancellationToken を取得しキャンセルをリッスンするタスクに渡す。(CancellationToken の取得先の CancellationTokenSource の初期化等は ASP.NET Core フレームワークがやってくれるようです)  
  2. タスクにはキャンセルをリッスンして適切に処置を行うコードを実装しておく。
  3. クライアントによる要求の中断が検出されると、フレームワークは CancellationTokenSource.Cancel メソッドを呼び出し、CancellationToken を通じてリッスンしているタスクにキャンセルを通知する。
  4. キャンセル通知を受けたタスクは、あらかじめ実装されているコードに従ってキャンセル処置を行う。  

CancellationToken を渡す方法ですが、ネットで見つけた記事 Handling aborted requests in ASP.NET Core に書いてある通り、渡し先のタスクが MVC や Web API のアクションメソッドであれば引数に CancellationToken を追加しておけば、それに HttpContext.RequestAborted から取得した CancellationToken をモデルバインドしてくれます。

検証は Visual Studio 2019 を使って、以下のコードを [デバッグ(D)] ⇒ [デバッグの開始(S)] で IIS 10 Express のインプロセスホスティングで実行して行いました。検証に使用したブラウザは Edge v91.0.864.67, Chrome v91.0.4472.124, Firefox v89.0.2, IE11, Opera v77.0.4054.203 で、いずれもこの記事を書いた時点での最新版です。

using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
using MvcCore5App4.Models;
using System.Diagnostics;
using Microsoft.AspNetCore.Authorization;
using System.Threading;
using System.Threading.Tasks;
using System;
using Microsoft.AspNetCore.Identity;
using MvcCore5App4.Data;

namespace MvcCore5App4.Controllers
{
    public class HomeController : Controller
    {
        private readonly ILogger<HomeController> _logger;

        public HomeController(ILogger<HomeController> logger)
        {
            _logger = logger;
        }

        // ・・・中略・・・

        public async Task<IActionResult> Cancel(CancellationToken token)
        {
            _logger.LogInformation($"start: {DateTime.Now:ss.fff}");
            await Task.Delay(5000, token);
            _logger.LogInformation($"end: {DateTime.Now:ss.fff}");
            return View();
        }
    }
}

上のコードの Task.Delay(5000, token) では渡した token を Deley メソッドの中で継続的に観察しているようで、このメソッドが実行が開始された後でもそれから 5 秒以内ならブラウザで要求を中断すると TaskCanceledException がスローされて処理が終わります。

Entity Framework を使ってデータベースにアクセスして処置を行うときに使う ToListAsync とか SaveChangesAsync などや、HttpClient の SendAsync とか PostAsync なども同様かというのが問題と思いますが、詳しくは調べてなくて分かりません。

ToListAsync(token) メソッドで少し調べてみた限りでは、ToListAsync(token) の次の行で CancellationTokenSource.Cancel() としても完了してしまいました。Task.Delay(5000, token) と違って即終わってしまうので間に合わないのか、走り出したらキャンセルは効かないということなのかは分かりません。

ホスティングモデルによる違いですが、Visual Studio 2019 で IIS 10 Express を使ってのアウトプロセス ホスティング モデルでは要求の中断による処理のキャンセルはできませんでした。

Microsoft のドキュメント「インプロセスおよびアウトプロセス ホスティングの相違点」にインプロセス ホスティングでは "クライアントの切断が検出されます。 クライアントが切断されると、HttpContext.RequestAborted キャンセル トークンが取り消されます" と書いてあります。裏を返すとアウトプロセスホスティングではダメと言っているようです。

構成の違いは以下の図(Microsoft のドキュメントから借用)の通りですが、IIS では切断は検出されるものの Kestrel との間は HTTP 通信なので Kestrel に切断を伝えるすべがないということではないかと思います。ちなみに IIS をリバースプロキシとして使わず Kestrel をエッジサーバーとした場合はインプロセスホスティングと同様に切断は検出されキャンセルも効きます。

インプロセス ホスティング

インプロセス ホスティング モデル

アウトプロセス ホスティング

アウトプロセス ホスティング モデル

IIS を使える環境で Kestrel をエッジに使うことはなさそうですし、Linux 系の OS の場合は Nginx とか Apache をリバースプロキシに使って、Kestrel で ASP.NET Core アプリをホストする、即ち IIS を使ってのアウトプロセスホスティングと同じ構成になるので、結局は IIS を使っての インプロセスホスティングモデルでないとキャンセルはできないということではないかと思います。

Tags: , , , ,

CORE

About this blog

2010年5月にこのブログを立ち上げました。主に ASP.NET Web アプリ関係の記事です。

Calendar

<<  2024年3月  >>
252627282912
3456789
10111213141516
17181920212223
24252627282930
31123456

View posts in large calendar