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

コンマ区切りスクリプトと検証の整合

by WebSurfer 2019年11月28日 15:41

数字を 3 桁でコンマ区切りする JavaScript と ASP.NET MVC5 のクライアント側での検証の話です。

コンマ区切りスクリプトと検証の整合

元の話は Teratail のスレッド「3桁コンマ区切り数字をコンマ無しでFrom送信したい」です。

コンマ区切り用 JavaScript のコードの動作は、初期画面では数字を 3 桁でコンマ区切りし、ユーザーが編集するときはコンマを除去し、編集完了後は再び 3 桁でコンマ区切りするというものです。コードは Teratail のスレッドの私 SurferOnWww の回答にありますので見てください。

そのコンマ区切り用 JavaScript の動作と、以下のようにモデルにアノテーション属性を付与するとデフォルトで有効になる控えめな JavaScript による検証が、かなり微妙ながら基本的なところでは整合を取って動きましたので、備忘録として書いておくことにしました。(実は、バッティングして動かないと思い込んでました(汗))

public class PurchaseRecord
{
  // ・・・中略・・・

  [Display(Name = "価格")]
  [Required(ErrorMessage = "{0} は必須")]
  [RegularExpression(@"^\d{1,6}$", 
    ErrorMessage = "数字 1 ~ 6 文字")]
  [Range(100, 10000, 
    ErrorMessage = "{0}は{1}~{2}の間で入力してください。")]
  [DisplayFormat(DataFormatString = "{0:N0}", 
      ApplyFormatInEditMode = true)]
  public decimal Price { get; set; }
}

どのような動きになるかと言うと以下の通りです:

  1. テキストボックスの初期表示は 1,234
  2. ユーザーが編集動作に入る時 focus イベントが発生しスクリプトで 1234 に書き換わる
  3. ユーザーが例えば 3210 というように編集
  4. 次の作業に移るためフォーカスを外す
  5. change イベントが発生し 3210 に対し検証がかかる
  6. blur イベントが発生しスクリプトで 3,210 に書き換える
  7. ユーザーが送信ボタンをクリック
  8. submit イベントが発生しスクリプトで 3,210 を 3210 に書き換える
  9. サーバーで 3210 を受信、サーバー側での検証は OK となる。

・・・という順序になってうまくいきます。

以上、基本的な動きは OK ではあるものの、かなり微妙なところで動いていますので、実際に運用に使う場合は十分な検証が必要だと思います。思いつくのは:

(1) change ⇒ blur の順序でイベントが発生しなければならないが、全てのブラウザでそうかは不明。(メジャーなブラウザは大丈夫のようですが、昔の Forefox は反対だったという話があります)

(2) 上のステップ 3 でユーザーが数字だけ入力してくれると期待するのは無理がある。(上のサンプルでは RegularExpression 属性を追加してチェックするようにしてますが、それで十分か?)

(3) ユーザーがブラウザの JavaScript を無効にした場合はサーバー側だけで検証することになる。

・・・などです。

特に、不特定多数のユーザーが不特定多種のブラウザでアクセスしてくるインターネットに公開するような場合は別の方法(サーバー側だけで検証するとか、String 型にするとか)を考えた方が良いかもしれません。

今のところ気が付いた問題点は以下の通りです:

問題 1: ステップ 3 でユーザーが 3,210 とカンマを入れて入力すると、ステップ 5 の時点ではカンマ入りなので正規表現での検証で引っかかるという問題があります。

問題 2: コンマ区切り用 JavaScript のコードには全角 ⇒ 半角変換の機能が実装されていますが、タイミングの問題で検証に引っかかってしまいます。どういうことかと言うと、全角数字を入力するとステップ 6 の時点で半角に変換しますが、検証がかかるステップ 5 の時点ではまだ全角なので正規表現による検証で NG となります。

その後、ステップ 6 の時点で半角に変換されるので、見かけは正しく半角なのにエラーが出て混乱を招くと思います。なので、全角 ⇒ 半角変換のコードは削除した方がよさそうです。

最後にオマケを二つ書いておきます。

その 1: 上のステップ 2 で編集操作に入った時、キャレットが末尾にあるのが自然と思いますが、そうしたい場合は以下のように 2 行追加してください。

elm.addEventListener('focus',
  function () {
    this.value = delFigure(this.value);

    // キャレットを文字列の末尾に持ってくる
    // ため以下の 2 行を追加
    var len = this.value.length;
    this.setSelectionRange(len, len);
  }, false);

その 2: クライアント側での検証を無効にすると「価格」として有効でない文字列、例えば 123x とかでもサーバーに送信されてしまいます。その場合、モデルバインディングできないのでアノテーション属性に設定した検証がかかる以前にエラーとなります。

そのエラーメッセージが気に入らないので自分で設定したいという場合は Controller にコードを追加して書き換えることができます。詳しくは別の記事「int 型プロパティの検証、エラーメッセージ」を見てください。

Tags: , , ,

Validation

defer 属性つき script 定義と IE の問題

by WebSurfer 2012年12月3日 21:37

外部スクリプトファイルを定義する script 要素 に defer="defer" 属性を追加すると、あるケースで、internet explorer (IE) がそのスクリプトファイルを解析できなくなるという問題の紹介です。

defer 属性つき script 定義と IE の問題

「あるケース」というのは、div 要素などの innerHTML を書き換えることです。自分でそのようなコートを書かなくても、例えば、SWFObject を使って Flash を埋め込む場合に innerHTML の書き換えが行われます。

ただし、html コードを書く順番が問題で、defer 属性を追加した script タグが出現した後、innerHTML を書き換える場合に限ります。順番が反対の場合は問題は起こりません。

確証がないのではっきりしたことは言えませんが、自分が試した限りでは、スクリプトの取得に時間がかかる(サーバーの応答が遅い)と問題が発生する確率が高いようです。ブラウザの解析の速度も関係があるようで、IE6 であればほぼ 100% 問題が発生するのに対し、IE8 は微妙なタイミングで問題が発生したりしなかったりします。

検証のため、スクリプトをダウンロードする HTTP ハンドラを作って、Thread.Sleep メソッドを使って応答に時間がかかるようにしてみました。

<%@ WebHandler Language="C#" Class="JavaScriptHandler" %>

using System;
using System.Web;
using System.Text;
using System.Threading;
using System.Diagnostics;

public class JavaScriptHandler : IHttpHandler
{
  public void ProcessRequest (HttpContext context)
  {
    Stopwatch stopWatch = new Stopwatch();
    stopWatch.Start();

    StringBuilder sb = new StringBuilder();
    sb.Append("DateTime accessed: " 
      + DateTime.Now.ToString("d MMM yyyy HH:mm:ss zzz",
        System.Globalization.DateTimeFormatInfo.InvariantInfo)
      + ", ");
       
    string delay = context.Request.QueryString["delay"];
    int time;
    bool result = Int32.TryParse(delay, out time);
    if (result)
    {
      Thread.Sleep(time);
      sb.Append(String.Format(
        "delay time set: {0} ms", time) + ", ");
    }
    else
    {
      sb.Append("delay time set: none, ");
    }
       
    context.Response.ContentType = "text/javascript";
    context.Response.Cache.VaryByHeaders["Accept-Encoding"] = 
      true;
    context.Response.Cache.SetCacheability(
      HttpCacheability.NoCache);
    context.Response.Cache.SetExpires(
      DateTime.Now.ToUniversalTime());
    context.Response.Cache.SetMaxAge(
      new TimeSpan(0, 0, 0, 0));
    context.Response.AppendHeader("Pragma", "no-cache");

    stopWatch.Stop();
    TimeSpan ts = stopWatch.Elapsed;
    sb.Append(String.Format(
      "TimeSpan measured: {0:000} ms", ts.Milliseconds));
    string script = sb.ToString();
    script = "var msg = '" + script + "'";       
    context.Response.Write(script);
  }
 
  public bool IsReusable
  {
    get
    {
      return false;
    }
  }
}

上記の HTTP ハンドラを呼び出す際、例えば、クエリ文字列を delay=200 とすると、リクエストを受けてから約 200ms 後にアクセスした時間、クエリ文字列の設定、実際に計った時間をスクリプトとして返します。

以下のような簡単な HTML コードで試すことができます。たぶん delay はもっと少なくても問題が再現すると思います。実際に動かして試すことができるよう 実験室 にアップしました。興味のある方は試してみてください。

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
 "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">

<html xmlns="http://www.w3.org/1999/xhtml">
<head>
    <title></title>
    <script type="text/javascript" 
        src="JavaScriptHandler.ashx?delay=200" defer="defer">
    </script>
    <script type="text/javascript">
    //<![CDATA[
        function write(id){
            document.getElementById(id).innerHTML = 
                "<h1>innerHTML changed!<\/h1>";
        }
        
        function ScriptTest() {
            var x = msg;
            alert(x);
        }
    //]]>
    </script>
</head>
<body>
    <div id="myContent">
        <h1>This will be replaced by write method</h1>
    </div>
    <script type="text/javascript">
    //<![CDATA[
        write("myContent");
    //]]>
    </script>
    <br />
    <input type="button" value="Script Test" 
        id="button1" onclick="javascript:ScriptTest();" />   
</body>
</html>

自分が検証した限りでは、IE6-9 で同じ問題が出ました(IE10 は未検証)。対応策は、(1) defer="defer" 属性を使用しない、または、(2) innnerHTML を書き換えた後で defer="defer" 属性付の script タグを読むよう順序を変更する、のいずれかしかなさそうです。

Tags: , ,

JavaScript

About this blog

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

Calendar

<<  2024年4月  >>
31123456
78910111213
14151617181920
21222324252627
2829301234
567891011

View posts in large calendar