ASP.NET Core Web API に CORS を実装してみました。下の画像は検証のため別プロジェクトの MVC アプリから fetch API を使って Web API に要求を出してデータを取得し表示したものです。
開発環境の 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 の実装とは直接関係ないことですが、fetch メソッドの引数の要求 URL の書き方に注意すべきことがあったのでそれを書いておきます。
PUT と DELETE については、Web API 側のアクションメソッドの引数に int id が含まれているので、要求 URL に api/values/5 というように id を渡す必要があります。そうしないと、プリフライトリクエストは成功しますが、それを受けてブラウザが要求に行った際に応答が 405 Method Not Allowed、Allow: GET, POST となって失敗します。
何故それで Method Not Allowed となるのか、GET, POST でないとダメと言われるのかは分かりませんが・・・