【C#/ASP.NET Web API】ASP.NET Web APIにHTTPSを適用

# 概要
ASP.NET Web APIにHTTPSを適用して、下記を実行する。

・ IIS Express上で動作確認する。
・ IIS上に配置する。
・ IIS上のWeb APIをクライアントアプリでコールする。

# 動作環境
## IDE
Visual Studio Community 2017 15.9.36
・ ASP.NET and Web Tools 2017 15.9.04012.0
・ ASP.NET Core Razor Language Services 15.8.31590
・ ASP.NET Web Frameworks and Tools 5.2.61435.0

## Application
サーバアプリケーション(.NET Framework 4.6.2)
・ C#/ASP.NET Web　API 
クライアントアプリケーション(.NET Framework 4.6.2)
・ C#/Consoleアプリ

## ブラウザ
Google Chrome 91.0.4472.164

# サーバーアプリケーションを作成する(HTTPS適用)
1. 評価用APIを作成する。
2. AuthorizationFilterAttributeを継承したカスタムフィルタクラスを定義する。
3. カスタムフィルタをAPIConfigに登録する。
4. Visual Studioの開発サーバのHTTPSサーポートの有効化する 。

## 1. 評価用APIを作成する

### データモデルクラスを定義する

~~~C#:Product.cs
namespace WebServer.Models
{
    /// <summary>
    /// 製品
    /// </summary>
    public class Product
    {
        /// <summary>
        /// ID
        /// </summary>
        public int Id { get; set; }
        /// <summary>
        /// 名前
        /// </summary>
        public string Name { get; set; }
        /// <summary>
        /// 価格
        /// </summary>
        public decimal Price { get; set; }
        /// <summary>
        /// カテゴリー
        /// </summary>
        public string Category { get; set; }
    }
}
~~~

### コントローラを定義する

~~~C#:ProductsController.cs
using System;
using System.Linq;
using System.Web.Http;
using WebServer.Models;

namespace WebServer.Controllers
{
    /// <summary>
    /// 製品のイベントハンドラ
    /// </summary>
    public class ProductsController : ApiController
    {
        /// <summary>
        /// 製品情報
        /// </summary>
        private static Product[] _products = new Product[]
        {
            new Product {Id = 1, Name = "Tomato Soup", Category = "Groceries", Price = 1},
            new Product {Id = 2, Name = "Yo-yo", Category = "Toys", Price = 3.75M},
            new Product {Id = 3, Name = "Hammer", Category = "Hardware", Price = 16.99M}
        };

        /// <summary>
        /// 製品情報をすべて取得する
        /// </summary>
        /// <returns></returns>
        public IHttpActionResult  GetAllProducts()
        {
            return Ok(_products);
        }

        /// <summary>
        /// 指定IDの製品情報を取得する
        /// </summary>
        /// <param name="id">識別子</param>
        /// <returns></returns>
        public IHttpActionResult GetProduct(int id)
        {
            var product = _products.FirstOrDefault(p => p.Id == id);
            if (product == null)
            {
                return NotFound();
            }
            return Ok(product);
        }

        /// <summary>
        /// 製品情報の更新
        /// </summary>
        /// <param name="product">更新する製品の情報</param>
        /// <returns>HTTP応答</returns>
        public IHttpActionResult PutProduct(Product product)
        {
            if (product == null)
            {
                return InternalServerError();
            }
            var resultProduct = _products.FirstOrDefault(p => p.Id == product.Id);
            if (resultProduct == null)
            {
                Array.Resize(ref _products, _products.Length + 1);
                _products[_products.Length - 1] = product;

                return Ok();
            }
            resultProduct.Id = product.Id;
            resultProduct.Name = product.Name;
            resultProduct.Category = product.Category;
            resultProduct.Price = product.Price;
            return Ok();
        }
    }
}
~~~

## 2. AuthorizationFilterAttributeを継承したカスタムフィルタクラスを定義する

~~~C#:RequireHttpsAttribute.cs
using System;
using System.Net;
using System.Net.Http;
using System.Web.Http.Controllers;
using System.Web.Http.Filters;

namespace WebServer
{
    /// <summary>
    /// HTTPSの認証フィルタ(カスタムクラス)
    /// </summary>
    public class RequireHttpsAttribute : AuthorizationFilterAttribute
    {
        /// <summary>
        /// 認証時に呼び出されるコールバック関数
        /// </summary>
        /// <param name="actionContext"></param>
        public override void OnAuthorization( HttpActionContext actionContext )
        {
            if (actionContext.Request.RequestUri.Scheme != Uri.UriSchemeHttps)
            {
                actionContext.Response = new HttpResponseMessage(HttpStatusCode.Forbidden)
                {
                    ReasonPhrase = "HTTPS Required"
                };
            }
            else
            {
                base.OnAuthorization(actionContext);
            }
        }
    }
}
~~~

## 3. カスタムフィルタをApiConfigに登録する
グローバルな設定として、HTTPSフィルタを適用する。すべてのWeb APIがHTTPS適用となる。

~~~~
config.Filters.Add(new RequireHttpsAttribute());
~~~~

~~~C#:WebApiConfig.cs
using System.Web.Http;
namespace WebServer
{
    public static class WebApiConfig
    {
        public static void Register(HttpConfiguration config)
        {
            // Web API の設定およびサービス

            // Web API ルート
            config.MapHttpAttributeRoutes();
            // HTTPSフィルターを登録する(すべてのWeb APIがHTTPS実行となる)
            config.Filters.Add(new RequireHttpsAttribute());

            config.Routes.MapHttpRoute(
                name: "DefaultApi",
                routeTemplate: "api/{controller}/{id}",
                defaults: new { id = RouteParameter.Optional }
            );
        }
    }
}
~~~

## 4. Visual　Studioの開発サーバのHTTPSサーポートの有効化
(1)VisualStudioでWeb APIソリューションを開く。
(2)ソリューションエクスプローラで[Web APIプロジェクト]を選択する。
(3)「プロパティウィンドウ」を表示する。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/156594/c140d1be-1fab-74da-b23e-f91db8a4e887.png)
(4)開発サーバのタブを展開して、[SSL有効化]を[True]にする。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/156594/305864e7-2854-1ac0-4295-d393f064a13e.png)
<b>SSL URLはHTTPS接続テストに使用するため、控えておく。<b/>

# IIS ExpressでHTTPS接続を動作テストする 

## HttpでWeb APIのGetメソッドをコールする
URL
http://localhost:59320/api/products

<b>実行結果:アクセスを拒否される</b>
 ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/156594/eeeaac4a-7608-f93d-0e36-cb3b118304b2.png)

## HttpSでWeb APIのGetメソッドをコールする

URL
https://localhost:44333/api/products

<b>実行結果：Web APIのGetメソッドに成功する</b>

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/156594/af49040d-f3fe-c09f-d2e2-7779c524665c.png)

# IISにデプロイする
下記記事の手順を実行する。
https://qiita.com/SY81517/items/347e86582054f8e92742

# IIS上のWeb APIをクライアントアプリでコールする

## クライアントアプリを作成する

~~~C#:Program.cs
using System;
using System.Net.Http;
using System.Text;
using Newtonsoft.Json;

namespace WebClient
{
    class Program
    {
        static void Main( string[] args )
        {
            var client = new WebApiClient();
            Console.WriteLine("=== GetProduct id=1 ===");
            client.GetProduct(id:1);
            Console.WriteLine();
            Console.WriteLine("=== GetAllProduct ===");
            client.GetAllProducts();
            Console.WriteLine();
            Console.WriteLine("=== PutProduct ===");
            var product = new Product()
            {
                Id = 4,
                Name = "Tomato",
                Category = "Vegetable",
                Price = 1.01M
            };
            client.PutProduct(product);

            Console.WriteLine("=== GetAllProduct ===");
            client.GetAllProducts();
        }
    }

    public class WebApiClient
    {
        static readonly HttpClient Client = new HttpClient()
        {
            BaseAddress = new Uri("https://localhost:59320/api/")
        };

        /// <summary>
        /// 製品情報をすべて取得する
        /// </summary>
        public void GetAllProducts()
        {
            var response = Client.GetAsync("products").GetAwaiter().GetResult();
            response.EnsureSuccessStatusCode();
            var responseBody = response.Content.ReadAsStringAsync().GetAwaiter().GetResult();
            Console.WriteLine(responseBody);
        }

        /// <summary>
        /// 指定IDの製品情報を取得する
        /// </summary>
        /// <param name="id">識別子</param>
        public void GetProduct(int id)
        {
            var response = Client.GetAsync($"products/{id}").GetAwaiter().GetResult();
            response.EnsureSuccessStatusCode();
            var responseBody = response.Content.ReadAsStringAsync().GetAwaiter().GetResult();
            Console.WriteLine(responseBody);
        }

        /// <summary>
        /// 製品情報の更新
        /// </summary>
        /// <param name="product">更新する製品の情報</param>
        public void PutProduct(Product product)
        {
            var jsonString = JsonConvert.SerializeObject(product);
            var content = new StringContent(jsonString, Encoding.UTF8, "application/json");
            var response = Client.PutAsync("products",content).GetAwaiter().GetResult();
            response.EnsureSuccessStatusCode();
        }
    }

    /// <summary>
    /// 製品
    /// </summary>
    public class Product
    {
        /// <summary>
        /// ID
        /// </summary>
        public int Id { get; set; }
        /// <summary>
        /// 名前
        /// </summary>
        public string Name { get; set; }
        /// <summary>
        /// 価格
        /// </summary>
        public decimal Price { get; set; }
        /// <summary>
        /// カテゴリー
        /// </summary>
        public string Category { get; set; }
    }

}
~~~

## クライアントアプリを実行した結果
(1)GetProduct()でid=1に対応した製品情報を取得する。
(2)GetAllProduct()ですべての製品情報を取得する。
(3)PutProduct()で新たな製品情報を登録する(id=4)。
(4)再び、GetAllProduct()ですべての製品情報を取得する。id=4が登録されていることを確認する。

~~~~bash:実行結果画面
=== GetProduct id=1 ===
{"Id":1,"Name":"Tomato Soup","Price":1.0,"Category":"Groceries"}

=== GetAllProduct ===
[{"Id":1,"Name":"Tomato Soup","Price":1.0,"Category":"Groceries"},{"Id":2,"Name":"Yo-yo","Price":3.75,"Category":"Toys"},{"Id":3,"Name":"Hammer","Price":16.99,"Category":"Hardware"}]

=== PutProduct ===
=== GetAllProduct ===
[{"Id":1,"Name":"Tomato Soup","Price":1.0,"Category":"Groceries"},{"Id":2,"Name":"Yo-yo","Price":3.75,"Category":"Toys"},{"Id":3,"Name":"Hammer","Price":16.99,"Category":"Hardware"},{"Id":4,"Name":"Tomato","Price":1.01,"Category":"Vegetable"}]
続行するには何かキーを押してください . . .
~~~~

# WireSharkでhttps通信を監視してみる

## localhostを対象にする
「Adapter for loopback traffic capture」を選択する。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/156594/285f3153-c572-c2f7-eeb6-4fe935021dbd.png)

## ポート番号で表示フィルターをかける
httpsのポート番号を指定すること
tcp.port == 59320
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/156594/b29785e9-b1b6-a4e2-0d59-92d2ea8e1608.png)

## キャプチャーを取得する
(1)「キャプチャー」→「開始」を押す。
(2)クライアントを実行する。
(3)クライアントの処理が完了したら、「キャプチャー」→「停止」を押す。

## キャプチャーの結果を確認する。
TCP/TLS 1.2が交互に繰り返される。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/156594/1b7cce90-5291-de58-2bfe-33f05eb1b1bd.png)

Application Dataを見ると、通信内容が読み取れなくなっている。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/156594/11a02ed8-5591-1ba2-a3c4-896bcf0d9d5f.png)

# 参考にしたサイト

| No | リンク | 概要 |
|:-|:-|:-|
| 1  | [httpsとは?htttpとの違いとSSL暗号化の仕組み][1]  |httpsの仕組みに関する解説が分かりやすい |
|2   |[C# Corner How To Enable HTTPS In ASP.NET Web API][2] |ASP.NET Web APIにHTTPSを適用する手順の記事(英語)   |
|3   |[MicrosoftDoc Web  APIでのSSL操作][3]   |公式ドキュメントのSSL操作に関する記事   |

[1]:https://www.ssl.ph/compare/ssl/datalibrary/contents06.html
[2]:https://www.c-sharpcorner.com/article/how-to-enable-https-in-asp-net-web-api/
[3]:https://docs.microsoft.com/ja-jp/aspnet/web-api/overview/security/working-with-ssl-in-web-api