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 ファイルを取得するようになります。