20210912のC#に関する記事は17件です。

[C#] 重み付きランダム要素選択

やりたかったこと pythonのrandom.choicesのような重み付きのランダム要素選択をC#でもしたい。 車輪の再発明感がすごいが勉強になったのでヨシ! 仕様 重み(weights)の累積和を要素のコレクション(items)と紐づけ、乱数が累積和の値を超えた最初の要素を返却する仕様とした。 ポイント itemsとweightsの個数が合わない(割り当てられている重みが存在しない場合)は重みが0とみなせば当選することはないので、Zipメソッドが個数が少ない方に合わせて出力されることを利用して簡潔に書くことができる。 コード WeightedRoulette.cs using System; using System.Collections.Generic; using System.Linq; namespace YourNamespace { public class WeightedRoulette { private readonly Random _random; public WeightedRoulette(Random random = null) { _random = random ?? new Random(); } public T Roll<T>(IEnumerable<T> items, IEnumerable<double> weights) { if (items is null) return default(T); if(weights is null) return items.First(); double sum = 0d; // 累積和の計算が遅延実行されないようにToArrayする IEnumerable<double> cumlate = w.Select(v => sum += Math.Max(0d, v)).ToArray(); var roulette = items.Zip(cumlate, (s, c) => (s, c)); double value = _random.NextDouble() * sum; return roulette.First(r => r.c >= value).s; } public int Roll(IEnumerable<double> weights) => Roll(Enumerable.Range(0, weights?.Count() ?? 1), weights); } }
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

以前リファクタリングしたスクレイピングバッチアプリをAWS Lambdaへアップロードしてみた。

はじめに 以前リファクタリングしたこちらのスクレイピングバッチアプリを今回はAWS Lambdaへアップロードしてみました。 今回はその手順を備忘録として残しておこうと思います。 AWS Toolkit for Visual Studioのインストール こちらからダウンロード&インストールを行います。 既存のASP.NET Coreプロジェクトのフレームワークを.NET 5から.NET Core 3.1へ変更する 調べた感じですと、まだAWS Lambdaでは.NET 5がサポートされていないようだったので.NET Core3.1へフレームワークを変更しました。 既存のASP.NET CoreプロジェクトファイルへAWS情報を入れる。 PropertyGroup要素へ以下を追加します。 ScrapingApp.csproj <AWSProjectType>Lambda</AWSProjectType> <!-- This property makes the build directory similar to a publish directory and helps the AWS .NET Lambda Mock Test Tool find project dependencies. --> <CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies> 上記の設定後、プロジェクトを右クリックするとLambdaへの公開オプションが見れるようになります。 AWS Explorerの設定 "表示" > "AWS Explorer"を選択すると画面左側にAWS Explorerが表示されるので赤枠部分をクリックします。 アカウント情報を入力します。 ProfileNameは任意の値を入れ、Access Key IDやSecret Access Keyは任意のIAMユーザの情報を入れて"OK"ボタンを押します。 AWS Lambdaへアップロードする プロジェクトを右クリックし、"Publish to AWS Lambda..."を選択します。 選択後、以下の画面が出てくるかと思います。 それぞれの項目へ値を入れていきます。 ラベル名 説明 Package Type Lambdaへのアップロード形式 Lambda Runtime ランタイム情報 Function Name Lambda関数の名前(任意の名前を入力) Description Lambda関数の説明 Configuration Release Framework netcoreapp3.1 Assembly アセンブリ名(デフォルトではプロジェクト名になっていると思います) Type プロジェクト名.エントリーポイントのクラス名 Method エントリーポイント "Next"をクリックすると以下の画面が表示されます。 Role Nameには任意のRoleを割り当てます(ない場合は作成する必要があります。) "Memory"項目ではLambda 関数が使用できるメモリの量を設定し、"Timeout"項目ではタイムアウトさせる時間を設定します。 設定完了後、"Upload"ボタンをクリックします。 アップロードが完了するまで待ちます。 完了したらAWS Lambdaへアップロードされていることが確認できます。 おわりに 今回はAWS Lambdaへアップロードした内容のみですが、後々はAmazon EventBridgeと組み合わせて定期的に動くようにしていきたいと思います!
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Blazor WebAssembly 使いまわしが利く部品(表)を作る。

はじめに この記事ではよく使われる表を少し使いまわしが利くような部品コンポーネントを作ります。 部品コンポーネントの仕様 表のカラム名は部品コンポーネントを呼ぶ側で決めたい レイアウトは統一したい 異なるデータでも使いまわせるようにしたい 行をクリックした際の動作を部品コンポーネントを呼ぶ側で決めたい 部品コンポーネントの内容 @typeparam TItem <table class="table table-hover table-striped"> <thead class="thead-light rounded"> <tr>@this.TableHeader</tr> </thead> <tbody> @if (this.Items != null) { @foreach (var item in this.Items) { <tr @onclick="@(() => this.RowOnClick.InvokeAsync(item))">@this.RowTemplate(item)</tr> } } </tbody> </table> @code { [Parameter] public EventCallback<TItem> RowOnClick { get; set; } [Parameter] public RenderFragment TableHeader { get; set; } [Parameter] public RenderFragment<TItem> RowTemplate { get; set; } [Parameter] public IList<TItem> Items { get; set; } } @typeparam TItemと記載するとジェネリックを指定できます。 EventCallbackは呼び出し元で関数を代入出来ます。 RenderFragmentは呼び出し元で表示内容を決める事が出来ます。 使い方(コードビハインドで書いています。) razorコンポーネント @page "/fetchdata" @using Microsoft.AspNetCore.Authorization @using Microsoft.AspNetCore.Components.WebAssembly.Authentication @using Qiita.Shared @attribute [Authorize] @inherits FetchDataModel <h1>Weather forecast</h1> <p>This component demonstrates fetching data from the server.</p> @if (forecasts == null) { <p><em>Loading...</em></p> } else { <CommonTable Items="forecasts" RowOnClick="this.RowOnClick" TItem="WeatherForecast" > <TableHeader> <th>Date</th> <th>Temp. (C)</th> <th>Temp. (F)</th> <th>Summary</th> </TableHeader> <RowTemplate> <td>@context.Date.ToShortDateString()</td> <td>@context.TemperatureC</td> <td>@context.TemperatureF</td> <td>@context.Summary</td> </RowTemplate> </CommonTable> } Model using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.Components.WebAssembly.Authentication; using Qiita.Shared; using Qiita.Shared.Models; using System; using System.Collections.Generic; using System.Linq; using System.Net.Http; using System.Net.Http.Json; using System.Threading.Tasks; namespace Qiita.Client.Pages { public class FetchDataModel : ComponentBase { [Inject] protected HttpClient Http { get; set; } protected WeatherForecast[] forecasts; protected override async Task OnInitializedAsync() { try { forecasts = await Http.GetFromJsonAsync<WeatherForecast[]>("WeatherForecast"); } catch (AccessTokenNotAvailableException exception) { exception.Redirect(); } } protected void RowOnClick(WeatherForecast value) { //Rowをクリックした時の処理を部品コンポーネントを呼ぶ側で決める。 } } } 動作確認 呼び出し元でカラム名/表示するデータ/行クリック時の動作を指定して使えています。 [Blazor関連のリンク] Blazor WebAssembly プロジェクト作成(認証あり) Blazor WebAssembly 初期プロジェクト構成の入門 Blazor WebAssembly Postgresを使うまで Blazor WebAssembly コードビハインド Blazor WebAssembly InputSelectの使い方 Blazor WebAssembly 部品コンポーネントへ渡した変数とのバインド Asp.net core Identity 翻訳開始まで Blazor WebAssembly 使いまわしが利く部品(表)を作る。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

C# Enviroment.NewLineが信頼できない場合の改行コード判定

文字列に改行コードが存在する場合のみ。それ以外はEnvironment.NewLineを信じる。 class Program { public static string GetNewLine(string s) { //using System.Text.RegularExpressions; var c = Environment.NewLine; if (Regex.IsMatch(s, "\r\n")) c = "\r\n"; else if (Regex.IsMatch(s, "\r")) c = "\r"; else if (Regex.IsMatch(s, "\n")) c = "\n"; return c; } static void Main(string[] args) { var rn="aaa\r\nbbb"; var n="aaa\nbbb"; var r="aaa\rbbb"; var not="aaabbb"; var _rn=GetNewLine(rn);//\r\n var _n=GetNewLine(n);//\n var _r=GetNewLine(r);//\r var _not=GetNewLine(not);//\r\n Console.WriteLine("Hello World!"); } }
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Unity いい感じに設定したUI.Textの使い回し。

1.Extensionをセット using UnityEngine; public static class GameObjectUtils { public static GameObject Clone(this GameObject go,string newname=null) { var clone = GameObject.Instantiate( go ) as GameObject; clone.transform.parent = go.transform.parent; clone.transform.localPosition = go.transform.localPosition; clone.transform.localScale = go.transform.localScale; if(newname!=null) clone.name=newname; return clone; } } public static class ColorStringExtension { public static Color ToColor(this string self) { var color = default(Color); ColorUtility.TryParseHtmlString(self, out color); return color; } } 2.UI.Textを適当に作成。いい感じに設計。 using System.Collections; using System.Collections.Generic; using UnityEngine; using UnityEngine.UI; public class baseText : MonoBehaviour { [SerializeField]Text m_text; void Awake(){ gameObject.name="BaseText";// gameObject.transform.localPosition=Vector3.zero; m_text=gameObject.GetComponent<Text>(); m_text.fontSize=16; m_text.color="#0f0".ToColor(); } public void SetText(float x,float y,string text){ m_text.text =text; gameObject.transform.localPosition = new Vector3(x,y,0); } public void SetText(string text)=>m_text.text=text; } 3.使い回す。Canvasの直下あたりで using System.Collections; using System.Collections.Generic; using UnityEngine; public class texttest : MonoBehaviour { // Start is called before the first frame update GameObject a; GameObject b; void Start() { a=GameObject.Find("BaseText").Clone("test1"); a.GetComponent<baseText>().SetText("test1"); // b=GameObject.Find("BaseText").Clone("test2"); b.GetComponent<baseText>().SetText(0,16,"test2222222"); } // Update is called once per frame void Update() { if(Input.anyKey){ GameObject.Destroy(a); GameObject.Destroy(b); } } }
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Visual Studio 2019 v16.4以降でC#を使う場合にはinheritdocを使った方がいいという話

はじめに 筆者がVisual Studio 2019 v16.4以降でC#で開発をしていた際にinheritdocを使えることを知って、便利だなと思ったので、inheritdocの使い方について本記事ではまとめます。 そもそもinheritdocとは 基底クラス、インターフェースのメソッドからXMLコメントを継承するためのXMLタグです。 詳細はこちらのMS公式の説明を参照ください。 Visual Studio v16.4でinferitdocに対応するようになりました。 inheritdocを知らなかったときの開発上での問題点 例えば、以下のような計算機インターフェースがあるとします。 interface ICalculator { /// <summary> /// 足し算する /// </summary> /// <param name="term1">項1</param> /// <param name="term2">項2</param> /// <returns>計算結果</returns> double Add(double term1, double term2); /// <summary> /// 引き算する /// </summary> /// <param name="term1">項1</param> /// <param name="term2">項2</param> /// <returns>計算結果</returns> double Subtract(double term1, double term2); } 計算機インターフェースを実装する計算機クラスでは、AddメソッドとSubtractメソッドのコメントがインターフェースと同じなので、コピペして作成することにしたとします。 以下のような感じです。 class Calculator : ICalculator { /// <summary> /// 足し算する /// </summary> /// <param name="term1">項1</param> /// <param name="term2">項2</param> /// <returns>計算結果</returns> double Add(double term1, double term2){...} /// <summary> /// 引き算する /// </summary> /// <param name="term1">項1</param> /// <param name="term2">項2</param> /// <returns>計算結果</returns> double Subtract(double term1, double term2){...} } このようにインターフェースと実装クラスでメンバのコメントが同じである場合、インターフェースのメンバが多いほどクラス側にコメントをコピペするといったことが多くなり、コメントを付ける手間が大きくなります。また、インターフェースのコメントと実装クラスのコメントを追従させるのが大変になるという問題も起きます。 inheritdocを使うとどうなるか inheritdocを使うと計算機クラスから計算機インターフェースでメンバのコメントの同期ができます。 以下のように書くことで、計算機クラスのAdd,Subtractメソッドのコメントは、計算機インターフェースのAdd,Subtranctメソッドのコメントと同期されます。さらにinheritdocではインターフェースと実装クラスで常にメンバのコメントが同期するので、計算機インターフェースのメンバのコメントを変更した際に計算機クラスのメンバのコメントを修正する必要がありません。 class Calculator : ICalculator { /// <inheritdoc> double Add(double term1, double term2){...} /// <inheritdoc> double Subtract(double term1, double term2){...} } まとめ Visual Studio 2019 v16.4以降でC#開発時に使った方が良いinheritdocタグについてメリットを中心にまとめました。 インターフェースと実装クラス間でのメンバコメントのコピペや追従をする必要がなくなるので、積極的に使っていきたいですね。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Commandパターンの利用:1つのボタンで複数の機能を使えるようにする

目的 1つのボタン(機能を割り当てる対象であれば何でもよい)に複数の機能を割り当てることの出来るような柔軟性を持たせるためにはどのように実装すればよいかを知る。 参考 : Game Programing Patterns 要求仕様 ボタンA,B,Cがある時に、ユーザーはそれぞれのボタンにキック、パンチ、ジャンプの機能を自由に割り当てる、実行できること。 完成動作 実現方法 お試しコード Commandクラスを仮想クラスとして、Jump,Punch,Kickそれぞれのクラスが継承し、独自のメソッドを実装しコマンド動作を実現します。 ボタンA,B,Cにはユーザーが指定したコマンドをAssignButtonCommand()で割り当てます。 ButtonStringでどのボタンに、CommadStringで何のコマンドを割り当てるか指定します。 InputHandler.cs public void AssignButtonCommand(string ButtonString, string CommadString) { //C#の参照はこれでよいのか良く知らない ref Command Button = ref GetButtonInstance(ButtonString); if (IsKick(CommadString)) { Button = new KickCommand(); } else if (IsJump(CommadString)) { Button = new JumpCommand(); } else if (IsPanch(CommadString)) { Button = new PunchCommand(); } else { //TODO デフォルト割り当て 各ボタンのデフォルトがセットされるようにする Button = new KickCommand(); } } 実行ボタンが押されると、実行コマンドに選択されているコマンドのインスタンスが取得され、対応するコマンドが実行されます。 Form1.cs //実行 private void checkBox13_CheckedChanged(object sender, EventArgs e) { Command command = GetActiveButtonCommand(); label8.Text = command.Execute(); } C#は普段使わないので、文法的に間違っている可能性があるので、コアの考え方だけ Sample.cpp class Sample { //Button_Aにコマンドを割り当てる処理 //Button_Bにコマンドを割り当てる処理 //実行 void func() { if (//ボタンAが押されたら) { Button_A.execute(); } else if (//ボタンBが押されたら) { Button_B.execute(); } else { }; } }
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

C# delegateの使い方メモ

delegateを自ら使う機会はほとんどないのですが、ライブラリのソースコードを読む時にたまに遭遇して、そこで「うっ」となったりします。 このままだとよくないので、delegateを脳に染み込ませるためにも、改めてサンプルコードを書いてみました。 あくまで、個人用のメモであり、delegateとは何なのか?いつ使うのか?等の話は出てきません。ご了承ください。 サンプルコード Unityで使えるサンプルコードを書いてみました。(delegateはUnity以外でも利用できます。) using UnityEngine; namespace Practice { // デリゲートを宣言します public delegate void Printer(string s); /// <summary> /// 任意のGameObjectにアタッチしてご利用ください。 /// </summary> public class DelegatePractice : MonoBehaviour { private void Start() { // newでデリゲートを生成できます。 var printA = new Printer(Debug.Log); // 実行すると、「Hello World A」とコンソールに表示されます。 printA("Hello World A"); // newを省略することができます。この場合、varは利用できません。 Printer printB = Debug.Log; // 実行すると、「Hello World B」と表示されます。 printB("Hello World B"); // 匿名メソッドでデリゲートを生成できます。 Printer printC = delegate(string s) { Debug.Log(s); }; // 「Hello World C」と表示されます。 printC("Hello World C"); // ラムダ式でデリゲートを生成できます。JavaScript経験者なら馴染みのある書き方だと思います。 Printer printD = (string s) => { Debug.Log(s); }; // 「Hello World D」と表示されます。 printD("Hello World D"); // 複数のデリゲートを生成し、+で結合できます。 var printE = new Printer(Debug.Log) + new Printer(Debug.Log); // この場合、「Hello World E」が2回表示されます。 printE("Hello World E"); // 登録したものを-で削除できます。以下では、「Debug.Log」を2回登録してから、1回削除しています。 var printF = new Printer(Debug.Log) + new Printer(Debug.Log) - new Printer(Debug.Log); // この場合、「Hello World F」が1回表示されます。 printF("Hello World F"); // +=で後から追加したり、-=で後から削除したりできます。以下では、「Debug.Log」を2回登録してから、1回削除しています。 var printG = new Printer(Debug.Log); printG += new Printer(Debug.Log); printG -= new Printer(Debug.Log); // この場合、「Hello World G」が1回表示されます。 printG("Hello World G"); } } } こんな風に、1つの変数に複数のデリゲートを登録したり削除したりできます。 delegateという英単語のニュアンス delegate という英単語を辞書で調べると「委任」という意味が出てきます。 この委任という言葉がいまいちピンと来ないんですが、あえて委任という言葉を使って、上記のサンプルコードを説明する場合、 「DelegatePractice というクラスが、 Printer という処理を、 Debug というクラスに委任している。」 ということになると思います。(間違っていたらすみません。) さいごに 本記事作成にあたり、以下の記事を参考にさせていただきました。わかりやすい記事、ありがとうございました。 C# デリゲートについて
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Asp.net core Identity 翻訳開始まで

はじめに Asp.net core Identityを使うと、アカウントの作成やログイン/ログアウトを始め外部認証など様々な認証関係の仕組みを手っ取り早くシステムに反映させる事が出来ます。特にセキュリティ関係に詳しくない人からすると1からコーディングするよりも遥かに安全に認証の仕組みを導入出来るので非常に大きい恩恵があります。 この記事はAsp.net core Identityを使うと翻訳作業は必須になると思うので翻訳が行えるまでの記事です。(ソースコードを見れるので読み解いてカスタマイズなども行っていけるようになります) 手順 Areasフォルダを右クリックし追加を選択、新しいスキャンホールディングを選択し、表示された画面上でIDを選択します。 後は、追加を押すとどのページのコードが必要かの選択画面が出ますので対象を選択し追加することでコードが追加されます。 (開発が進んだ後にこの作業を行いエラーが発生する場合は、別の新規プロジェクトでこの作業を行い、作成されたコードをまるまる移動して追加してやれば問題なく進みました。)実体験なので念のため記載。 正常に追加すると以下のようにソースファイルが追加されますので、後は翻訳をしていく事になります。 [Blazor関連のリンク] Blazor WebAssembly プロジェクト作成(認証あり) Blazor WebAssembly 初期プロジェクト構成の入門 Blazor WebAssembly Postgresを使うまで Blazor WebAssembly コードビハインド Blazor WebAssembly InputSelectの使い方 Blazor WebAssembly 部品コンポーネントへ渡した変数とのバインド Asp.net core Identity 翻訳開始まで Blazor WebAssembly 使いまわしが利く部品(表)を作る。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Blazor WebAssembly 部品コンポーネントへ渡した変数とのバインド

はじめに この記事は部品コンポーネントへ変数を渡し、部品コンポーネントの中で渡された変数が変更された際、渡し元の変数も自動的に変更させる事を実現するまでの記事です。 準備 以下のように、ボタンを押したら渡された変数の文字列が変更されるだけの部品コンポーネントを作りました。 部品コンポーネント <h3>Component1</h3> <p>部品コンポーネント内の値 : @Test1</p> <button @onclick="ButtonOnClick">クリック</button> @code { [Parameter] public string Test1 { get; set; } private void ButtonOnClick() { Test1 = "ボタンが押されました。"; } } 呼び出し元 @page "/test" <p>呼び出し元の値 : @Test1</p> <Component1 Test1=@Test1/> @code { private string Test1 { get; set; } = "ボタン押される前の文字列"; } 動き ボタンを押しても呼び出し元の変数の値は変わっていない 呼び出し元の変数の値の変更方法 部品コンポーネント側で、バインドしたい変数のEventCallbackを追加する。この時の変数名の名前は〇〇〇Changedとする必要がある。(今回はTest1の変数をバインドしたいためTest1Changedとしている。) Test1の値が変更されるタイミングでTest1Changedを実行する事で、呼び出し元の変数も自動で変わってくれる。 また、呼び出し元もバインドしたい変数に@bind-をつける必要がある。 部品コンポーネント <h3>Component1</h3> <p>部品コンポーネント内の値 : @Test1</p> <button @onclick="ButtonOnClickAsync">クリック</button> @code { [Parameter] public string Test1 { get; set; } [Parameter] public EventCallback<string> Test1Changed { get; set; } private async Task ButtonOnClickAsync() { Test1 = "ボタンが押されました。"; await Test1Changed.InvokeAsync(Test1); } } 呼び出し元 @page "/test" <p>呼び出し元の値 : @Test1</p> <Component1 @bind-Test1=@Test1> </Component1> @code { private string Test1 { get; set; } = "ボタン押される前の文字列"; } 動き 渡し元の変数も変わっている。 [Blazor関連のリンク] Blazor WebAssembly プロジェクト作成(認証あり) Blazor WebAssembly 初期プロジェクト構成の入門 Blazor WebAssembly Postgresを使うまで Blazor WebAssembly コードビハインド Blazor WebAssembly InputSelectの使い方 Blazor WebAssembly 部品コンポーネントへ渡した変数とのバインド Asp.net core Identity 翻訳開始まで Blazor WebAssembly 使いまわしが利く部品(表)を作る。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

PowerApps/PowerAutomateのカスタムコネクタでOAuth1.0のAPIに接続する

概要 PowerApps/PowerAutomateで公式にコネクタが提供されていないサービス(API)を利用するには、カスタムコネクタを作成する必要があります。 カスタムコネクタはOAuth1.0認証には対応しておらず、どうしても接続したい場合はAzure Functionsなどの自前のAPIを介する必要がありました。 今回はカスタムコネクタだけでOAuth1.0のAPIに接続してみます。 実現方法 カスタムコネクタのアップデートにより、C#のカスタムコード が追加されましたので、この機能を利用します。 プレビュー機能につき継続的に動作する保証はありません。 C#のコードを使ってAuthorizationヘッダを生成し、リクエスト時に付加するだけです。 カスタムコネクタが呼び出されると、Context.RequestにHttpRequestMessageとしてアプリなどから受け取ったリクエスト情報が格納され、これをSendAsync(Context.Request)の形で明示的にAPI要求します。 このHttpRequestMessageに手を加えるといった仕組みです。 OAuth1.0に接続する場合、Authorizationヘッダは以下のようになります。 oauth_consumer_keyは管理ページから取得でき固定なのですが、oauth_nonceやoauth_signatureなどは接続の都度計算する必要があります。 Authorization : OAuth oauth_consumer_key="",oauth_nonce="",oauth_signature="",oauth_signature_method="HMAC-SHA1",oauth_timestamp="",oauth_token="",oauth_verifier="",oauth_version="1.0" 仕組み上、トークン情報を直接コード内に記述する必要があり誤って公開するリスクがありますので自己責任でお願いします。 準備 今時OAuth1.0しか対応していないAPIなんてそんなにないと思いますが、 今回は家計簿アプリのZaim APIを例題に実現してみます。 開発者登録 先人たちの記事を参考に開発者登録しておきます。 https://qiita.com/shutosg/items/6845057432bca551024b 以下が管理ページから取得できる情報です。 トークンの有効期限は永続的に設定します。 トークンパラメータの取得 カスタムコネクタ画面からユーザー認証画面は出せないので、事前に認証に必要なパラメータを取得しておく必要があります。 以下の5つです。 consumerKey(開発者ページから取得) consumerSecret(開発者ページから取得) token tokenSecret verifier 取得方法は以下のいずれかを使用します。 ①POSTMANを使用する場合 ②Pythonで作られている方のものを使用する場合 ③雑に作ったC#コンソールアプリで取得する場合 class Program { static RestClient _client; static string _requestTokenUrl = "https://api.zaim.net/v2/auth/request"; static string _userAuthUrl = "https://auth.zaim.net/users/auth"; static string _accessTokenUrl = "https://api.zaim.net/v2/auth/access"; static string _callbackUrl = "http://localhost:4336/"; static string _comsumerKey = "{Your Key}"; static string _comsumerSecret = "{Your Key}"; static string _accessToken; static string _tokenSecret; static string _verifier; async static Task Main(string[] args) { _client = new RestClient() { Authenticator = new OAuth1Authenticator() }; // Authentication workflow //token , token secretの取得 await GetTokens(); //ユーザー認証用URLにアクセス LaunchUserAuthorization(); Console.WriteLine("リダイレクトされたURLを入力してください。 ex." + _callbackUrl + "?oauth_token=~"); var callbackedUrl = Console.ReadLine(); //verifierの取得 GetVerifier(callbackedUrl); //取得したtokenの検証 if (await ValidateAccessToken() != true) return; var oauthParams = $@"OAuth1.0のトークン情報です。 comsumerKey : {_comsumerKey} comsumerSecret : {_comsumerSecret} accessToken : {_accessToken} tokenSecret : {_tokenSecret} verifier : {_verifier} "; Console.Write(oauthParams); Console.ReadLine(); } static async Task GetTokens() { _client.Authenticator = OAuth1Authenticator.ForRequestToken(_comsumerKey, _comsumerSecret, _callbackUrl); var request = new RestRequest(_requestTokenUrl, Method.GET); var reponse = await _client.ExecuteAsync(request); var parameters = HttpUtility.ParseQueryString(reponse.Content); if (parameters.TryGet("oauth_token", out _accessToken) == false) { throw new Exception(nameof(_accessToken) + "の取得に失敗しました"); } if (parameters.TryGet("oauth_token_secret", out _tokenSecret) == false) { throw new Exception(nameof(_tokenSecret) + "の取得に失敗しました"); } } static void LaunchUserAuthorization() { System.Diagnostics.Process.Start(_userAuthUrl + "?oauth_token=" + _accessToken); } static void GetVerifier(string url) { var uri = new Uri(url); NameValueCollection query = HttpUtility.ParseQueryString(uri.Query); if (query.TryGet("oauth_verifier", out _verifier) == false) { throw new Exception("tokenの取得に失敗しました"); } } static async Task<bool> ValidateAccessToken(){ _client.Authenticator = OAuth1Authenticator.ForAccessToken(_comsumerKey, _comsumerSecret, _accessToken, _tokenSecret, _verifier); var request = new RestRequest(_accessTokenUrl, Method.GET); var reponse = await _client.ExecuteAsync(request); if (reponse.StatusCode != HttpStatusCode.OK) { throw new Exception("Tokenの検証に失敗しました。" + reponse.Content ); } return true; } } public static class Extentions { public static bool TryGet(this NameValueCollection nameValueCollection, string keyName, out string value) { var nameValue = nameValueCollection[keyName]; if (nameValue == null) { value = null; return false; } value = nameValue; return true; } } う~ん、複雑・・・ カスタムコネクタを使って接続 カスタムコネクタの作成 こちらの記事を参考にカスタムコネクタを作成します。 https://qiita.com/Rambosan/items/7cda28bac81da794b935 認証は無しで作成。 定義画面で各アクションのエンドポイントや要求スキーマを作成しておきます。 カスタムコードに貼り付けるコード リクエスト時にAuthorizationヘッダを生成して付加するだけですので、アクション(エンドポイント)によって処理を分ける必要はありません。右から左に流すだけです。 カスタムコードはAzure Functionsのように外部ライブラリは利用できませんので、 RestSharpの実装を参考にカスタムコネクタで使えるように書きました。 業界未経験者の糞コードです。 oauth_signature_methodはHMAC-SHA1のみ対応です。 public class Script:ScriptBase { public override async Task<HttpResponseMessage> ExecuteAsync() { // Define your oauth1.0 key parameters string _consumerKey = "{YourParam}"; string _consumerSecret = "{YourParam}"; string _token = "{YourParam}"; string _tokenSecret = "{YourParam}"; string _verifier = "{YourParam}"; // Build OAuth1.0 Authorization. var auth = new OAuth1AuthorizationBuilder(this.Context.Request, _consumerKey, _consumerSecret, _token, _tokenSecret, _verifier); Context.Request.Headers.Authorization = new AuthenticationHeaderValue("OAuth", auth.ToString()); // Send request to backend api. var result = await Context.SendAsync(this.Context.Request, CancellationToken); return result; } /// <summary> /// OAuth1.0のAuthorizationを生成します。HMAC-SHA1方式固定です。 /// </summary> public class OAuth1AuthorizationBuilder { private HttpRequestMessage _requestMessage = new HttpRequestMessage(); private List<WebPair> _authParameters; private string _tokenSecret = ""; private string _consumerSecret = ""; public OAuth1AuthorizationBuilder(HttpRequestMessage requestMessage, string consumerKey, string consumerSecret, string token, string tokenSecret, string verifier) { _requestMessage = requestMessage; //必要なパラメータをListに定義 _authParameters = new List<WebPair>() { new WebPair("oauth_consumer_key",consumerKey,true), new WebPair("oauth_token",token,true), new WebPair("oauth_verifier",verifier,true), new WebPair("oauth_version","1.0"), new WebPair("oauth_signature_method","HMAC-SHA1") }; // Secret _tokenSecret = tokenSecret; _consumerSecret = consumerSecret; // timestamp var timeStamp = GetTimestamp(); _authParameters.Add(new WebPair("oauth_timestamp", timeStamp)); // nonce var nonce = GetNonce(); _authParameters.Add(new WebPair("oauth_nonce", nonce)); // Generate signature var signatureBase = ConcatenateRequestElements(_requestMessage.RequestUri, _requestMessage.Method.ToString(), _authParameters); var _signature = GetHmacSignature(new HMACSHA1(), _consumerSecret, _tokenSecret, signatureBase); _authParameters.Add(new WebPair("oauth_signature", Encode(_signature))); } public override string ToString() { var authHeaders = _authParameters.Select(x => $"{x.Name}=\"{x.Value}\""); return string.Join(",", authHeaders); } private string Encode(string value) { return Uri.EscapeDataString(value); } static readonly Random Random = new Random(); static readonly object RandomLock = new object(); private string GetNonce() { const string chars = "1234567890abcdefghijklmnopqrstuvwxyz"; var nonce = new char[16]; //英数字の中からランダムに16文字を生成する。 lock (RandomLock) { for (var i = 0; i < nonce.Length; i++) nonce[i] = chars[Random.Next(0, chars.Length)]; } return new string(nonce); } private string GetTimestamp() { var timeSpan = DateTime.UtcNow - new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc); return timeSpan.TotalSeconds.ToString(); } private string GetHmacSignature(KeyedHashAlgorithm crypto, string consumerSecret, string tokenSecret, string signatureBase) { //暗号化キーを設定 var key = $"{consumerSecret}&{tokenSecret}"; crypto.Key = Encoding.UTF8.GetBytes(key); //signatureBaseをバイト変換してハッシュを計算 var hash = crypto.ComputeHash(Encoding.UTF8.GetBytes(signatureBase)); return Convert.ToBase64String(hash); } private string ConcatenateRequestElements(Uri requestUri, string methodName, IEnumerable<WebPair> webPairs) { // method name as upper case var method = methodName.ToUpper(); // encoded uri var baseUri = Encode(requestUri.GetLeftPart(UriPartial.Path)); //var signatureParam = webPairs.Where(x => x.IsAuthHeader == true).ToList(); var signatureParam = webPairs.ToList(); // add query name-value to signatureParam var queryNameValue = HttpUtility.ParseQueryString(requestUri.Query); signatureParam.AddRange(queryNameValue.AllKeys.Select(x => new WebPair(x, queryNameValue[x]))); // sort and join signature params string parameters = Uri.EscapeDataString(string.Join("&", SortParameters(signatureParam))); return $"{method}&{baseUri}&{parameters}"; } /// <summary> /// List<WebPair>を昇順でソートします。 /// </summary> /// <param name="parameters"></param> /// <returns></returns> private IEnumerable<string> SortParameters(List<WebPair> parameters) { return parameters .Select(x => new WebPair(x.Name, x.Value, x.Encode)) .OrderBy(x => x, WebPair.Comparer) .Select(x => $"{x.Name}={x.Value}"); } class WebPair { public WebPair(string name, string value, bool encode = false) { Name = name; Value = value; WebValue = encode ? Uri.EscapeDataString(value) : value; Encode = encode; } public string Name { get; } public string Value { get; } public string WebValue { get; } public bool Encode { get; } internal static WebPairComparer Comparer { get; } = new WebPairComparer(); internal class WebPairComparer : IComparer<WebPair> { public int Compare(WebPair x, WebPair y) { var compareName = string.CompareOrdinal(x.Name, y.Name); return compareName != 0 ? compareName : string.CompareOrdinal(x.Value, y.Value); } } } } } テストと応答スキーマ ここからは通常のカスタムコネクタと同様です。 カスタムコードを保存して有効にしたら、各アクションをテストしていき、成功したら応答スキーマを定義に登録します。 PowerAppsでの利用例 カスタムコネクタが完成すれば、後は普通にPowerAppsから呼んで利用するだけです。 以下はZaimに登録した出費一覧を表示するアプリの画面です。 PowerAutomateでも同様に利用できます。 カスタムコネクタでローコードの領域にもってくれば、後はこちらのものですね。 LINEと連携して家計簿に登録とかもできそうです。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Blazor WebAssembly InputSelectの使い方

はじめに このページはInputSelectコンポーネントの使い方の説明をしています。コンボボックスなどは使用する頻度が高い部品だと思いますが、情報を探すのに苦労した記憶があります。 InputCheckboxを使う場合もほぼ同じ考えで使えます。 バインドして使うだけなら簡単だが、これだと変更イベントをキャッチ出来ない。 以下のやり方の場合は、選択した内容が即時にバインドしている変数に反映されるが、例えば変更したタイミングで別の処理を行う。など、変更のイベントを取得しようとしても出来ない。 @onchangeを使えば出来そうに感じるが、InputSelectやInputCheckboxの場合は機能しない。 razorコンポーネント側のコード @page "/fetchdata" @using Microsoft.AspNetCore.Authorization @using Microsoft.AspNetCore.Components.WebAssembly.Authentication @using Qiita.Shared @attribute [Authorize] @inherits FetchDataModel <h1>Weather forecast</h1> <p>This component demonstrates fetching data from the server.</p> @if (forecasts == null) { <p><em>Loading...</em></p> } else { <table class="table"> <thead> <tr> <th>Date</th> <th>Temp. (C)</th> <th>Temp. (F)</th> <th>Summary</th> </tr> </thead> <tbody> @foreach (var forecast in forecasts) { <tr> <td>@forecast.Date.ToShortDateString()</td> <td>@forecast.TemperatureC</td> <td>@forecast.TemperatureF</td> <td>@forecast.Summary</td> </tr> } </tbody> </table> <EditForm Model="@this.InputSelectModels"> <InputSelect @bind-Value="InputSelection"> @foreach (var item in this.InputSelectModels) { <option value="@item.Id">@item.Name</option> } </InputSelect> </EditForm> <h2>選択中ID : @InputSelection</h2> } C#側コード using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.Components.WebAssembly.Authentication; using Qiita.Shared; using Qiita.Shared.Models; using System; using System.Collections.Generic; using System.Linq; using System.Net.Http; using System.Net.Http.Json; using System.Threading.Tasks; namespace Qiita.Client.Pages { public class FetchDataModel : ComponentBase { [Inject] protected HttpClient Http { get; set; } protected WeatherForecast[] forecasts; protected List<InputSelectModel> InputSelectModels { get; set; } protected int InputSelection { get; set; } protected override async Task OnInitializedAsync() { try { forecasts = await Http.GetFromJsonAsync<WeatherForecast[]>("WeatherForecast"); this.InputSelectModels = new List<InputSelectModel>(); for(var i = 0; i < 10; i++) { this.InputSelectModels.Add(new InputSelectModel() { Id = i,Name = $"Test{i}"}); } } catch (AccessTokenNotAvailableException exception) { exception.Redirect(); } } } } InputSelectの変更イベントの取得方法 以下のようValue ValueExpression ValueChangedを実装すると変更イベントを取得出来るようになる。 この時バインドしようとしている変数の値は変わらないため入れる必要がある。 razorコンポーネント側のコード <EditForm Model="@this.InputSelectModels"> <InputSelect Value="@InputSelection" ValueExpression="@(() => InputSelection)" ValueChanged="@((int id) => InputSelectionOnChange(id))"> @foreach (var item in this.InputSelectModels) { <option value="@item.Id">@item.Name</option> } </InputSelect> </EditForm> C#側追加コード protected void InputSelectionOnChange(int id) { InputSelection = id; } [Blazor関連のリンク] Blazor WebAssembly プロジェクト作成(認証あり) Blazor WebAssembly 初期プロジェクト構成の入門 Blazor WebAssembly Postgresを使うまで Blazor WebAssembly コードビハインド Blazor WebAssembly InputSelectの使い方 Blazor WebAssembly 部品コンポーネントへ渡した変数とのバインド Asp.net core Identity 翻訳開始まで Blazor WebAssembly 使いまわしが利く部品(表)を作る。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Blazor WebAssembly コードビハインド

はじめに この記事はC#コードをrazorコンポーネント内に書かなくてもよくする方法を書いています。 個人的には小規模~中規模案件はコードビハインドを使用する選択をすると思います。いずれViewModelの記事とコードビハインドとの比較記事は書こうと思います。 FetchData.razorをコードビハインドで書く ・FetchDataModel.csファイルを新規作成 ・FetchDataModelクラスにComponentBaseを継承する。 ・FetchData.razor内の、@code{ ~ }で書かれているコードをFetchDataModelクラスにコピペ。 ・HttpClient型のHttp変数を作成し[Inject]をつける。[Inject]をつけると依存注入でNewされたインスタンスが自動でセットされる。 WeatherForecast変数をprotectedに変更 ここまでのコードは以下の通り。 using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.Components.WebAssembly.Authentication; using Qiita.Shared; using System; using System.Collections.Generic; using System.Linq; using System.Net.Http; using System.Net.Http.Json; using System.Threading.Tasks; namespace Qiita.Client.Pages { public class FetchDataModel : ComponentBase { [Inject] protected HttpClient Http { get; set; } protected WeatherForecast[] forecasts; protected override async Task OnInitializedAsync() { try { forecasts = await Http.GetFromJsonAsync<WeatherForecast[]>("WeatherForecast"); } catch (AccessTokenNotAvailableException exception) { exception.Redirect(); } } } } 現時点ではFetchData.razorコンポーネントからFetchDataModelまでの関連付けはされていない状態。 FetchData.razorに@inherits FetchDataModelを入れる。 @page "/fetchdata" @using Microsoft.AspNetCore.Authorization @using Microsoft.AspNetCore.Components.WebAssembly.Authentication @using Qiita.Shared @attribute [Authorize] @inherits FetchDataModel <h1>Weather forecast</h1> <p>This component demonstrates fetching data from the server.</p> @if (forecasts == null) { <p><em>Loading...</em></p> } else { <table class="table"> <thead> <tr> <th>Date</th> <th>Temp. (C)</th> <th>Temp. (F)</th> <th>Summary</th> </tr> </thead> <tbody> @foreach (var forecast in forecasts) { <tr> <td>@forecast.Date.ToShortDateString()</td> <td>@forecast.TemperatureC</td> <td>@forecast.TemperatureF</td> <td>@forecast.Summary</td> </tr> } </tbody> </table> } 動作確認 エラーなくforecastページにアクセス出来ればOK [Blazor関連のリンク] Blazor WebAssembly プロジェクト作成(認証あり) Blazor WebAssembly 初期プロジェクト構成の入門 Blazor WebAssembly Postgresを使うまで Blazor WebAssembly コードビハインド Blazor WebAssembly InputSelectの使い方
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[ASP.NET Core in Action, Second Edition] 18 Improving your application’s security

This chapter covers Encrypting traffic using HTTPS and configuring local SSL certificates Defending against cross-site scripting attacks Protecting from cross-site request forgery attacks Allowing calls to your API from other apps using CORS Webアプリケーションのセキュリティは現在ホットな話題だ。ほぼ毎週、新たな侵害が報告されたり、機密情報が流出したりする。状況は絶望的に見えるかもしれませんが、実際には、ほとんどの侵害は最小限の努力で回避できたはずです。 この章では、アプリケーションとアプリケーションのユーザーを攻撃者から保護するいくつかの方法について説明します。セキュリティはさまざまな方法をカバーする非常に広範なトピックであるため、この章は決して網羅的なガイドではありません。これは、アプリに対する最も一般的な脅威のいくつかとそれらに対処する方法をユーザーに認識させることを目的としており、注意しないと誤って脆弱性をもたらす可能性のある領域を強調することも目的としている。 TIP この章をお読みになった後で、セキュリティに関するその他のリソースを調べることを強くお勧めします。Open Web Application Security Project (OWASP) (www.owasp.org) は優れたリソースですが、少し味気ないこともあります。また、Troy Hunt氏はセキュリティに関する優れたコースやワークショップを用意している。NET開発者 (www.troyhunt.com/) をサポートしています。 まず、HTTPS暗号化をウェブサイトに追加して、ユーザーがインターネット上を移動するコンテンツを第三者に監視されたり変更されたりするリスクなしにアプリにアクセスできるようにする方法を検討します。これは最近の商用アプリでは事実上必須であり、ChromeやFirefoxのような最近のブラウザのメーカーによって強く奨励されている。ASP.NET Core開発証明書を使用してHTTPSをローカルで使用する方法、本番環境でHTTPS用にアプリを設定する方法、アプリ全体にHTTPSを適用する方法について説明します。 セクション18.2と18.3では、クロスサイトスクリプティング (XSS) とクロスサイトリクエストフォージェリ (CSRF) という攻撃の可能性について説明します。攻撃がどのように機能するのか、アプリ内でどのように防御できるのかを見ていきましょう。ASP.NET Coreには両方のタイプの攻撃に対する保護機能が組み込まれていますが、安全であると確信できない限り、保護機能を正しく使用し、回避しようとする誘惑に抵抗する必要があります。 第18.4項では一般的なシナリオについて説明します。JavaScript AJAX (Asynchronous JavaScript and XML) リクエストを使用して別のアプリケーションからデータを取得するアプリケーションがあるとします。デフォルトでは、Webブラウザは他のアプリケーションへのリクエストをブロックするため、これを実現するにはAPIでCross-Origin Resource Sharing (CORS) を有効にする必要があります。ここでは、CORSの仕組み、アプリのCORSポリシーの作成方法、特定のアクションメソッドへの適用方法について説明します。 この章の最後のセクションであるセクション18.5では、アプリケーションに対する一般的な脅威について説明します。これらはいずれも、攻撃者がアプリケーションに侵入するために使用する可能性のある、潜在的に重大な脆弱性を表しています。各脅威に対する解決策は、一般に比較的単純です。重要なのは、自分のアプリのどこに欠陥があるかを認識して、脆弱性を放置しないようにすることだ。 まず、HTTPSを検討し、ユーザーのブラウザとアプリ間のトラフィックを暗号化するためにHTTPSを使用すべき理由を説明します。HTTPSを使用しない場合、攻撃者はアプリに追加する多くの保護手段を無効にする可能性があるため、重要な第一歩となります。 18.1 Adding HTTPS to an application このセクションでは、HTTPSの概要と、すべての本番アプリケーションでHTTPSを認識する必要がある理由について説明します。アプリケーションにHTTPSを追加するには、アプリケーションで直接HTTPSをサポートする方法と、リバースプロキシでSSL/TLSオフロードを使用する方法の2つの方法があります。次に、開発証明書を使用してローカルマシンでHTTPSを使用する方法と、本番環境のアプリケーションにHTTPS証明書を追加する方法について説明します。最後に、セキュリティヘッダーやHTTPリダイレクションなどのベストプラクティスを使用して、アプリでHTTPSを強制する方法を学びます。 これまでこの本では、ユーザーのブラウザがHTTPプロトコルを使ってインターネット経由でアプリにリクエストを送る方法を紹介してきた。このプロトコルの詳細については、要求のタイプ (GETやPOSTなど) を記述するために動詞を使用すること、要求に関するメタデータを含むヘッダーが含まれること、オプションでデータの本文ペイロードが含まれることを確立する以外は、あまり調べていません。 デフォルトでは、HTTP要求は暗号化されません。インターネット経由で送信されるプレーンテキストファイルです。ユーザーと同じネットワーク上のすべてのユーザー (喫茶店で同じ公共Wi-Fiを使用しているユーザーなど) が、送受信される要求と応答を読み取ることができます。攻撃者は、送信中のリクエストやレスポンスを変更することもできます。 このように暗号化されていないWebアプリケーションを使用すると、ユーザーにプライバシーとセキュリティの両方のリスクが生じます。攻撃者は、フォームで送信されアプリから返されたデータを読み取ったり、攻撃ユーザーへの応答に悪意のあるコードを挿入したり、認証Cookieを盗んでアプリ上のユーザーになりすますことができます。 ユーザーを保護するために、アプリはユーザーのブラウザとアプリ間のトラフィックをHTTPSプロトコルを使って暗号化する必要があります。これはHTTPトラフィックに似ていますが、SSL/TLS 1証明書を使用して要求と応答を暗号化するため、攻撃者は内容を読み取ったり変更したりできません。ブラウザでは、図18.1に示すように、サイトがHTTPSを使用していることがURLのhttps ://プレフィックス ( 「s」 に注意) によって、または場合によっては南京錠によってわかります。 TIP SSL/TLSプロトコルの動作の詳細については、David Wong氏(マニング (2021))のReal-World Cryptography, http://mng.bz/zxz1の第9章を参照してください。 現実には、今日では常にHTTPSを使用して本番用のWebサイトを提供する必要があります。業界では、デフォルトでHTTPSへの移行が進められており、ほとんどのブラウザはHTTPサイトを明示的に 「安全でない」 とマークするようになっています。HTTPSをスキップすることは、長い目で見ればアプリの認知度を下げることになるので、セキュリティ上のメリットに興味がなくても、HTTPSを設定することはあなたにとって最善のことです。 HTTPSをイネーブルにするには、サーバのTLS証明書を取得して設定する必要があります。残念ながら、そのプロセスは以前よりずっと簡単になり、Let’s Encrypt (https://letsencrypt.org/),のおかげで実質的に無料になったが、多くの場合はまだ簡単ではない。本番サーバをセットアップする場合は、Let's Encryptサイトのチュートリアルを注意深く読むことをお勧めします。間違えるのは簡単ですので、ごゆっくりどうぞ。 TIP クラウドでアプリをホスティングしている場合、ほとんどのプロバイダーはワンクリックでTLS証明書を提供するので、証明書を自分で管理する必要はありません。これは非常に便利で、みんなにとてもお勧めです。 ASP.NET Coreアプリケーション開発者は、図18.2に示すように、リバースプロキシのアーキテクチャを利用してSSL/TLSオフロード/ターミネーションと呼ばれるプロセスを実行することで、アプリケーションでHTTPSを直接サポートせずに済むことが多い。アプリケーションがHTTPSを使用してリクエストを直接処理するのではなく、HTTPを引き続き使用します。リバース・プロキシは、ブラウザへのHTTPSトラフィックの暗号化および復号化を担当します。これにより、ユーザーのブラウザとサーバの間でデータが暗号化されるため、アプリケーションで証明書を設定する必要がなくなります。 アプリをホスティングしている特定のインフラストラクチャに応じて、SSL/TLSはネットワーク上の専用デバイス、Cloudflareのようなサードパーティのサービス、または同じサーバーや別のサーバーで実行されているリバースプロキシ(IIS、NGINX、HAProxyなど)にオフロードされます。 それでも、状況によってはSSL/TLSをアプリで直接処理する必要があるかもしれない。 Kestrelをインターネットに直接公開していて、リバースプロキシがない場合。ASP.NET Core 3.0では、Kestrelサーバが強化されたため、より一般的になった。ローカルでアプリを開発しているときにもよくあることだ。 リバースプロキシとアプリの間にHTTPがあることは受け入れられない。ネットワーク内のトラフィックをセキュリティで保護することは、外部トラフィックと比較して重要度は低くなりますが、内部トラフィックにHTTPSを使用する方が確実に安全です。 HTTPSを必要とするテクノロジを使用している場合。gRPCやHTTP/2などの新しいネットワークプロトコルでは、HTTPS接続が必要です。 これらの各シナリオでは、KestrelがHTTPSトラフィックを受信できるように、アプリケーションのTLS証明書を設定する必要があります。セクション18.1.1では、ローカルでの開発時にHTTPSを使用する最も簡単な方法を説明し、セクション18.1.2では、アプリケーションを本稼働用に設定する方法を説明します。 18.1.1 Using the ASP.NET Core HTTPS development certificates HTTPS証明書を扱うのは以前よりも簡単になったが、残念ながら、特にWebに慣れていない人にとっては、まだややこしい話題になる可能性がある。.NET SDK、Visual Studio、およびIIS Expressは、多くの面倒な作業をユーザーに代わって処理することによって、このエクスペリエンスを向上させようとしています。 .NET SDKを使用してdotnetコマンドを初めて実行すると、SDKによってHTTPS開発証明書がマシンにインストールされます。デフォルトテンプレートを使用して作成したASP.NET Coreアプリケーション (または証明書を明示的に設定しないアプリケーション) は、この開発証明書を使用してHTTPSトラフィックを処理します。ただし、開発証明書はデフォルトでは信頼されません。つまり、最初に.NET SDKをインストールした後でサイトにアクセスすると、図18.3に示すようなブラウザの警告が表示されます。 A brief primer on certificates and signing HTTPSは、データ暗号化プロセスの一部として公開キー暗号化を使用します。これには2つのキーが使用されます。1つは誰でも見ることができる公開キーで、もう1つはサーバーだけが見ることができる秘密キーです。公開鍵で暗号化されたものはすべて、秘密鍵でのみ復号化できます。これにより、ブラウザはサーバの公開キーを使用して何かを暗号化でき、サーバだけがそれを復号化できます。完全なTLS証明書は、パブリック部分とプライベート部分の両方で構成されます。 ブラウザがアプリに接続すると、サーバーはTLS証明書の公開キー部分を送信します。しかし、証明書を送信したのが間違いなくあなたのサーバーであることを、ブラウザーはどのようにして知るのでしょうか。これを実現するために、TLS証明書には、サードパーティの証明書、認証局 (CA) を含む追加の証明書が含まれています。この信頼できる証明書をルート証明書と呼びます。 CAは特別な信頼されたエンティティであり、ブラウザは特定のルート証明書を信頼するようにハードコードされています。アプリのTLS証明書を信頼するには、信頼されたルート証明書が含まれている (または署名されている) 必要があります。 ASP.NET Core開発証明書を使用する場合、または独自の自己署名証明書を作成する場合、サイトのHTTPSには信頼されたルート証明書がありません。これは、ブラウザが証明書を信頼せず、デフォルトでサーバに接続しないことを意味します。これを回避するには、開発マシンに証明書を明示的に信頼するように指示する必要があります。 本番環境では、開発証明書や自己署名証明書は使用できません。これは、ユーザーのブラウザが信頼できないためです。Let's Encryptなどのサービスや、AWS、Azure、Cloudflareなどのクラウドプロバイダーから、署名付きHTTPS証明書を入手する必要がある。これらの証明書は信頼できるCAによってすでに署名されているため、ブラウザによって自動的に信頼されます。 これらのブラウザの警告を解決するには、証明書を信頼する必要があります。証明書の信頼は機密性の高い操作です。「この証明書が正しくないことはわかっていますが、無視してください。」と表示されるため、自動的に実行するのは困難です。WindowsまたはmacOSを実行している場合は、dotnet dev-certs https--trust を実行して開発証明書を信頼できます。 このコマンドは、オペレーティング・システムの 「証明書ストア」 に登録することで証明書を信頼します。このコマンドを実行すると、図18.4に示すように、警告や 「保護されていない」 ラベルが表示されずにWebサイトにアクセスできるようになります。 TIP 証明書を信頼してブラウザのキャッシュをクリアした後で、ブラウザを閉じる必要がある場合があります。 開発者証明書はWindowsとmacOSでスムーズに動作しますが、Linuxで証明書を信頼するのは少し難しく、使用している特定の種類に依存します。さらに、Linux上のソフトウェアは独自の証明書ストアを使用することが多いため、通常はお気に入りのブラウザに証明書を直接追加する必要があります。お好みのブラウザのドキュメントを見て、最適な方法を見つけることをお勧めします。Dockerなどその他のプラットフォームに関するアドバイスについては、Microsoftの「ASP.NET CoreでHTTPSを強制する」ドキュメントの“How to set up a developer certificate for Docker”のセクションを参照してください:http://mng.bz/0mBJ。 開発にWindows、Visual Studio、およびIIS Expressを使用している場合、開発証明書を信頼する必要がない場合があります。IIS Expressは、ローカルでの開発時にリバースプロキシとして機能するため、SSL/TLSセットアップ自体を処理します。さらに、Visual Studioはインストールの一部としてIIS開発証明書を信頼する必要があるため、ブラウザの警告がまったく表示されない可能性がある。 ASP.NET CoreとIISの開発証明書は、HTTPSでKestrelをローカルに使うのを容易にするが、それらの証明書は、本番環境に移行した後は役に立たない。次のセクションでは、プロダクションTLS証明書を使用するようにKestrelを設定する方法を説明します。 18.1.2 Configuring Kestrel with a production HTTPS certificate 本番用のTLS証明書を作成するには、証明書を作成するドメインを所有していることをサードパーティの認証局 (CA) に証明する必要があるため、多くの場合、骨の折れる作業になります。これは 「信頼」 プロセスにおける重要なステップであり、攻撃者がサーバを偽装できないようにします。このプロセスの結果、1つ以上のファイルが生成されます。これは、アプリケーション用に設定する必要があるHTTPS証明書です。 TIP 証明書の取得方法の詳細は、プロバイダーおよびOSプラットフォームによって異なるため、プロバイダーのマニュアルに注意して従ってください。このプロセスの気まぐれさと複雑さが、前述したSSL/TLSオフロードまたは 「ワンクリック」 アプローチを強く支持する理由の1つです。これらのアプローチは、私のアプリが証明書を処理する必要がなく、このセクションで説明したアプローチを使用する必要がないことを意味します。その責任は、ネットワークの別の部分、または基盤となるプラットフォームに委任します。 証明書を取得したら、HTTPSトラフィックの処理に使用するようにKestrelを設定する必要があります。第16章では、ASPNETCORE_URLS環境変数またはコマンドラインを使用して、アプリケーションがリッスンするポートを設定する方法を説明し、HTTPS URLを指定できることを説明しました。証明書の構成を指定しなかったため、Kestrelはデフォルトで開発証明書を使用しました。本番環境では、どの証明書を使用するかをKestrelに伝える必要があります。 Kestrelは非常に設定しやすく、複数の方法で証明書を設定できます。ポートごとに異なる証明書を使用することも、.pfxファイルまたはOS証明書ストアからロードすることも、公開するURLエンドポイントごとに異なる設定を使用することもできます。詳細については、次のURLにあるMicrosoft社のマニュアルの「ASP.NET CoreでのKestrel Webサーバの実装」の項を参照してください。http://mng.bz/KMdX 次のリストは、HTTPS接続にKestrelが使用するデフォルトの証明書を設定することで、本稼働アプリケーション用のカスタムHTTPS証明書を設定する1つの方法を示しています。appsettings.jsonファイル(または11章で説明している他の設定ソースを使用して)に「Kestrel:証明書:デフォルト」セクションを追加して、使用する証明書の.pfxファイルを定義できます。証明書にアクセスするためのパスワードも指定する必要があります。 Listing 18.1 Configuring the default HTTPS certificate for Kestrel using a .pfx file { "Kestrel": { ❶ "Certificates": { ❶ "Default": { ❶ "Path": "localhost.pfx", ❷ "Password": "testpassword" ❸ } } } } ❶ Create a configuration section at Kestrel:Certificates:Default. ❷ The relative or absolute path to the certificate ❸ The password for opening the certificate 前述の例は、Kestrelのデフォルトを変更する必要がないため、HTTPS証明書を置き換える最も簡単な方法です。同様の方法を使用して、前述の 「エンドポイント設定」 のドキュメント (http://mng.bz/KMdX) に示すように、OS証明書ストア (WindowsまたはmacOS) からHTTPS証明書をロードできます。 WARNING リスト18.1では、簡単にするために証明書のファイル名とパスワードをハードコードしていますが、第11章で説明したように、これらのファイルをユーザー・シークレットのような構成ストアからロードするか、ローカル・ストアから証明書をロードする必要があります。appsettings.jsonファイルに本番パスワードを入れないでください。 すべてのデフォルトのASP.NET Coreテンプレートは、HTTPとHTTPSの両方のトラフィックを処理するようにアプリケーションを構成し、これまで見てきた構成では、アプリケーションが開発と運用の両方でHTTPとHTTPSの両方を処理できることを保証できる。 ただし、HTTPとHTTPSのどちらを使用するかは、ユーザーがアプリケーションを最初に参照したときにクリックするURLに依存する場合があります。たとえば、アプリケーションがデフォルトURL (HTTPトラフィックの場合はhttp://localhost:5000、HTTPSトラフィックの場合はhttps://localhost:5001) を使用してリスニングする場合、ユーザーがHTTP URLに移動すると、そのトラフィックは暗号化されません。せっかくHTTPSを設定したのだから、ユーザーに強制的に使用させるのがベストだろう。 18.1.3 Enforcing HTTPS for your whole app 最近では、ウェブサイト全体でHTTPSを強制することが現実的に求められている。ブラウザがHTTPページをセキュアでないものとして明示的にラベル付けし始めている;セキュリティ上の理由から、機密性の高いデータをインターネット経由で送信する場合は常にTLSを使用する必要があります。また、HTTP/2のおかげでTLSを追加することで、アプリケーションのパフォーマンスを向上させることができます。4 アプリケーションにHTTPSを適用するには、複数の方法があります。SSL/TLSオフロードでリバースプロキシを使用している場合は、アプリケーション内で心配することなく、いずれにしても処理される可能性があります。それにもかかわらず、リバースプロキシが何をしているかにかかわらず、あなたのアプリケーションでSSL/TLSを強制しても害はありません。 NOTE Razor Pagesアプリではなく、Web APIを構築している場合、このセクションで説明する方法を使用せずにHTTPリクエストを拒否するのが一般的です。これらの保護は、主にブラウザで使用するアプリを構築する際に適用される。詳細については、Microsoft 「ASP.NET CoreでHTTPSを強制する」のドキュメントhttp://mng.bz/j46aを参照してください。 アプリのセキュリティを向上させる1つの方法は、HTTPセキュリティヘッダーを使用することです。これらはHTTPレスポンスの一部として送信されるHTTPヘッダーで、ブラウザーに動作を指示します。さまざまなヘッダが用意されており、そのほとんどはアプリがセキュリティを強化する代わりに使用できる機能を制限する。5次の章では、カスタム・ミドルウェアを作成して独自のカスタム・ヘッダーをHTTPレスポンスに追加する方法を説明します。 これらのセキュリティヘッダーの1つであるHTTP Strict Transport Security (HSTS) ヘッダーは、ブラウザがデフォルトのHTTPではなくHTTPSを使用できるようにするのに役立ちます。 ENFORCING HTTPS WITH HTTP STRICT TRANSPORT SECURITY HEADERS 残念ながら、特に指定がない限り、デフォルトではブラウザは常にHTTP経由でアプリをロードする。つまり、HTTP経由のトラフィックを処理したくない場合でも、アプリケーションは通常、HTTPとHTTPSの両方をサポートする必要があります。この問題を緩和する1つの方法 (およびセキュリティのベストプラクティス) は、HTTP Strict Transport Securityヘッダーを応答に追加することです。 DEFINITION HTTP Strict Transport Security (HSTS) は、アプリケーションに対するそれ以降のすべてのリクエストにHTTPSを使用するようブラウザに指示するヘッダーです。ブラウザはアプリにHTTPリクエストを送信せず、代わりにHTTPSのみを使用します。HTTPS要求への応答でのみ送信できます。これはブラウザから発信された要求にのみ関連し、サーバ間の通信には影響しません。 本番アプリケーションでは、HSTSヘッダーを使用することを強くお勧めします。一般的には、ローカル開発でそれらを有効にすることは望ましくありません。同様に、HTTPSを使用する予定のサイトでのみHSTSを使用するようにしてください。HSTSを使用してHTTPSを強制すると、HTTPSを無効にすることが困難 (場合によっては不可能) になります。 ASP.NET Coreには、HSTSヘッダーを設定するためのミドルウェアが組み込まれており、これは一部のデフォルトテンプレートに自動的に含まれている。次のリストは、Startup.csでHstsMiddlewareを使用して、アプリケーションのHSTSヘッダーを構成する方法を示しています。 Listing 18.2 Using HstsMiddleware to add HSTS headers to an application public class Startup { public void ConfigureServices(IServiceCollection services) { services.AddRazorPages(); services.AddHsts(options => ❶ { ❶ options.MaxAge = TimeSpan.FromHours(1); ❶ }); ❶ } public void Configure(IApplicationBuilder app, IWebHostEnvironment env) { if (env.IsProduction()) ❷ { app.UseHsts(); ❸ } app.UseStaticFiles(); ❹ app.UseRouting(); ❹ app.UseAuthorization(); ❹ app.UseEndpoints(endpoints => ❹ { ❹ endpoints.MapRazorPages(); ❹ }); ❹ } } ❶ Configure your HSTS header settings. This changes the MaxAge from the default of 30 days. ❷ You shouldn’t use HSTS in local environments. ❸ Adds the HstsMiddleware ❹ The HstsMiddleware should be very early in the middleware pipeline. TIP 前述の例は、HSTSヘッダーで送信されるMaxAgeを変更する方法を示しています。最初は小さな値から始めることをお勧めします。アプリのHTTPSが正しく機能していることを確認したら、セキュリティを強化するために使用期間を増やします。HSTSの詳細については、Scott Helmeの記事 「HSTS—The missing link in Transport Layer Security」 (https://scotthelme.co.uk/hsts-the-missing-link-in-tls/) を参照してください。 HSTSは、ユーザーにウェブサイトでHTTPSの使用を強制するための優れたオプションです。ただし、ヘッダーの1つの問題は、HTTPS要求にしか追加できないことです。つまり、最初のリクエストがHTTPの場合、HSTSヘッダーは送信されず、HTTPのままになります。残念ながら、安全でないリクエストをすぐにHTTPSにリダイレクトすることで、これを緩和することができる。 REDIRECTING FROM HTTP TO HTTPS WITH THE HTTPS REDIRECTION MIDDLEWARE HstsMiddlewareは通常、すべてのHTTP要求をHTTPSにリダイレクトするミドルウェアと一緒に使用します。 TIP 特定のRazorページなど、アプリケーションの一部にのみHTTPSリダイレクトを適用することは可能ですが、アプリケーションにセキュリティホールを開くのは簡単すぎるので、お勧めしません。 ASP.NET CoreにはHttpsRedirectionMiddlewareが付属しており、これを使用してアプリケーション全体にHTTPSを適用できます。これは、 「≪起動|Startup|emdw≫」 の 「≪構成|Configure|emdw≫」 セクションでミドルウェア・パイプラインに追加すると、それを通過するすべてのリクエストが保護されます。HTTPリクエストがHttpsRedirectionMiddlewareに到達すると、ミドルウェアはただちにリクエストのHTTPSバージョンへのリダイレクトでパイプラインをショートします。その後、ブラウザはHTTPではなくHTTPSを使用してリクエストを繰り返します。 NOTE あなた方の中の鋭い目は、HSTSとリダイレクションミドルウェアをもってしても、内在的な弱点があることに気付くだろう。デフォルトでは、ブラウザは常に最初のセキュアでないリクエストをHTTP経由でアプリケーションに送信します。これを回避する唯一の方法は、ブラウザに常にHTTPSを使用するように指示するHSTSプリロードです。プリロードを含むHSTSの優れたガイドは、ForwardPMXのサイトで提供されています:Chris Herbrand, http://mng.bz/Wdmgの 「The Ultimate Guide to HSTS Protocol」 。 HttpsRedirectionMiddlewareがデフォルトのASP.NET Coreテンプレートに追加されます。通常は、次のリストに示すように、エラー処理とHstsMiddlewareの後に配置されます。デフォルトでは、ミドルウェアはHTTP 307 Temporary Redirectステータスコードを使用して、すべてのHTTP要求をセキュアなエンドポイントにリダイレクトします。 Listing 18.3 Using HttpsRedirectionMiddleware to enforce HTTPS for an application public void Configure(IApplicationBuilder app, IWebHostEnvironment env) { app.UseExceptionHandler("/Error"); if (env.IsProduction()) { app.UseHsts(); } app.UseHttpsRedirection(); ❶ app.UseStaticFiles(); app.UseRouting(); app.UseAuthorization(); app.UseEndpoints(endpoints => { endpoints.MapRazorPages(); }); } } ❶ Adds the HttpsRedirectionMiddleware to the pipeline. Redirects all HTTP requests to HTTPS. HttpsRedirectionMiddlewareは、アプリケーションに対して最初に構成されたHTTPSエンドポイントにHTTPリクエストを自動的にリダイレクトします。アプリケーションがHTTPS用に設定されていない場合、ミドルウェアはリダイレクトせず、代わりに警告をログに記録します。 warn: Microsoft.AspNetCore.HttpsPolicy.HttpsRedirectionMiddleware[3] Failed to determine the https port for redirect. Kestrelが認識しているポートとは異なるポートにミドルウェアをリダイレクトしたい場合は、ASPNETCORE_HTTPS_PORT環境変数を設定してください。これは、リバースプロキシを使用している場合に必要になることがあります。Microsoftの「ASP.NET CoreでHTTPSを強制する」ドキュメントhttp://mng.bz/QmN4で説明されているように、別の方法で設定することもできます。 SSL/TLS offloading, header forwarding, and detecting secure requests 18.1章の最初で、私はリバースプロキシでHTTPSリクエストを終了させることを検討するよう勧めました。これにより、ユーザーはHTTPSを使用してリバースプロキシと通信し、リバースプロキシはHTTPを使用してアプリと通信します。この設定では、ユーザーは保護されますが、アプリ自体はTLS証明書を処理する必要はありません。 HttpsRedirectionMiddlewareが正しく動作するためには、Kestrelはリバースプロキシが受信した元のリクエストがHTTP経由かHTTPS経由かを知る何らかの方法が必要です。リバースプロキシはHTTP経由でアプリと通信するので、Kestrelは特別な支援なしにそれを理解することはできません。 ほとんどのリバースプロキシ(IIS、NGINX、HAProxyなど)で使用される標準的なアプローチは、リクエストをアプリに転送する前に、リクエストにヘッダーを追加することです。具体的には、X-Forwarded-Protoというヘッダーが追加され、元のリクエストプロトコルがHTTPであったかHTTPSであったかが示されます。 ASP.NET CoreにはForwardedHeadersMiddlewareが含まれており、このヘッダ (と他のヘッダ) を探してそれに応じてリクエストを更新するので、アプリはHTTPSでセキュリティ保護されたリクエストをあらゆる目的でセキュアとして扱うことができる。 UseIisIntegration () 拡張と共にIISを使用している場合は、ヘッダー転送が自動的に処理されます。NGINXやHAProxyなどの別のリバースプロキシを使用している場合は、16章で説明したように、環境変数ASPNETCORE_FORWARDEDHEADERS_ENABLED=trueを設定してミドルウェアを有効にできます。また、セクション16.3.2に示すように、ミドルウェアを手動でアプリケーションに追加することもできます。 リバース・プロキシーが要求を転送すると、ForwardedHeadersMiddlewareはX-Forwarded-Protoヘッダーを探し、必要に応じて要求の詳細を更新します。後続のすべてのミドルウェアでは、リクエストはセキュアであるとみなされます。ミドルウェアを手動で追加する場合は、転送されたヘッダーが読み取られ、リクエストが適切に保護された状態でマークされるように、UseHsts () またはUseHttpsRedirection () を呼び出す前にForwardedHeadersMiddlewareを配置することが重要です。 HTTPSは、最近のアプリケーションにセキュリティを追加するための最も基本的な要件の1つです。最初に設定するのは難しいかもしれませんが、一度設定して実行してしまえば、特にリバースプロキシでSSL/TLS終端を使用している場合には、その設定はほとんど忘れてしまいます。 残念なことに、他のほとんどのセキュリティ対策では、アプリの成長や開発に伴って誤って脆弱性を持ち込むことがないように、かなりの警戒が必要です。多くの攻撃は概念的にシンプルであり、何年も前から知られていますが、新しいアプリケーションでは依然として一般的に見られます。次のセクションでは、そのような攻撃の1つを取り上げ、Razor Pagesを使ってアプリを構築する際にどのように防御するかを見ていく。 18.2 Defending against cross-site scripting (XSS) attacks このセクションでは、クロスサイト・スクリプティング攻撃と、攻撃者がそれらを使用してユーザーを攻撃する方法について説明します。ここでは、Razor Pagesフレームワークがこれらの攻撃からユーザーを保護する方法、必要なときに保護を無効にする方法、および注意事項について説明します。また、HTMLエンコーディングとJavaScriptエンコーディングの違い、および間違ったエンコーダーを使用した場合の影響についても説明します。 攻撃者は、アプリの脆弱性を不正利用して、別のユーザーのブラウザでコードを実行するクロスサイトスクリプティング (XSS) 攻撃を作成する可能性があります。6一般的に、攻撃者は入力フォームなどの正当なアプローチを使用してコンテンツを送信します。攻撃者は、悪意のある入力を慎重に作成することで、ユーザーのブラウザー上で任意のJavaScriptを実行し、Cookieを盗み出したり、ユーザーになりすますことができます。 図18.5に、XSS攻撃の基本的な例を示します。アプリの正規ユーザーは、フォームを送信することで自分の名前をアプリに送信できます。次に、この名前を内部リストに追加し、リスト全体をページにレンダリングします。名前が安全に表示されない場合、リストを表示する他のすべてのユーザーのブラウザで、悪意のあるユーザーがJavaScriptを実行できます。 図18.5では、ユーザーが名前などのHTMLのスニペットを入力しています。ユーザーが名前のリストを表示すると、Razorテンプレートは@Html.Raw()を使用して名前をレンダリングし、scriptタグをドキュメントに直接書き込みます。ユーザーの入力は、ページのHTML構造の一部になりました。ページがユーザのブラウザにロードされるとすぐに、scriptタグが実行され、ユーザは危険にさらされます。攻撃者がユーザーのブラウザ上で任意のJavaScriptを実行できるようになれば、ほとんど何でもできるようになります。 この脆弱性は、ユーザ入力を安全でない方法でレンダリングすることが原因です。データがレンダリング前に安全になるようにエンコードされていない場合は、ユーザーを攻撃にさらす可能性があります。デフォルトでは、タグ・ヘルパー、HTMLヘルパー、または@構文を使用して記述されたデータをHTMLエンコードすることで、RazorはXSS攻撃から保護します。第7章で見たように、一般的には安全であるべきです。 危険があるのは@Html.Raw()を使用する場合です。レンダリングするHTMLにユーザー入力が含まれている場合 (間接的であっても) 、XSSの脆弱性が存在する可能性があります。代わりに@を使用してユーザー入力をレンダリングすることで、図18.6に示すように、出力に書き込まれる前にコンテンツがエンコードされます。 この例では、HTMLエンコーディングを使用してHTML DOMに要素が直接追加されないようにしていますが、考えなければならないのはHTMLエンコーディングだけではありません。信頼できないデータをJavaScriptに渡したり、URLクエリ値で信頼できないデータを使用したりする場合は、データを正しくエンコードする必要があります。 一般的なシナリオは、RazorページでjQueryまたはJavaScriptを使用していて、サーバーからクライアントに値を渡す場合です。標準の@記号を使用してデータをページにレンダリングする場合、出力はHTMLエンコードされます。残念ながら、ストリングをHTMLエンコードしてJavaScriptに直接注入すると、期待どおりの結果が得られない可能性があります。 たとえば、nameという名前の変数がRazorファイルにあり、それをJavaScriptで使用可能にしたい場合、次のようなものを使用したくなるかもしれません。 <script>var name = '@name'</script> 名前に特殊文字が含まれている場合、RazorはHTMLエンコーディングを使用して特殊文字をエンコードしますが、これはこのJavaScriptコンテキストでは望ましくない可能性があります。たとえば、名前がArnold 「アーニー」 Schwarzeneggerの場合、以前と同じようにレンダリングすると次のようになります。 <script>var name = 'Arnold &quot;Arnie&quot; Schwarzenegger';</script> 二重引用符 (") はHTMLでエンコードされています」に注意してください。この値を 「安全な」 エンコード値としてJavaScriptで直接使用すると、図18.7に示すように正しく表示されません。 代わりに、JavaScriptエンコーディングを使用して変数をエンコードし、ダブルクォート文字が安全なUnicode文字\u 0022として表示されるようにします。これを行うには、ビューにJavaScriptEncoderを注入し (第10章を参照) 、name変数でEncode () を呼び出します。 @inject System.Text.Encodings.Web.JavaScriptEncoder encoder; <script>var name = '@encoder.Encode(name)'</script> JavaScriptエンコーディングの使用を忘れないようにするために、JavaScriptにこのような値を記述しないことをお勧めします。代わりに、HTML要素の属性に値を書き込み、後でJavaScript変数に読み込みます。これにより、JavaScriptエンコーダの必要性が完全になくなります。 Listing 18.4 Passing values to JavaScript by writing them to HTML attributes <div id="data" data-name="@name"></div> ❶ <script> var ele = document.getElementById('data'); ❷ var name = ele.getAttribute('data-name'); ❸ </script> ❶ Write the value you want in JavaScript to a data-* attribute. This will HTML-encode the data. ❷ Gets a reference to the HTML element ❸ Reads the data-* attribute into JavaScript, which will convert it to JavaScript encoding XSS攻撃は依然として一般的であり、ユーザーにデータの入力を許可すると、いつでもXSS攻撃にさらされやすくなります。受信データの検証が役立つこともありますが、多くの場合、これは難しい問題です。たとえば、ナイーブな名前バリデータでは、ほとんどの攻撃を防ぐために文字だけを使用する必要があります。残念ながら、名前にハイフンやアポストロフィが含まれているユーザーや、欧米以外の名前のユーザーは含まれていない。自分の名前が無効だと言うと (当然のことながら) 人々は怒るので、このやり方には注意してください。 厳密な検証を使用するかどうかにかかわらず、データをページに表示するときは常にデータをエンコードする必要があります。@Html.Raw()を書いていることに気付いたら、いつでも注意深く考えなさい。ユーザーが悪意のあるデータをそのフィールドに取り込む方法はありますか。その場合は、別の方法でデータを表示する必要があります。 XSSの脆弱性により、攻撃者はユーザーのブラウザ上でJavaScriptを実行することができます。今後検討する次の脆弱性では、ユーザーがアプリを使用していない場合でも、別のログインユーザーであるかのようにAPIにリクエストを行うことができます。怖い?そうだといいですね! 18.3 Protecting from cross-site request forgery (CSRF) attacks このセクションでは、クロスサイトリクエストフォージェリ攻撃、攻撃者がこれらを使用してサイト上のユーザーを偽装する方法、および偽造防止トークンを使用してユーザーを保護する方法について説明します。Razor Pagesはデフォルトでこれらの攻撃からユーザーを保護しますが、これらの検証を無効にすることもできるため、その影響を理解することが重要です。 クロスサイトリクエストフォージェリ (CSRF) 攻撃は、認証にCookieを使用するWebサイトまたはAPIにとって問題となる可能性があります。CSRF攻撃では、悪意のあるWebサイトがユーザーの代わりに認証済みのリクエストをAPIに送信し、ユーザーがリクエストを開始することはありません。このセクションでは、これらの攻撃がどのように機能するか、偽造防止トークンを使用して攻撃を軽減する方法について説明します。 この攻撃の典型的な例は、銀行送金/引き出しです。(特に従来のサーバ側でレンダリングされたアプリケーションで) 一般的なように、認証トークンをCookieに保存する銀行アプリケーションがあるとします。ブラウザは、リクエストごとにドメインに関連付けられたCookieを自動的に送信するため、アプリケーションはユーザーが認証されているかどうかを認識します。 ここで、アプリケーションに、ユーザーがBalance RazorページへのPOSTリクエストを使用して、自分のアカウントから別のアカウントに送金できるページがあるとします。フォームにアクセスするにはログインする必要がありますが ( [Authorize] 属性でRazorページを保護しています) 、それ以外の場合は、転送する金額と転送先を示すフォームを投稿するだけです。 ユーザーがサイトにアクセスしてログインし、トランザクションを実行するとします。次に、攻撃者が制御できる別のWebサイトにアクセスします。攻撃者は、あなたの銀行のウェブサイトへのPOSTを実行するフォームを、あなたの銀行のウェブサイトの振込フォームと同じように、彼らのウェブサイトに埋め込みました。このフォームは、図18.8に示すように、ユーザーのすべての資金を攻撃者に転送するなど、悪意のある処理を行います。ブラウザは、ページが完全なフォームの投稿を行うと、アプリケーションのCookieを自動的に送信します。バンキング・アプリケーションは、これが悪意のある要求であることを認識できません。疑うことを知らないユーザーが、すべての資金を攻撃者に渡しました。 この脆弱性は、ページが要求されたとき (GETリクエストを使用) 、またはフォームがPOSTされたときに、ブラウザが自動的にCookieを送信するという事実に基づいています。あなたのバンキングアプリのフォームの正規のPOSTと、攻撃者の悪意のあるPOSTの間に違いはありません。残念ながら、この動作はWebに組み込まれています。最初にログインした後、Webサイトをシームレスにナビゲートすることができます。 この攻撃に対する一般的なソリューションは、シンクロナイザトークン・パターンです。このパターンでは、ユーザー固有の一意の偽造防止トークンを使用して、正規のPOSTと攻撃者からの偽造POSTとの違いを強制します。7 1つのトークンがCookieに格納され、別のトークンが保護するフォームに追加されます。あなたのアプリは、現在ログインしているユーザーに基づいて実行時にトークンを生成するので、攻撃者が偽造フォーム用にトークンを作成する方法はありません。 Balance RazorページはフォームPOSTを受信すると、フォーム内の値とCookie内の値を比較します。いずれかの値が欠落しているか、一致しない場合、リクエストは拒否されます。攻撃者がPOSTを作成すると、ブラウザは通常どおりcookieトークンをポストしますが、フォーム自体にトークンがないか、トークンが有効ではありません。Razor Pageはリクエストを拒否し、図18.9のようにCSRF攻撃から保護します。 良いニュースは、Razor Pagesが自動的にCSRF攻撃から保護してくれることだ。フォーム・タグ・ヘルパーは、偽造防止トークンCookieを自動的に設定し、__RequestVerificationTokenという非表示フィールドにトークンをレンダリングします。 エレメントを選択します (特に無効にしない限り) 。たとえば、同じRazorページに投稿する次の単純なRazorテンプレートを考えてみましょう。 <form method="post"> <label>Amount</label> <input type="number" name="amount" /> <button type="submit">Withdraw funds</button> </form> HTMLにレンダリングされると、偽造防止トークンはhiddenフィールドに格納され、正当な要求とともにポストされます。 <form method="post"> <label>Amount</label> <input type="number" name="amount" /> <button type="submit" >Withdraw funds</button> <input name="id__RequestVerificationToken" type="hidden" value="CfDJ8Daz26qb0hBGsw7QCK"/> </form> ASP.NET Coreはすべてのフォームに偽造防止トークンを自動的に追加し、Razor Pagesはそれらを自動的に検証する。このフレームワークは、偽造防止トークンがCookieとフォーム・データの両方に存在することを確認し、トークンが一致することを確認し、一致しないリクエストはすべて拒否します。 Razor PagesではなくMVCコントローラをビューで使用している場合、ASP.NET Coreはすべてのフォームに偽造防止トークンを追加します。残念ながら、お客様のためにそれらを検証することはできません。代わりに、コントローラとアクションを [V a l idateAntiForgeryToken] 属性で装飾する必要があります。これにより、偽造防止トークンがCookieとフォームデータの両方に存在することが保証され、一致するかどうかがチェックされ、一致しない要求は拒否されます。 WARNING ビューでMVCコントローラを使用している場合、ASP.NET Coreは偽造防止トークンを自動的に検証しません。「ASP.NET Coreでクロスサイトリクエストフォージェリ (XSRF/CSRF) 攻撃を防ぐ」ドキュメントhttp://mng.bz/Xd6Eの説明に従って、すべての脆弱なメソッドを [ValidateAntiForgeryToken] 属性でマークしてください。Web APIコントローラを使用していて、認証にCookieを使用していない場合、CSRF攻撃に対する脆弱性はありません。 通常は、POST、DELETE、および状態の変更に使用されるその他の危険な要求タイプに対してのみ偽造防止トークンを使用する必要があります。GETリクエストはこの目的に使用すべきではないため、フレームワークは有効な偽造防止トークンを要求しません。Razor Pagesは、POSTのような危険な動詞の偽造防止トークンを検証し、GETのような安全な動詞を無視します。このパターン(そうすべきだ!)に従ってアプリを作成する限り、フレームワークはユーザーの安全を守るために正しいことを行います。 何らかの理由でRazor Page上の偽造防止トークンを明示的に無視する必要がある場合は、 [IgnoreAntiforgeryToken] 属性をRazor PageのPageModelに適用することで、検証を無効にできます。これにより、安全で保護する必要がないとわかっていることを実行している場合のフレームワーク保護が回避されますが、ほとんどの場合は、安全性を重視して検証することをお勧めします。 CSRF攻撃は、技術的な観点からあなたの頭を動かすのが難しいかもしれませんが、ほとんどの場合、あなたの側ではそれほどの努力をしなくてもすべてが動くはずです。Razorはフォームに偽造防止トークンを追加し、Razor Pagesフレームワークがあなたのために検証を行います。 もっと難しいのは、JavaScriptを使ってAPIに多くのリクエストをしていて、フォームデータではなくJSONオブジェクトをポストしている場合だ。このような場合、検証トークンをフォームの一部として送信することはできないため (JSONを送信するため) 、代わりにリクエストのヘッダーとして追加する必要があります。8 TIP クッキー認証を使用せず、認証トークンをヘッダーに送信するSPAを使用している場合は、CSRFをまったく心配する必要はありません。悪意のあるサイトは、ヘッダーではなくCookieのみをAPIに送信できるため、認証リクエストを行うことはできません。 Generating unique tokens with the data protection APIs CSRF攻撃を防ぐために使用される偽造防止トークンは、強力な対称暗号化を使用してデータを暗号化および復号化するフレームワークの機能に依存しています。暗号化アルゴリズムは通常、暗号化を初期化し、プロセスを再現可能にするために使用される1つ以上のキーに依存します。キーがあれば、データを暗号化および復号化できます。これがないと、データは安全です。 ASP.NET Coreでは、暗号化はデータ保護APIによって処理されます。これらは、偽造防止トークンの作成、認証Cookieの暗号化、および一般的なセキュアトークンの生成に使用されます。重要なのは、暗号化に使用されるキーファイルの管理も制御することです。 キーファイルは、ASP.NET Coreアプリケーションで暗号化に使用されるランダムなキー値を含む小さなXMLファイルです。それを安全に保存することが重要です。攻撃者がそれを手に入れた場合、アプリのどのユーザーにでもなりすまして、一般的に悪いことをする可能性があります。 データ保護システムは、アプリケーションをホストする方法と場所に応じて、キーを安全な場所に格納します。たとえば、次のようになります。 Azure Web App—リージョン間で共有される特別な同期フォルダ IIS (ユーザプロファイルなし) :レジストリで暗号化されています。 ユーザプロファイルを持つアカウント:Windowsの場合は%LOCALAPPDATA%\ASP.NET\DataProtection-Keys、LinuxまたはmacOSの場合は~/.aspnet/DataProtection-Keys その他のすべての場合:メモリ内;アプリを再起動すると、キーが失われる なぜ気にする?アプリがユーザーの認証Cookieを読み取ることができるようにするには、暗号化に使用されたものと同じキーを使用して暗号化を解除する必要があります。Webファームのシナリオで実行している場合、デフォルトでは、各サーバーは独自のキーを持ち、他のサーバーによって暗号化されたCookieを読み取ることはできません。 これを回避するには、アプリのデータ保護キーを中央の場所に保存するように設定する必要があります。例えば、ハードディスク上の共有フォルダ、Redisインスタンス、Azure Blobストレージインスタンスなどだ。 データ保護APIに関するMicrosoftのドキュメントは非常に詳細ですが、膨大な量になる可能性があります。データ保護の構成(「ASP.NET Core Data Protectionを構成します。」 http://mng.bz/d40i)、およびWebファームシナリオで使用するキーストレージプロバイダーの構成(「ASP.NET Coreの主要なストレージプロバイダ」 http://mng.bz/5pW6)に関するセクションを読むことをお勧めします。 このセクションで説明するCSRFの脆弱性では、悪意のあるサイトがアプリケーションに対して完全な形式のPOSTを実行する必要があることを明確にしておく必要があります。悪意のあるサイトは、クライアント側のJavaScriptのみを使用してAPIにリクエストを送信することはできません。これは、ブラウザが、別の発信元からのAPIに対するJavaScriptリクエストをブロックするためです。 これは安全機能ですが、問題が発生することがよくあります。クライアントサイドのSPAを構築している場合や,サーバサイドでレンダリングされたアプリに小さなJavaScriptがある場合でも,このようなクロスオリジン要求が必要になることがあります。次のセクションでは、よくあるシナリオと、それを回避するためにアプリを変更する方法を説明します。 18.4 Calling your web APIs from other domains using CORS このセクションでは、Cross-Origin Resource Sharing (CORS) について説明します。Cross-Origin Resource Sharingは、JavaScriptがドメイン間でリクエストを行うためのプロトコルです。CORSは多くの開発者にとってしばしば混乱する領域であるため、このセクションでは、CORSが必要な理由とCORSヘッダーの動作について説明します。次に、アプリケーション全体と特定のWeb APIアクションの両方にCORSを追加する方法、およびアプリケーションに複数のCORSポリシーを構成する方法について説明します。 すでに説明したように、CSRF攻撃は強力ですが、ブラウザーが同一生成元ポリシーを実装していなければ、さらに危険です。このポリシーは、Web APIが明示的に許可しない限り、アプリケーションがJavaScriptを使用して別の場所でWeb APIを呼び出すことをブロックします。 DEFINITION オリジンは、スキーム (HTTPまたはHTTPS) 、ドメイン(example.com) 、およびポート(HTTPの場合はデフォルトで80、HTTPSの場合は443)に一致する場合、同じと見なされます。アプリケーションがJavaScriptを使用してリソースにアクセスしようとし、オリジンが同一でない場合、ブラウザーはリクエストをブロックします。 同一発信元ポリシーは厳密です。要求を許可するには、2つのURLの発信元が同一である必要があります。たとえば、次のオリジンは同じです。 http://example.com/home http://example.com/site.css これらのURLではパスが異なりますが(/homeおよび/site.css)、スキーム、ドメイン、およびポート (80) は同じです。そのため、アプリケーションのホームページにいる場合は、JavaScriptを使用して/site.cssファイルをリクエストしても問題はありません。 対照的に、次のサイトのオリジンはすべて異なるため、http://example.comオリジンのJavaScriptを使用してこれらのURLをリクエストすることはできません。 https://example.com—Different scheme (https) http://www.example.com—Different domain (includes a subdomain) http://example.com:5000—Different port (default HTTP port is 80) 単一のウェブアプリがすべての機能を処理しているシンプルなアプリの場合、この制限は問題にならないかもしれないが、アプリが別のドメインにリクエストを出すことは極めて一般的だ。 たとえば、http://shopping.com,でホスティングされているEコマースサイトがあり、http://api.shopping.comからデータをロードして販売可能な製品の詳細を表示しようとしているとします。この設定では、同一発信元ポリシーの違反となります。JavaScriptを使用してAPIドメインにリクエストを行おうとすると、図18.10のようなエラーで失敗します。 クライアント側SPAの台頭とモノリシックなアプリケーションからの移行に伴い、JavaScriptからのクロスオリジン要求を行う必要性はますます一般的になっています。幸いなことに、安全な方法でこの問題を回避できるWeb標準があります。この標準はCross-Origin Resource Sharing (CORS) と呼ばれます。CORSを使用して、APIを呼び出すことができるアプリケーションを制御できるため、前述のようなシナリオを有効にできます。 18.4.1 Understanding CORS and how it works CORSは、誰がオリジン間リクエストを行うことができるかについて記述するWeb APIを可能にするWeb標準です。たとえば、次のようなステートメントを作成できます。 Allow cross-origin requests from http://shopping.com and https://app.shopping.com. Only allow GET cross-origin requests. Allow returning the Server header in responses to cross-origin requests. Allow credentials (such as authentication cookies or authorization headers) to be sent with cross-origin requests. これらのルールをポリシーに結合して、APIの異なるエンドポイントに異なるポリシーを適用できます。アプリケーション全体にポリシーを適用することも、すべてのAPIアクションに異なるポリシーを適用することもできます。 CORSはHTTPヘッダーを使用して機能します。Web APIアプリケーションがリクエストを受信すると、レスポンスに特別なヘッダーを設定して、オリジン間リクエストが許可されるかどうか、許可されるオリジン、リクエストで使用できるHTTP動詞とヘッダー (リクエストに関するほとんどすべて) を示します。 場合によっては、実際のリクエストをAPIに送信する前に、ブラウザがプリフライトリクエストを送信します。これはOPTIONS動詞を使用して送信されるリクエストで、ブラウザはこれを使用して実際のリクエストの実行が許可されているかどうかをチェックします。APIが正しいヘッダーを返すと、図18.11に示すように、ブラウザは実際のクロスオリジン・リクエストを送信します。 TIP For a more detailed discussion of CORS, see CORS in Action by Monsur Hossain (Manning, 2014), available at http://mng.bz/aD41. CORSの仕様は、多くの技術文書と同様に非常に複雑であり、さまざまなヘッダーやプロセスと競合します。9ありがたいことに、ASP.NET Coreはあなたのために仕様の詳細を処理してくれるので、あなたの主な関心事は、誰があなたのAPIにアクセスする必要があるのか、そしてどのような状況下にあるのかを正確に把握することです。 18.4.2 Adding a global CORS policy to your whole app 通常は、必要になるまでAPIのCORSを設定しないでください。ブラウザが発信元を越えた通信をブロックするのは、攻撃の道を塞ぐためであり、厄介ではありません。APIにアクセスする必要があるアプリケーションとは別のドメインにAPIがあるまで待ちます。 アプリケーションにCORSサポートを追加するには、次の4つのことが必要です。 Add the CORS services to your app. Configure at least one CORS policy. Add the CORS middleware to your middleware pipeline. Either set a default CORS policy for your entire app or decorate your Web API actions with the [EnableCors] attribute to selectively enable CORS for specific endpoints. アプリケーションにCORSサービスを追加するには、Startup.ConfigureServices メソッドで AddCors() 呼び出しを追加します。 services.AddCors() CORSの設定作業の大部分は、ポリシーの設定に費やされます。CORSポリシーは、アプリケーションがオリジン間リクエストに応答する方法を制御します。どのオリジンが許可され、どのヘッダーが返され、どのHTTPメソッドが許可されるかなどを定義します。通常、CORSサービスをアプリケーションに追加するときに、ポリシーをインラインで定義します。 たとえば、前のEコマースサイトの例を考えてみましょう。http://api.shopping.comでホストされているAPIを、http://shopping.comでホストされているクライアントサイドJavaScriptを介してメインアプリから利用できるようにしたいとします。したがって、オリジン間リクエストを許可するようにAPIを設定する必要があります。 NOTE クロスオリジンのリクエストをしようとするとエラーが発生するのはメインアプリですが、CORSを追加する必要があるのはアクセスしているAPIであり、リクエストを行っているアプリではありません。 次のリストは、「AllowShoppingAppの設定」という名前のポリシーを構成して、http://shopping.comからAPIへのクロスオリジン・リクエストを有効にする方法を示しています。さらに、任意のHTTP動詞タイプを明示的に許可します。この呼び出しがなければ、単純なメソッド(GET、HEAD、およびPOST)のみが許可されます。ポリシーは、本書全体を通してお馴染みの流暢なビルダースタイルを使用して構築されています。 Listing 18.5 Configuring a CORS policy to allow requests from a specific origin public void ConfigureServices(IServiceCollection services) { services.AddCors(options => { ❶ options.AddPolicy("AllowShoppingApp", policy => ❷ policy.WithOrigins("http://shopping.com") ❸ .AllowAnyMethod()); ❹ }); // other service configuration } ❶ The AddCors method exposes an Action overload. ❷ Every policy has a unique name. ❸ The WithOrigins method specifies which origins are allowed. Note that the URL has no trailing /. ❹ Allows all HTTP verbs to call the API WARNING WithOrigins () でオリジンをリストする場合、末尾に「/」が付いていないことを確認してください。そうしないと、オリジンは一致せず、オリジン間リクエストは失敗します。 CORSポリシーを定義したら、アプリケーションに適用できます。次のリストでは、Startup.csのConfigureメソッドでUseCors () を呼び出して、CorsMiddlewareを使用して「AllowShoppingAppの設定」ポリシーをアプリケーション全体に適用します。 Listing 18.6 Adding the CORS middleware and configuring a default CORS policy public void Configure(IApplicationBuilder app, IWebHostEnvironment env) { app.UseRouting(); ❶ app.UseCors("AllowShoppingApp"); ❷ app.UseAuthentication(); app.UseAuthorization(); app.UseEndpoints(endpoints => ❸ { endpoints.MapControllers(); }); } ❶ The CORS middleware must come after the call to UseRouting(). ❷ Adds the CORS middleware and uses AllowShoppingApp as the default policy ❸ Place the CORS middleware before the endpoint middleware. NOTE すべてのミドルウェアと同様に、CORSミドルウェアの順序は重要です。UseRouting () の後、UseEndpoints () の前に、UseCors () にコールを発信する必要があります。CORSミドルウェアは、プリフライトリクエストに対する正しいレスポンスを生成し、必要なヘッダーを追加できるように、Web APIアクションに対するオリジン間リクエストをインターセプトする必要があります。通常は、UseAuthentication () を呼び出す前にCORSミドルウェアを配置します。 CORSミドルウェアをAPIに配置することで、ショッピングアプリはクロスオリジンリクエストを行うことができるようになる。図18.12に示すように、http://shopping.comサイトからAPIを呼び出すことができ、ブラウザはCORSリクエストを通します。http://shopping.com,以外のドメインから同じリクエストを行った場合、リクエストは引き続きブロックされます。 この方法でCORSポリシーをアプリケーションにグローバルに適用するのは過剰かもしれません。API内に他のオリジンからアクセスする必要があるアクションのサブセットしかない場合は、それらの特定のアクションに対してのみCORSを有効にすることをお勧めします。これは、 [EnableCors] 属性で実現できます。 18.4.3 Adding CORS to specific Web API actions with EnableCorsAttribute ブラウザは、悪意のあるサイトや侵害されたサイトによって悪用される可能性があるため、デフォルトでクロスオリジン要求をブロックします。アプリ全体のCORSを有効にしても、Cross-Originにアクセスする必要があるアクションのサブセットのみであることがわかっている場合は、リスクに見合う価値がない可能性があります。 その場合は、それらの特定のアクションに対してのみCORSポリシーを有効にすることをお勧めします。ASP.NET Coreには [EnableCors] 属性があり、特定のコントローラやアクションメソッドに適用するポリシーを選択することができる。この方法では、異なるアクションメソッドに異なるCORSポリシーを適用できます。たとえば、http://shopping.comドメインからのAPI全体へのGETリクエストアクセスを許可し、特定のコントローラーの他のHTTP動詞のみを許可し、すべてのユーザーに製品リストのアクションメソッドへのアクセスを許可することができます。 これらのポリシーは、リスト18.5で見たように、AddPolicy () を使用してConfigureServicesの中で定義し、ポリシーに名前を付けます。しかし、リスト18.6で見たようにUseCors (「AllowShoppingAppの設定」) を呼び出すのではなく、UseCors () のみを呼び出すことで、デフォルトのポリシーなしでミドルウェアを追加します。 ポリシーをコントローラまたはアクションメソッドに適用するには、次のリストに示すように [EnableCors] 属性を適用します。アクションの [EnableCors] 属性は、コントローラの属性よりも優先されます。または、 [DisableCors] 属性を使用して、メソッドへのクロスオリジンアクセスを完全に無効にすることもできます。 Listing 18.7 Applying the EnableCors attribute to a controller and action [EnableCors("AllowShoppingApp")] ❶ public class ProductController: Controller { [EnableCors("AllowAnyOrigin") ❷ public IActionResult GeteProducts() { /* Method */ } public IActionResult GeteProductPrice(int id) { /* Method */ } ❸ [DisableCors] public IActionResult DeleteProduct(int id) { /* Method */ } ❹ } ❶ Applies the AllowShoppingApp CORS policy to every action method ❷ The AllowAnyOrigin policy is “closer” to the action, so it takes precedence. ❸ The AllowShoppingApp policy (from the controller) will be applied. ❹ The DisableCors attribute disables CORS for the action method completely. ほとんどのアクションにCORSポリシーを適用したいが、別のポリシーを使用したい、または一部のアクションでCORSを完全に無効にしたい場合は、例えばUseCors (「AllowShoppingAppの設定」) を使用してミドルウェアを構成するときにデフォルト・ポリシーを渡すことができます。「その他のポリシー」で装飾されたアクションはOtherPolicyを優先的に適用し、で装飾されたアクションはCORSをまったく有効にしません。 単一のデフォルトCORSポリシーを使用するか、複数のポリシーを使用するかにかかわらず、ConfigureServicesでアプリケーションのCORSポリシーを設定する必要があります。CORSの構成時には、さまざまなオプションを使用できます。次のセクションでは、可能性の概要を説明します。 18.4.4 Configuring CORS policies ブラウザはセキュリティ上の理由からクロスオリジン・ポリシーを実装するため、これらのポリシーが課す制限を緩和することによる影響を慎重に検討する必要があります。オリジン間リクエストを有効にしても、オリジン間リクエストが送信できるデータとAPIが返すデータを制御できます。 The origins that may make a cross-origin request to your API The HTTP verbs (such as GET, POST, and DELETE) that can be used The headers the browser can send The headers that the browser can read from your app’s response Whether the browser will send authentication credentials with the request リスト18.5で見たように、CorsPolicyBuilderを使用してAddCors () を呼び出す際にCORSポリシーを作成する場合は、これらのオプションをすべて定義します。ポリシーでは、これらのオプションをすべて設定することも、まったく設定しないこともできます。そのため、結果を自分の好みに合わせてカスタマイズできます。表18.1に、使用可能なオプションとその効果を示します。 CORSを設定する際の最初の問題の1つは、クロスオリジンの問題があることに気付くことです。私は何度も、リクエストがクロスドメインになったり、HTTPからHTTPSになったりすることに気づくまで、なぜリクエストがうまくいかないのか理解しようとしていた。 可能な限り、オリジン間のリクエストは完全に避けることをお勧めします。ブラウザの処理方法に微妙な違いが生じることがあり、頭痛の種になります。特に、HTTPSの背後ですべてのアプリケーションを実行することで、ドメイン間のHTTPからHTTPSへの問題を回避します。18.1で説明したように、これはいずれにせよベストプラクティスであり、CORSの頭痛の種をすべて回避するのに役立ちます。 一度CORSポリシーを確立したら、通常はWithOrigins () メソッドから始めます。その後、必要に応じてポリシーをさらに拡張または制限し、必要な機能を許可したまま、APIのクロスオリジンロックダウンを提供します。CORSは扱いにくいかもしれませんが、安全のために制限があることを覚えておいてください。10 Cross-originリクエストは、攻撃者がアプリを侵害するために利用できる多くの可能性のある手段の1つにすぎません。これらの多くは簡単に防御できますが、それらを認識し、軽減する方法を知っておく必要があります。次のセクションでは、一般的な脅威とその回避方法について説明します。 18.5 Exploring other attack vectors この章ではこれまで、攻撃者がアプリを危険にさらす可能性のある2つの方法 (XSSとCSRF攻撃) と、その防止方法について説明してきました。これらの脆弱性はいずれも、OWASPの最も重大なウェブアプリケーションのリスクトップ10 (11) に頻繁に掲載されています。このセクションでは、その他の最も一般的な脆弱性の概要と、それらの脆弱性を回避する方法について説明します。 18.5.1 Detecting and avoiding open redirect attacks OWASPの一般的な脆弱性は、オープンなリダイレクト攻撃によるものです。オープンリダイレクト攻撃とは、ユーザーが安全なアプリへのリンクをクリックすると、マルウェアを仕掛けるウェブサイトなどの悪意あるウェブサイトにリダイレクトされる攻撃だ。安全なアプリには、悪意のあるウェブサイトへの直接のリンクが含まれていませんが、これはどのようにして起こるのですか? オープンリダイレクト攻撃は、次のページがアクションメソッドへのパラメータとして渡される場合に発生します。最も一般的な例は、アプリケーションにログインするときです。通常、アプリケーションは、returnUrlクエリ文字列パラメータとして現在のページを渡すことで、ユーザーがログインページにリダイレクトされる前に、ユーザーがいるページを記憶します。ユーザーがログインすると、アプリケーションはユーザーをreturnUrlにリダイレクトし、ユーザーが中断したところから続行します。 ユーザーが電子商取引サイトを閲覧しているとします。製品の購入をクリックすると、ログインページにリダイレクトされます。彼らがいた製品ページはreturnUrlとして渡されるので、ログイン後はホーム画面に戻されるのではなく、製品ページにリダイレクトされる。 オープンリダイレクト攻撃は、図18.13に示すように、この一般的なパターンを利用します。悪意のある攻撃者は、returnUrlがユーザーに送信するWebサイトに設定されているログインURLを作成し、ユーザーにWebアプリケーションへのリンクをクリックするように促します。ユーザーがログインすると、脆弱なアプリケーションがユーザーを悪意のあるサイトにリダイレクトします。 この攻撃に対する単純な解決策は、returnUrlがあなたのアプリに属するローカルURLであることを、ユーザをリダイレクトする前に常に検証することです。デフォルトのIdentity UIはすでにこれを実行しているため、Identityを使用している場合はログインページを気にする必要はありません (第14章を参照) 。 アプリケーションの他の部分にリダイレクトがある場合、ASP.NET Coreは安全を保つためのいくつかのヘルパーメソッドを提供し、その中で最も有用なのがUrlです。IsLocalUrl () を返します。以下のリストは、提供されたリターンURLが安全であることを確認する方法と、安全でない場合はアプリのホームページにリダイレクトする方法を示している。ControllerBaseクラスおよびRazor Page PageModelクラスでLocalRedirect () ヘルパーメソッドを使用することもできます。このメソッドは、指定されたURLがローカルでない場合に例外をスローします。 Listing 18.8 Detecting open redirect attacks by checking for local return URLs [HttpPost] public async Task<IActionResult> Login( LoginViewModel model, string returnUrl = null) ❶ { // Verify password, and sign user in if (Url.IsLocalUrl(returnUrl)) ❷ { return Redirect(returnUrl); ❸ } else { return RedirectToAction("Index", "Home"); ❹ } } ❶ The return URL is provided as an argument to the action method. ❷ Returns true if the return URL starts with / or ~/ ❸ The URL is local, so it’s safe to redirect to it. ❹ The URL was not local and could be an open redirect attack, so redirect to the homepage for safety. この単純なパターンは、ユーザーを悪意のあるコンテンツにさらす可能性のあるオープンなリダイレクト攻撃から保護します。クエリ文字列またはその他のユーザー入力からURLにリダイレクトする場合は、常にこのパターンを使用する必要があります。 オープンリダイレクト攻撃は、アプリに直接影響するのではなく、ユーザーにリスクをもたらします。次の脆弱性は、アプリ自体の重大な脆弱性を表しています。 18.5.2 Avoiding SQL injection attacks with EF Core and parameterization SQLインジェクション攻撃は、アプリケーションに対する最も危険な脅威の1つです。攻撃者は単純な悪意のある入力を作成し、従来のフォームベースの入力としてアプリケーションに送信したり、URLやクエリ文字列をカスタマイズしてデータベースに対して任意のコードを実行したりします。SQLインジェクションの脆弱性は、データベース全体を攻撃者にさらす可能性があるため、このような脆弱性を見つけて削除することが非常に重要です。 今回は、EF Core (あるいは他のほとんどのORM) を標準的な方法で使っているなら、安全であるべきだ。EF CoreにはSQLインジェクションに対する保護機能が組み込まれているので、おかしなことをしていなければ問題ない。 SQLインジェクションの脆弱性は、SQL文を自分で作成し、攻撃者が間接的にも提供する動的入力を含む場合に発生します。EF Coreには、FromSqlRaw () メソッドを使ってraw SQLクエリを作成する機能があるので、このメソッドを使うときは注意が必要だ。 例えば、レシピアプリに検索フォームがあり、レシピを名前で検索できるとします。LINQ拡張メソッド (第12章で説明) を使用してクエリを記述する場合、SQLインジェクション攻撃のリスクはありません。しかし、SQLクエリーを手動で作成しようとすると、このような脆弱性にさらされます。 Listing 18.9 An SQL injection vulnerability in EF Core due to string concatenation public IList<User> FindRecipe(string search) ❶ { return _context.Recipes ❷ .FromSqlRaw("SELECT * FROM Recipes" + ❸ "WHERE Name = '" + search + "'") ❹ .ToList(); } ❶ The search parameter comes from user input, so it’s unsafe. ❷ The current EF Core DbContext is held in the _context field. ❸ You can write queries by hand using the FromSqlRaw extension method. ❹ This introduces the vulnerability—including unsafe content directly in an SQL string. このリストでは、searchで保持されているユーザ入力が直接SQLクエリに含まれています。悪意のある入力を作成することで、ユーザーはデータベースに対してあらゆる操作を実行できる可能性があります。攻撃者がテキストを使ってあなたのウェブサイトを検索するところを想像してみてください ';DROP TABLEレシピ;―― アプリケーションはこれを検索パラメータに割り当て、データベースに対して実行されたSQLクエリは次のようになります。 SELECT*FROM Recipes WHERE Name='';DROP TABLEレシピ;――」 あなたのアプリの検索フォームにテキストを入力するだけで、攻撃者はあなたのアプリからレシピテーブル全体を削除してしまいます!これは致命的ですが、SQLインジェクションの脆弱性により、多かれ少なかれデータベースへの自由なアクセスが可能になります。このような破壊的なアクションを防ぐためにデータベースのパーミッションを正しく設定している場合でも、攻撃者はユーザーの詳細を含むデータベースのすべてのデータを読み取ることができます。 このような状況を避ける簡単な方法は、このように手動でSQLクエリーを作成しないようにすることです。独自のSQLクエリを記述する必要がある場合は、リスト18.9のように文字列連結を使用しないでください。代わりに、パラメータ化されたクエリを使用してください。パラメータ化されたクエリでは、 (安全でない可能性がある) 入力データがクエリ自体から分離されます。次に例を示します。 Listing 18.10 Avoiding SQL injection by using parameterization public IList<User> FindRecipe(string search) { return _context.Recipes .FromSqlRaw("SELECT * FROM Recipes WHERE Name = '{0}'", ❶ search) ❷ .ToList(); } ❶ The SQL query uses a placeholder {0} for the parameter. ❷ The dangerous input is passed as a parameter, separate from the query. パラメータ化されたクエリはSQLインジェクション攻撃に対して脆弱ではないため、以前に提示された攻撃は機能しませんでした。EF Core (または他のORM) を使って、標準のLINQクエリを使ってデータにアクセスすれば、インジェクション攻撃に対して脆弱になることはない。EF Coreは、ユーザーを保護するためにパラメータ化されたクエリを使って、すべてのSQLクエリを自動的に作成する。 注意:SQLインジェクション攻撃についてはリレーショナルデータベースの観点からしか説明していませんが、この脆弱性はNoSQLやドキュメントデータベースにも存在する可能性があります。常にパラメータ化されたクエリ (または同等のクエリ) を使用し、文字列をユーザ入力と連結してクエリを作成しないでください。 インジェクション攻撃は、10年以上にわたってWeb上の最大の脆弱性でした。そのため、インジェクション攻撃とその発生方法を認識しておくことが重要です。raw SQLクエリを記述する必要がある場合は、必ずパラメータ化されたクエリを使用してください。 次の脆弱性は、攻撃者がアクセスすべきでないデータにアクセスすることにも関連している。これは直接インジェクション攻撃よりも少し巧妙だが、簡単に実行できる―攻撃者が必要とするスキルは数える能力だけだ。 18.5.3 Preventing insecure direct object references 安全ではない直接オブジェクト参照は少々面倒だが、それはユーザーがURLのパターンに気づくことでアクセスすべきでないものにアクセスすることを意味する。旧友のレシピアプリをもう一度見てみましょう。アプリはレシピのリストを表示します。いずれも表示できますが、編集できるのは自分で作成したレシピのみです。他のユーザーのレシピを表示しても、 「編集」 ボタンは表示されません。 たとえば、ユーザーがいずれかのレシピの 「編集」 ボタンをクリックすると、そのURLが/Recipes/Edit/120であることがわかります。この 「120」 は、編集中のエンティティの基礎となるデータベースIDとしては完全な情報です。単純な攻撃は、通常はアクセスできない別のエンティティにアクセスするためにそのIDを変更することです。ユーザーは/Recipes/Edit/121と入力してみることができます。入力したレシピを編集または表示することができない場合は、安全でない直接オブジェクト参照の脆弱性が存在します。 この問題の解決策は簡単です。アクション方式にはリソースベースの認証と認可が必要です。ユーザがアクセスを許可されていないエンティティにアクセスしようとすると、permission-deniedエラーが発生します。ブラウザの検索バーに直接URLを入力することで、認証を回避することはできません。 ASP.NET Coreアプリでは、通常、編集ボタンを非表示にするなど、UIの要素を非表示にしてユーザーを制限しようとすると、この脆弱性が発生します。代わりに、第15章で説明するように、リソースベースの認証を使用してください。 警告ユーザがアクセスできるエンティティを制限するには、常にリソースベースの認証を使用する必要があります。UI要素を非表示にすると、ユーザエクスペリエンスが向上しますが、セキュリティ対策ではありません。 URL内のエンティティの整数IDを回避することで、この脆弱性をある程度回避できます。たとえば、代わりに疑似乱数GUID (たとえば、C 2 E 296 BA-7 EA 8-4195-9 CA 7-C 323304 CCD 12)を使用します。これにより、既存の番号に1つを追加するだけではなく、問題を修正するのではなくマスクするだけなので、他のエンティティを推測するプロセスが難しくなります。それにもかかわらず、GUIDは、 (認証を必要としない) 一般にアクセス可能なページを持ちたいが、そのIDを簡単に見つけられないようにしたい場合に便利です。 この章の最後のセクションでは、単一の脆弱性は扱いません。代わりに、ユーザーのデータを保護するという、別の関連する問題について説明します。 18.5.4 Protecting your users’ passwords and data 多くのアプリでは、最も機密性の高いデータはユーザーの個人データです。これには、電子メール、パスワード、住所の詳細、または支払い情報が含まれます。これらのデータを保存するときは注意が必要です。攻撃者を誘うターゲットを提示するだけでなく、データ保護法やPCIコンプライアンス要件など、その処理方法に関する法的義務を負う場合があります。 最も簡単な方法は、不要なデータを保存しないことです。ユーザーのアドレスが不要な場合は、要求しないでください。そうすれば、失うことはありません。同様に、第14章で説明したように、ユーザーの詳細を保存するためにサードパーティのIDサービスを使用する場合は、ユーザーの個人情報を保護するためにそれほど努力する必要はありません。 ユーザーの詳細を独自のアプリに保存したり、独自のIDプロバイダーを構築したりする場合は、ユーザー情報を処理するときにベストプラクティスに従うようにする必要があります。ASP.NET Core Identityを使用する新しいプロジェクトテンプレートは、デフォルトでこれらのプラクティスのほとんどに従うため、これらのいずれかから開始することを強くお勧めします。さまざまな側面を考慮する必要がありますが (詳細はここでは説明できません12) 、次のような側面があります。 ユーザパスワードを直接どこにも保存しないでください。暗号化ハッシュは、BCryptやPBKDF 2などの高価なハッシュアルゴリズムを使用して計算されたものだけを格納してください。 必要以上のデータを保存しないでください。クレジットカードの詳細を保存しないでください。 ユーザーが2要素認証 (2 FA) を使用してサイトにサインインできるようにします。 脆弱または侵害されたことがわかっているパスワードをユーザが使用できないようにします。 認証Cookieを 「http」 (JavaScriptを使用して読み取ることができないようにする) および 「安全」 としてマークし、HTTPS接続でのみ送信され、HTTPでは送信されないようにします。 ユーザーがすでにアプリに登録されているかどうかを公開しないでください。この情報を漏洩すると、列挙型攻撃にさらされる可能性があります。13 これらはすべてガイドラインですが、ユーザーを保護するために最低限行うべきことを示しています。最も重要なことは、アプリを構築する際に、潜在的なセキュリティの問題を認識することです。セキュリティを最後に放棄しようとすることは、最初から考えるよりも常に難しいので、後で考えるよりも、早めに考えることをお勧めします。 この章では、注意すべき点をまとめています。セキュリティの脆弱性に関するほとんどの有名企業について説明しましたが、この章で説明した他のリソースについても確認してください。これらは、この章で説明した防御を補完するものであり、考慮すべき事項のより包括的なリストを提供します。第6章で説明したように、ASP.NET Coreには最も一般的な攻撃に対する基本的な保護機能が含まれていますが、それでも自分の足を撃つことができます。あなたのアプリが侵害されたことで見出しを飾ることがないようにしてください! Summary HTTPSは、アプリのデータがサーバからブラウザに転送されたり戻ったりする際に暗号化するために使用される。これにより、サードパーティによる表示や変更ができなくなります。 ChromeやFirefoxのような最近のブラウザでは、HTTPSは非HTTPSアプリを 「安全ではない」 と明示的にマークしているので、HTTPSは事実上、本番アプリに必須だ。 実稼働環境では、SSL/TLSオフロードを使用することで、アプリでTLSを処理するのを避けることができます。この場合、リバースプロキシはHTTPSを使用してブラウザと通信しますが、トラフィックはアプリとリバースプロキシの間で暗号化されません。リバースプロキシは、IISやNGINXなどの同じサーバーまたは異なるサーバーに存在する場合もあれば、Cloudflareなどのサードパーティのサービスに存在する場合もあります。 開発中にHTTPSを有効にするには、ASP.NET Core開発者証明書またはIIS Express開発者証明書を使用できます。これは本番環境では使用できませんが、ローカルでのテストには十分です。最初に.NET SDKをインストールして証明書を信頼するときは、dotnet dev-certs https--trustを実行する必要があります。 「Kestrel:Certificates:Default configuration」 セクションを使用して、本番環境のKestrelにHTTPS証明書を設定できます。アプリケーションを変更する必要はありません。Kestrelは、アプリケーションの起動時に証明書を自動的にロードし、HTTPSリクエストの処理に使用します。 HstsMiddlewareを使用してアプリケーションのHTTP Strict Transport Security (HSTS) ヘッダーを設定し、ブラウザがHTTPリクエストではなくHTTPSリクエストをアプリケーションに送信するようにできます。これは、アプリに対してHTTPSリクエストが行われたときにのみ適用されるため、HTTPからHTTPSへのリダイレクションと組み合わせて使用するのが最適です。 HttpsRedirectionMiddlewareを使用して、アプリケーション全体にHTTPSを強制できます。これにより、HTTP要求がHTTPSエンドポイントにリダイレクトされます。 クロスサイトスクリプティング (XSS) 攻撃では、悪意のあるユーザーがアプリケーションにコンテンツを挿入し、通常はユーザーがアプリケーションを参照するときに悪意のあるJavaScriptを実行します。安全でない入力をページに書き込む前に常にエンコードすることで、XSSインジェクション攻撃を回避できます。Razor Pagesは@Html.Raw()方式を使用しない限りこれを自動的に行うため、慎重に使用してください。 クロスサイトリクエストフォージェリ (CSRF) 攻撃は、ASP.NET Core Identityのようなクッキーベースの認証を使用するアプリにとって問題である。これは、ブラウザが自動的にCookieをWebサイトに送信するという事実に基づいています。悪意のあるWebサイトは、ユーザーのサイトにPOSTするフォームを作成することができ、ブラウザは認証Cookieを要求と共に送信します。これにより、悪意のあるWebサイトは、ログインユーザーであるかのように要求を送信できます。 偽造防止トークンを使用してCSRF攻撃を軽減できます。これには、現在のユーザーに基づいてランダムな文字列を含むすべてのフォームに非表示フィールドを書き込むことが含まれます。同様のトークンがCookieに格納されます。正規のリクエストには両方の部分が含まれますが、悪意のあるWebサイトからの偽造されたリクエストにはCookieの半分しか含まれません。フォーム内の非表示フィールドを再作成することはできません。これらのトークンを検証することで、APIは偽造されたリクエストを拒否できます。 Razor Pagesフレームワークは、Razorを使用して作成したフォームに偽造防止トークンを自動的に追加し、着信要求のトークンを検証します。必要に応じて、 [IgnoreAntiForgeryToken] 属性を使用して検証チェックを無効にできます。 ブラウザは、Webサイトが1つのアプリケーションから異なるオリジンの他のアプリケーションにJavaScript AJAX要求を行うことを許可しません。オリジンに一致させるには、アプリケーションのスキーム、ドメイン、およびポートが同じである必要があります。このようなCross-Originリクエストを行うには、APIでCross-Origin Resource Sharing (CORS) を有効にする必要があります。 CORSはHTTPヘッダーを使用してブラウザと通信し、APIを呼び出すことができるオリジンを定義します。ASP.NET Coreでは、複数のポリシーを定義でき、アプリケーション全体にグローバルに適用することも、特定のコントローラおよびアクションに適用することもできます。 CORSミドルウェアを追加するには、StartupでUseCors () を呼び出します。適用するデフォルトCORSポリシーの名前を設定し、オプションで指定します。[EnableCors] 属性を追加し、適用するポリシーの名前を指定することで、CORSをWeb APIアクションまたはコントローラに適用することもできます。 オープンリダイレクト攻撃は、ログイン後に一般的なreturnURLメカニズムを使用して、ユーザを悪意のあるWebサイトにリダイレクトします。この攻撃を防ぐには、ローカルURL (アプリに属するURL) にのみリダイレクトするようにします。 安全でない直接オブジェクト参照は、URL内のデータベース・エンティティのIDを公開する場合によく発生する問題です。アクション・メソッドでリソースベースの認可を使用して、要求されたリソースにアクセスまたは変更する権限がユーザーにあることを常に確認する必要があります。 SQLインジェクション攻撃は、SQL要求を手動で作成する場合の一般的な攻撃ベクターです。リクエストを構築するときは常にパラメータ化されたクエリを使用するか、SQLインジェクションの影響を受けないEF Coreのようなフレームワークを使用する。 アプリ内で最も機密性の高いデータは、多くの場合ユーザーのデータです。必要なデータのみを保存することで、このリスクを軽減します。パスワードをハッシュとしてのみ保存し、脆弱なパスワードまたは侵害されたパスワードから保護し、2 FAのオプションを提供するようにしてください。ASP.NET Core Identityは、これらのすべてをすぐに利用できるため、IDプロバイダを作成する必要がある場合に最適です。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Blazor WebAssembly Postgresを使うまで

はじめに この記事はPostgresでBlazorを使えるようにするまでの記事です。 NuGetでインストールが必要なもの Npgsql.EntityFrameworkCore.PostgreSQL Npgsql.EntityFrameworkCore.PostgreSQL.Design コード側の変更点 Server側のStartup.csファイルのUseSqlServerをUseNpgsqlに以下を変更 services.AddDbContext<ApplicationDbContext>(options => options.UseSqlServer( Configuration.GetConnectionString("DefaultConnection"))); services.AddDbContext<ApplicationDbContext>(options => options.UseNpgsql( Configuration.GetConnectionString("DefaultConnection"))); appsettings.json DefaultConnectionの部分、接続先をPostgresのDBへ設定  Data/Migrationsの中のファイルを全て削除 このフォルダにはコマンドAdd-Migration InitialMigrationなどで作成したファイル(DB構成の変更情報)が格納されるが、デフォルトではSQLServerのDBを更新する内容でありPostresにあったものではないため削除をする。  パッケージマネージャーコンソールでコマンド実行 Add-Migration InitialMigrationを実行し、Postgres用のMigrationファイルを生成する。 Update-Databaseを実行しMigrationファイルを実行  動作確認 アカウント作成が出来れば正常にPostgresを使えている。 [Blazor関連のリンク] Blazor WebAssembly プロジェクト作成(認証あり) Blazor WebAssembly 初期プロジェクト構成の入門 Blazor WebAssembly Postgresを使うまで Blazor WebAssembly コードビハインド Blazor WebAssembly InputSelectの使い方
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

.NET 単体テストで IHttpClientFactory を使うメソッドを Mock する

毎回テストを書こうとするたびに忘れてしまうのでメモ。 テスト対象のコードはこんな感じ。 public class ProductClass { public ProductClass(IHttpClientFactory httpClientFactory) { HttpClientFactory = httpClientFactory; } public IHttpClientFactory HttpClientFactory { get; } public async Task<ResponseObject> Get() { using var client = HttpClientFactory.CreateClient("ClientName"); using var request = new HttpRequestMessage(HttpMethod.Get, "/request/path"); var response = await client.SendAsync(request).ConfigureAwait(false); if (!response.IsSuccessStatusCode) { return null; } var json = await response.Content.ReadAsStringAsync().ConfigureAwait(false); return JsonConvert.DeserializeObject<ResponseObject>(json); } } テストコードはこうなる。 ポイントは Moq.Protected を使って HttpMessageHandler をモックすること。 [TestFixture] [TestOf(typeof(ProductClass))] public class ProductClassTests { [Test] public async Task Get() { var responseObject = new ResponseObject { SomeProperty = "value" }; var mockHttpMessageHandler = new Mock<HttpMessageHandler>(); mockHttpMessageHandler.Protected() .Setup<Task<HttpResponseMessage>>( "SendAsync", ItExpr.IsAny<HttpRequestMessage>(), ItExpr.IsAny<CancellationToken>()) .Returns(() => { var response = new HttpResponseMessage { StatusCode = HttpStatusCode.OK, Content = new StringContent(JsonSerializer.Serialize(responseObject)) }; return Task.FromResult(response); }); var baseUrl = "https://test.example.jp"; var httpClient = new HttpClient(mockHttpMessageHandler.Object) { BaseAddress = new Uri(baseUrl) }; var mockHttpClientFactory = new Mock<IHttpClientFactory>(); mockHttpClientFactory.Setup(m => m.CreateClient(It.IsAny<string>())).Returns(httpClient); var targetClient = new MedicalInstituteClient(mockHttpClientFactory.Object); var result = await targetClient.Get(); Assert.That(result, Is.Not.Null); Assert.That(result.SomeProperty, Is.EqualTo("value")); mockHttpMessageHandler.Protected() .Verify( "SendAsync", Times.Once(), ItExpr.Is<HttpRequestMessage>( request => request.Method == HttpMethod.Get && request.RequestUri == new Uri($"{baseUrl}/request/path")), ItExpr.IsAny<CancellationToken>()); mockHttpClientFactory.Verify(m => m.CreateClient(It.Is<string>(s => s == "ClientName")), Times.Once()); } } 参考サイト Mocking HttpClient SendAsync
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

.NET5.0製のREST APIサーバーを、AWS CodePipelineでビルドしてコンテナ化してECSでデプロイする。

先日、個人で開発している.NET5.0製のREST APIサーバーをAWS上で動かすにあたり、コンテナ化してECSで管理することにしました。AWSが用意してくれている仕組みに頼ることで、高い品質と高い作業効率を両立できるためです。 この記事では、その際行った設定の概要と設定について記載します。 概要 構成と作業の流れ APIサーバーの構成は下記のようになります。  Internet → CloudFront → EC2(ECS管理) → RDS 参照専用のAPIのため、CloudFrontでキャッシュが利くようにしています。それ以外はAPIサーバーでよく用いられる構成と思います。 このうちの"EC2(ECS管理)"とある個所で、.NET5.0製のコードを動かす。それが今回の目的です。 作業の流れは次のようになります。 GitHub上のコードを取得する 取得したコードをビルドし、さらにコンテナ化する 作成したコンテナをデプロイする この3点をAWS CodePipelineで実現しました。 設定の実際 CodePipelineについて 前置きとして、CodePipelineについて少し触れます。 GithubにPushしたソースコードをビルドしてデプロイするまでの全作業は、CodePipelineで行えます。ですが、実際はビルドをCodeBuildが、デプロイをECSが行っています。 CodePipeline <>--- CodeBuild <>--- ECS 先に記載した作業も、動いているサービスはそれぞれ次のようになります。 GitHub上のコードを取得する ← CodePipeline 取得したコードをビルドし、さらにコンテナ化する ← CodeBuild 作成したコンテナをデプロイする ← ECS したがってCodePipelineでコンテナを用いた自動ビルドを行うためには、CodeBuildとECSも扱える必要があります。初めて扱う場合、いきなりCodePipelineをフルで動かそうとすると大変なため、まずCodeBuild単体、ECS単体を扱えるようになってから挑戦するほうが良いと思います。 なお、今回の記事ではECSやECR、S3やEC2等の設定について記載しませんが、CodePipelineの設定を開始した時点でそれらはすべて用意済の状態です。 1. GitHub上のコードを取得する まず、CodePipelineの名前・Role・CodePipelineが作業用に使うS3バケットを指定した後、ソースコードの取得方法について設定します。Githubを使っている場合はOAuth経由で簡単に設定できます。手なりに進めるだけのため、ここで詰まることはそうそうないと思います。 ちなみに、CodePipelineでは、インプットにあたるソースコード群もアウトプットにあたる生成物(.Net5.0ならXXX.DLL等ですね)も両方 "アーティファクト"と呼びます。前者がSource Artifactや入力アーティファクト、後者がBuild Artifactや出力アーティファクトと呼ばれるようですが、設定画面上ではただアーティファクトとだけ記載されるため入力なのか出力なのか判別し難い事態にしばしば遭遇します。当然ながら入力と出力を勘違いするとひどい結果になるため、設定中は注意が必要です。 2. 取得したコードをビルドし、さらにコンテナ化する CodeBuildの設定を行います。コードのビルドからコンテナ化、ECRへの保存までCodeBuildで行えます。 今回、CodeBuildの環境イメージ、コードをビルドするためのコンテナ、ビルドした生成物を起動するためのコンテナとして、それぞれ次のものを用いました。 環境イメージ aws/codebuild/amazonlinux2-aarch64-standard:2.0 (AWS公式のまま) ビルド用コンテナ mcr.microsoft.com/dotnet/sdk:5.0.400-focal-arm64v8 実行用コンテナ mcr.microsoft.com/dotnet/aspnet:5.0-alpine-arm64v8 いずれもARMアーキテクチャタイプのコンテナである点にご注意ください。私はt4g系のEC2で動かしているためこうなっていますが、x64のインスタンスを利用している方はx64を使う必要があるはずです。 Dockerfile.arm64 コンテナをビルドするためのDockerfileは次のようになっています。ファイル名はDockerfile.arm64です。 # https://github.com/dotnet/dotnet-docker/blob/main/samples/aspnetapp/Dockerfile.alpine-arm64 FROM mcr.microsoft.com/dotnet/sdk:5.0.400-focal-arm64v8 AS build WORKDIR /source # copy csproj and restore as distinct layers COPY src/WritingApi/*.csproj . RUN dotnet restore -r linux-musl-arm64 # copy and publish app and libraries COPY src/WritingApi/ . RUN dotnet publish -c release -o /app -r linux-musl-arm64 --self-contained false # final stage/image FROM mcr.microsoft.com/dotnet/aspnet:5.0-alpine-arm64v8 WORKDIR /app RUN apk --no-cache add curl COPY --from=build /app . # See: https://github.com/dotnet/announcements/issues/20 # Uncomment to enable globalization APIs (or delete) # ENV \ # DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=false \ # LC_ALL=en_US.UTF-8 \ # LANG=en_US.UTF-8 # RUN apk add --no-cache icu-libs ENTRYPOINT ["./WritingApi"] 公式のサンプルを参考にしつつ、自分の環境に合わせて作りました。 ディレクトリ構成 ディレクトリ構成は下記となります(抜粋)。 ./ │ buildspec.yml │ Dockerfile.arm64 │ Dockerfile.x64 │ imagedefinitions.json │ └─src │ WritingApi.sln │ └─WritingApi │ appsettings.Development.json │ appsettings.json │ appsettings.Production.json │ Program.cs │ Startup.cs │ WritingApi.csproj │ ..... buildspec.yml CodeBuildで用いるbuildspec.ymlは下記の内容となっています。 version: 0.2 phases: build: commands: - echo Build started on `date` - docker build -t writing-tools-api -f Dockerfile.arm64 --platform linux/arm64 . post_build: commands: - aws ecr get-login-password --region ap-northeast-1 | docker login --username AWS --password-stdin 314544748592.dkr.ecr.ap-northeast-1.amazonaws.com - docker tag writing-tools-api:latest 314544748592.dkr.ecr.ap-northeast-1.amazonaws.com/writing-tools-api:latest - docker push 314544748592.dkr.ecr.ap-northeast-1.amazonaws.com/writing-tools-api:latest 余談ながら、開発時は最初からbuildspec.ymlをgithubにpushするのではなく、まず"ビルドコマンドの挿入"を使いました。ある程度固まってからbuildspec.ymlに保存しました。このほうがフィードバックが早くてTry&Errorを繰り返しやすい上、gitの履歴も汚さずにすむためです。 3. 作成したコンテナをデプロイする デプロイの設定を行います。デプロイ作業はECSを通して行われるため、デプロイプロバイダーもECSを選んでいます。 imagedefinitions.json イメージ定義ファイルは下記の内容になっています。今のところlatestで運用しているためimageUriのタグはlatestです。 [ { "name": "WritingToolsApi", "imageUri": "314544748592.dkr.ecr.ap-northeast-1.amazonaws.com/writing-tools-api:latest" } ] nameにはECSのタスクで設定したコンテナ名を入力します。 以上。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む