WebSurfer's Home

トップ > Blog 1   |   Login
Filter by APML

Web API に CORS 実装 (CORE)

by WebSurfer 21. January 2023 22:50

ASP.NET Core Web API に CORS を実装してみました。下の画像は検証のため別プロジェクトの MVC アプリから fetch API を使って Web API に要求を出してデータを取得し表示したものです。

Web API に CORS 実装

開発環境の IIS Express で動かしているのでホストは同じ localhost ですが、ポートが異なるので要求はクロスドメインになります。上の画像では fetch API のメソッドが PUT なので、ブラウザがまずプリフライトリクエストを出し、その応答を見て要求を出してデータを取得しています。

ASP.NET Core の場合はフレームワーク組み込みの CORS 用のミドルウェアが用意されていて、それを Microsoft のドキュメント「ASP.NET Core でクロスオリジン要求 (CORS) を有効にする」に従って有効にします。

(ちなみに、.NET Framework 版の ASP.NET Web アプリでは、先の記事「クロスドメインの WCF サービス」で書いたように自力で実装していました)

具体的には、Visual Studio 2022 のテンプレートでフレームワーク .NET 6.0 で作った ASP.NET Web API アプリであれば、上に紹介した Microsoft のドキュメントの「名前付きポリシーとミドルウェアを使用した CORS」のセクションに従って Program.cs に以下のコードを追加するだけで CORS は有効になります。

var MyAllowSpecificOrigins = "_myAllowSpecificOrigins";
builder.Services.AddCors(options =>
{
    options.AddPolicy(name: MyAllowSpecificOrigins,
        policy =>
        {
            policy.WithOrigins("https://localhost:44343")
                  .AllowAnyHeader()
                  .AllowAnyMethod();
        });
});

// ・・・中略・・・

app.UseCors(MyAllowSpecificOrigins);

「名前付き」にすると、[EnableCors("{Policy String}")] 属性をコントローラーに付与しないと CORS は有効にならないと思っていたがそうではなかったです。コントローラーには何も付与しなくても上の設定だけで CORS は有効になります。

WithOrigins メソッドに設定した https://localhost:44343 は CORS でのアクセスを許可する要求元です。すべての要求元を許可する AllowAnyOrigin メソッドもあります。詳しくは Microsoft のドキュメント「CorsPolicyBuilder クラス」を見てください。

AllowAnyHeader メソッド、AllowAnyMethod メソッドはプリフライトが必要になる場合は必要です(Any ではなく特定の Header, Method を指定することもできます)。それらが無いとプリフライトの応答ヘッダに、

Access-Control-Allow-Headers
Access-Control-Allow-Methods

・・・が含まれないのでプリフライトの後の要求がブラウザから出ず失敗します。

上の画像のプリフライトリクエストの要求・応答ヘッダを Fiddler でキャプチャした画像を下に貼っておきます。

プリフライトリクエスト

要求側はブラウザの仕事で開発者は何もする必要はありません。プリフライトが必要か否かもブラウザが判断して CORS に必要な要求を出してくれます。

Response Headers の Security の項目は、要求を受けてサーバー側のミドルウェア(上のコード参照)が設定したものです。

参考に検証に使ったコードを下に載せておきます。Windows 10 22H2 の Edge 109.0.1518.61, Chrome 109.0.5414.75, Firefox 109.0, Opera 94.0.4606.65 で確認しました。

Web API

using Microsoft.AspNetCore.Mvc;

namespace WebApi.Controllers
{
    public class Hero
    {
        public int Id { get; set; }
        public string? Name { get; set; }
    }

    [Route("api/[controller]")]
    [ApiController]
    public class ValuesController : ControllerBase
    {
        private List<Hero> heroes = new List<Hero> {
              new Hero {Id = 1, Name = "スーパーマン"},
              new Hero {Id = 2, Name = "バットマン"},
              new Hero {Id = 3, Name = "ウェブマトリクスマン"},
              new Hero {Id = 4, Name = "チャッカマン"},
              new Hero {Id = 5, Name = "スライムマン"}
        };

        // GET: api/values (Read...すべてのレコードを取得)
        [HttpGet]
        public List<Hero> Get()
        {
            return heroes;
        }

        // GET api/values/5 (Read...id 指定のレコード取得)
        [HttpGet("{id}")]
        public Hero Get(int id)
        {
            return heroes[id - 1];
        }

        // POST api/values (Create...レコード追加)
        [HttpPost]
        public List<Hero> Post([FromBody] Hero postedHero)
        {
            heroes.Add(postedHero);
            return heroes;
        }

        // PUT api/values/5 (Update...id 指定のレコード更新)
        [HttpPut("{id}")]
        public List<Hero> Put(int id, [FromBody] Hero postedHero)
        {
            heroes[id - 1].Name = postedHero.Name;
            return heroes;
        }

        // DELETE api/values/5 (Delete...id 指定のレコード削除)
        [HttpDelete("{id}")]
        public List<Hero> Delete(int id)
        {
            heroes.RemoveAt(id - 1);
            return heroes;
        }
    }
}

クライアント側 (MVC の View)

@{
    ViewData["Title"] = "ApiCors";
}

<h1>ApiCors</h1>

<input type="button" value="READ ALL" onclick="apiHeroesGet();" />
<input type="button" value="READ 5" onclick="apiHeroesGet5();" />
<input type="button" value="UPDATE 5" onclick="apiHeroesPut5();" />
<input type="button" value="DELETE 5" onclick="apiHeroesDelete5();" />
<input type="button" value="CREATE 6" onclick="apiHeroesPost();" />

<ul id="heroes"></ul>

@section Scripts {
    <script type="text/javascript">
        //<![CDATA[
        const url = "https://localhost:44371/api/values"; // IIS Express
        //const url = "https://localhost:7216/api/values";    //Kestrel
        const elem = document.querySelector("#heroes");

        const apiHeroesGet = async () => {
            const response = await fetch(url);
            if (response.ok) {
                const data = await response.json();
                elem.innerHTML = "";
                for (let i = 0; i < data.length; i++) {
                    elem.insertAdjacentHTML("beforeend", 
                        `<li>${data[i].id}: ${data[i].name}</li>`);
                }
            } else {
                elem.innerHTML = "失敗";
            }
        };

        const apiHeroesGet5 = async () => {
            const response = await fetch(url + "/5");
            if (response.ok) {
                const data = await response.json();
                elem.innerHTML = "";
                elem.insertAdjacentHTML("beforeend",
                    `<li>${data.id}: ${data.name}</li>`);
            } else {
                elem.innerHTML = "失敗";
            }
        };

        const apiHeroesPut5 = async () => {
            const params = {
                method: "PUT",
                body: '{"Id":5,"Name":"Updated Hero"}',
                headers: { 'Content-Type': 'application/json' }
            }
            const response = await fetch(url + "/5", params);
            if (response.ok) {
                const data = await response.json();
                elem.innerHTML = "";
                for (let i = 0; i < data.length; i++) {
                    elem.insertAdjacentHTML("beforeend",
                        `<li>${data[i].id}: ${data[i].name}</li>`);
                }
            } else {
                elem.innerHTML = "失敗";
            }
        };

        const apiHeroesDelete5 = async () => {
            const params = {
                method: "DELETE"
            }
            const response = await fetch(url + "/5", params);
            if (response.ok) {
                const data = await response.json();
                elem.innerHTML = "";
                for (let i = 0; i < data.length; i++) {
                    elem.insertAdjacentHTML("beforeend",
                        `<li>${data[i].id}: ${data[i].name}</li>`);
                }
            } else {
                elem.innerHTML = "失敗";
            }
        };

        const apiHeroesPost = async () => {
            const params = {
                method: "POST",
                body: '{"Id":6,"Name":"Created Hero"}',
                headers: { 'Content-Type': 'application/json' }
            }
            const response = await fetch(url, params);
            if (response.ok) {
                const data = await response.json();
                elem.innerHTML = "";
                for (let i = 0; i < data.length; i++)
                {
                    elem.insertAdjacentHTML("beforeend", 
                        `<li>${data[i].id}: ${data[i].name}</li>`);
                }
            } else {
                elem.innerHTML = "失敗";
            }
        };

        //]]>
    </script>
}

本題の CORS の実装とは直接関係ないことですが、予想外のことがあったのでそれを書いておきます。

PUT については Web API 側のアクションメソッドの引数�� int id が含まれているので、要求 URL に api/values/5 というように id を渡さないと 405 Method Not Allowed、Allow: GET, POST となって失敗します。

何故それで Method Not Allowed となるのか、GET, POST でないとダメと言われるのかは分かりませんが・・・

Tags: ,

Web API

Web API に XML データを送信

by WebSurfer 9. August 2020 15:08

ASP.NET Web API 2 に XML 形式のデータを送信し、サーバー側のアクションメソッドで受け取る方法を書きます。(注: .NET Framework 版の話です。Core 版は未検証・未確認)

UTF-8 の場合

ASP.NET Web API で XML データを送信するというのは需要がなさそうで、あまり役には立たないかもしれませんが、せっかく考えたことなので整理して備忘録として残しておくことにしました。

(1) UTF-8 の場合

元の話は Teratail のスレッド「ASP.NET Web APIでPOSTされたXMLデータをXML形式で取得する方法」のものです。

サンプル XML データは以下の通りです。

<?xml version="1.0" encoding="utf-8"?>
<sample code="1.0">
  <data>
   <id>100</id>
  </data>
  <value>
    <name>日本語</name>
    <version>1.0.0</version>
  </value>
</sample>

この XML データを ASP.NET Web API に POST 送信し、サーバー側でアクションメソッド Post(SampleData value) の引数 value にモデルバインドするにはどうしたらよいかという話です。SampleData クラスの定義は以下の通り。属性を付与した理由は下に書きます。

namespace WebApi.Models
{
    [System.Xml.Serialization.XmlRoot("sample")]
    public class SampleData
    {
        [System.Xml.Serialization.XmlAttribute("code")]
        public string Code { get; set; }

        [System.Xml.Serialization.XmlElement("data")]
        public Data Data { get; set; }

        [System.Xml.Serialization.XmlElement("value")]
        public Value Value { get; set; }
    }

    public class Data
    {
        [System.Xml.Serialization.XmlElement("id")]
        public string Id { get; set; }
    }

    public class Value
    {
        [System.Xml.Serialization.XmlElement("name")]
        public string Name { get; set; }

        [System.Xml.Serialization.XmlElement("version")]
        public string Version { get; set; }
    }
}

XML データの文字コードが UTF-8 であれば以下の 3 つの設定でこの記事の一番上の画像の通りモデルバインドできます。

  1. 要求ヘッダに Content-Type: application/xml; charset=UTF-8 の設定

    Microsoft のドキュメント How WebAPI does Parameter Binding には content-type の設定だけで済みそうなことが書いてありますが、自分が試した限りでは下の 2, 3 も必要でした。
  2. App_Start/WebApiConfig.cs へ以下の設定の追加

    config.Formatters.XmlFormatter.UseXmlSerializer = true;  
  3. モデルのクラス、プロパティに XmlRoot, XmlAttribute, XmlElement 属性の付与

    xml コードの要素名とそれをバインドするクラス、プロパティ名を関連付けるために必要です。たとえ同名でも、上の例のように大文字小文字の違いがある場合は、プロパティにXmlElement 属性を付与してその引数に xml の要素名を設定しないとバインドされません。また、xml コードの属性値(この記事の例では code)をバインドする場合は、それに該当するプロパティへの XmlAttribute 属性の付与が必要です。

(2) Shift_JIS の場合

XML データの文字コードが Shift_JIS の場合はどうでしょう? (これも Teratail のスレッド「ASP.NET Web API POSTされた文字エンコードShift_JISのXMLデータをモデルバインドできない」の話です)

上の「(1) UTF-8 の場合」のような方法ではうまくいかないようです(null がバインドされる)。もちろん XML データを encoding="shift_jis" とし、要求ヘッダを Content-Type: application/xml; charset=shift_jis としたのですがダメでした。

検索するなどして調べてみましたが成果がなかったです。Shift_JIS ではヒットしないので UTF-16 に範囲を広げてググってみましたが、stackoverflow のスレッド Web API not able to bind model for POST with utf-16 encoded XML など、できなかったという記事しか見つかりませんでした。

そこを何とかするには、Microsoft のドキュメント How WebAPI does Parameter Binding の最初の方に書いてあるように、HttpRequestMessage クラスを引数に持つメソッドを使う他には手がなさそうな感じです。

HttpRequestMessage.Content プロパティで HttpContent オブジェクトを取得し、HttpContent.ReadAsStringAsync メソッドで POST されてきた XML 文字列を読んで、XDocument.Load メソッドで XDocument オブジェクトを作ってそれを操作するようにしてみました。

なお、この方法を取った場合、上の「UTF-8 の場合」で行った「2. App_Start/WebApiConfig.cs へ以下の設定の追加」と「3. モデルのクラス、プロパティに XmlRoot, XmlAttribute, XmlElement 属性の付与」は必要ありません。

検証は以下のようにしました。

まず Fiddler の Composer を使って、要求ヘッダに Content-Type: application/xml; charset=shift_jis を付与し、Request Body に XML データを設定して送信します。

Fiddler の Composer で送信

Fiddler の Inspectors で HexView を選択し、Request Body に設定して送信した XML データの 16 進数表現をチェックします。XML データの中の <name>日本語</name> の「日本語」の文字コードは 93 FA 96 7B 8C EA と Shift_JIS になっているのが分かります。

要求ヘッダとボディの 16 進数データ

Visual Studio のデバッガでアクションメソッドの Local 変数を確認します。「日本語」は文字化けせず期待通り取得できています。

アクションメソッドの Local 変数

ちょっと予想外だったのは、Fiddler の Composer の Request Body に書いた「日本語」の文字コードが Shift_JIS になることと、HttpContent.ReadAsStringAsync メソッドがそれを正しく「日本語」と読んでくれることです。そんなに都合よくできているというのが信じ難く、何か見落としがあるかも。(汗)

Tags: , , ,

Web API

Web API と要求ヘッダの application/xml

by WebSurfer 7. June 2020 14:12

.NET Framework 版の ASP.NET Web API を要求する際、要求ヘッダの Accept に application/xml が含まれると応答は JSON ではなく XML になります。

ASP.NET Web API の応答

知ってました? 実は自分は知らなかったです。(汗) ちなみに、MVC の Json メソッド、および Core 3.1 Web API と MVC の Json メソッドは、要求ヘッダの Accept に application/xml が含まれていても JSON が返ってきます。

Web API の応答をチェックするには Postman などのツールを使うのが一般的なようです。でも、GET 要求ならブラウザのアドレスバーに直接 URL を入力して応答を見ることでも十分だったので自分は IE11 を使ってそうしてました。それで期待した通り JSON 文字列が返ってきてました。

ところが、Chrome, Edge, Firefox では応答が JSON ではなく XML になってしまいます。理由は要求ヘッダの Accept に application/xml が含まれるからのようです。ハマったのは上の画像のようなサーバーエラーとなってしまったことです(HTTP 500 応答が返ってきます。上の画像は形式は XML ですが中身はエラーメッセージです)。

エラーメッセージ "The 'ObjectContent`1' type failed to serialize the response body for content type 'application/xml; charset=utf-8'." によるとシリアライズに失敗したということです。

IE11 や jQuery ajax を使った検証ではシリアライズには問題はないことは確認しており、ブラウザに Chrome などを使ったからと言ってそれがシリアライズの部分に影響があるとは考えられなかったです。何故なのでしょう?

循環参照の問題でした。

上の画像の InnerException のエラーメッセージにある Blog_E36679EB... D5AD0 という文字列が、先の記事「JSON シリアライズの際の循環参照エラー」にもあったのを思い出して気が付きました。

.NET Framework 版の ASP.NET Web API の JSON シリアライザは Newtonsoft の Json.NET のもので、JsonIgnoreAttribute クラスという属性が利用できます。なので、問題のプロパティに [JsonIgnore] を付与してシリアライズの際の循環参照エラーは回避していました。

でも [JsonIgnore] が有効なのは JSON にシリアライズするときのみで、XML にシリアライズするときは循環参照の問題は回避できません。

Chrome, Edge などを使った時は、ASP.NET Web API は要求ヘッダの Accept に application/xml が含まれるのを見て、XML にシリアライズしようとして循環参照の問題に陥ったということのようです。

Tags: , , ,

Web API

About this blog

2010年5月にこのブログを立ち上げました。その後ブログ2を追加し、ここはプログラミング関係、ブログ2はそれ以外のトピックスに分けました。

Calendar

<<  February 2023  >>
MoTuWeThFrSaSu
303112345
6789101112
13141516171819
20212223242526
272812345
6789101112

View posts in large calendar