まだプログラマーですが何か?

プログラマーネタとアスリートネタ中心。たまに作成したウェブサービス関連の話も http://twitter.com/dotnsf

タグ:nodejs

マーメイド記法を勉強している中で生じたアイデアを実装したら、なんとなくノーコード/ローコードっぽい仕上がりになり、自分で使っていても便利に感じたので公開して紹介することにしました。


【マーメイド記法とは】
最初に「マーメイド記法」について紹介します。マーメイド記法とは「特定のルールでテキストを記述することでフローチャート図を作る」ための記述ルールです。マークダウン記法の一種ですが、実際にマークダウンの中に含める形で使うこともできるようなサービスもあります。有名どころでは今年のバレンタインデーに GitHub 内のマークダウンファイル(.md)内にマーメイド記法で記述された部分があると、フロー図に変換して表示されるようになりました。

例えばこんな内容を含むマークダウンファイルを GitHub にコミットします(1行目はマークダウンそのものですが、3行目の ``` (バッククォート3つ)から ``` までの部分がマーメイド記法で書かれています。後述しますが、もう少し情報量を増やした書き方もあります):
2202110601

(↑なんとなくわかりますかね? AからB、AからC、BからD、CからD、という4つの流れが定義されているものです。先頭行の "graph DB;" は「フロー図を上から下(TopDown) に描く」という意味です)

そしてコミットされたファイルをブラウザで GitHub から見ると、マーメイド記法で書かれた部分がこのようにフロー図に変換されて表示されます:
2202110602



このマーメイド記法はウェブアプリケーション設計時の「画面遷移の定義」の表現に使えると思いました。例えばこんな感じのウェブアプリケーションの画面遷移を考えた時に・・・
 ・最初にログイン画面
 ・ログイン後にトップ画面
 ・トップ画面からA、B、Cの3つの画面に遷移できる

マーメイド記法だとこのように表現できると思ったのです:
```mermaid
  graph TD;
  Login --> Top;
  Top --> A;
  Top --> B;
  Top --> C;
```

また上でも少し触れたのですが、このマーメイド記法はかなり単純な最小限の情報しか含んでいませんが、もう少し情報量を増やしてこのような書き方もできます:
```mermaid
  graph TD;
  Login["ログイン"] --"/"--> Top["トップ"];
  Top --"/info"--> A["商品情報"];
  Top --"/download"--> B["ダウンロード"];
  Top --"/about"--> C["○○について"];
```


このマーメイド記法をフロー化するとこのようになります:
2022110503


A や B といったノードの名前そのままではなく、["~~"] で括られた部分がノードの表示用の名称として表示されています(一度 Top["トップ"] と表示名を定義しておくことで、2回目からはただ Top と書くだけでよくなります)。加えて矢印にも名前が付けることで、なんとなく UI のないワイヤーフレームっぽくなりました。どのページからどのページへ、なんという URL パスで進むようになるか、というのが視覚化できます(上の図の「ログイン」ページに相当する URL パスがないのでまだ不完全ですが、詳しくは後述します)。


マーメイド記法はフロー図だけでなく、色々な種類のグラフを描くことができます。ただマーメイド記法そのものの書き方についてはこのブログエントリの目的ではないので、他のサイトでの紹介を参照いただきたいです。私自身はこのページに多くお世話になりました:
https://zenn.dev/okazuki/articles/learning-mermaid


【アイデアと、実装して作ったツール】
このマーメイド記法を使って作ったファイルを使って、以下のような3つの機能を作れないだろうか、というアイデアを思いつきました:
 (1)ワイヤーフレームの全画面を表示できるようなウェブアプリ化
 (2)ワイヤーフレームの中で DB 定義が必要と思われる部分を抜き出して API 化
 (3)(おまけ)マーメイド記法で書かれた部分をフロー図化

(1)は上で示したようなフローを実現できるような最小限のウェブアプリを、マーメイド記述されたファイルから、各ページの URL パスをそのまま再現する形で自動生成する、というものです。 各ページの UI は最小限ですが、例えば上の例であれば「ログイン」ページを表示すると「トップ」ページに遷移できるようなリンクがあり、「トップ」ページを表示すると「商品情報」、「ダウンロード」、「○○について」という3つのリンクがあって、それぞれのページに繋がっている、というものです。フロー定義から画面実装までを自動化して、生成された HTML テンプレートをカスタマイズすることで見た目も定義できるようにする、というものです。

(1)についてはもう1つ、(2)にも関わるのですが例えばマーメイド記述された中に "/(複数形)""/(単数形)/:id" というフォーマットに合致する2つのパスが含まれていた場合は、前者を一覧ページ、後者を詳細ページとみなして、それに近い UI を用意する、という機能を持たせました。例えば、マーメイド記述が
```mermaid
  graph TD;
  Login["ログイン"] --"/"--> Top["トップ"];
  Top --"/users"--> A["ユーザー一覧"];
  A --"/user/:id"--> B["ユーザー"];
    :
```

のようになっていた場合、単に「ユーザー一覧」画面と「ユーザー」画面がリンクでつながっているのではなく、「「ユーザー一覧」は「複数のユーザーの一覧画面」」で「「ユーザー」画面は「ユーザー ID が :id となる単数のユーザーの詳細画面」」であることを識別できるようにしました。自動生成される画面も単なるリンクで繋げるのではなく、「ユーザー一覧」画面にはユーザーが複数表示され、それぞれのユーザーがクリックできるようになっていて、クリックするとクリックしたユーザーの詳細ページに遷移する、というものです。

そして(2)の機能は、この「ユーザー一覧」画面と「ユーザー詳細」画面があるということは「ユーザーのデータを扱う必要がある」と判断し、ユーザー情報を CRUD できるような API (と Swagger ドキュメントと DDL)を自動生成する機能です。具体的には、
 ・POST /user (一人のユーザーを新規作成する)
 ・POST /users (複数のユーザーをバルクインサートする)
 ・GET /user/:id (一人のユーザーの情報を ID を指定して取得する)
 ・GET /users (複数のユーザーの情報をまとめて取得する)
 ・PUT /user/:id (ID を指定したユーザーの情報を更新する)
 ・DELETE /user/:id (ID を指定したユーザーを削除する)
 ・DELETE /users (複数のユーザーをまとめて削除する)
という7つの CRUD API を作る、というものです。加えてこれらの API のデータ格納先となるテーブルの DDL 定義(id と name の2列だけですが・・)と、実際に動作確認できる Swagger API ドキュメントも作成します。


最後におまけの(3)、これはマーメイド記法で表記した内容を GitHub にコミットしなくても見ることができるよう、ローカルでマーメイドファイルを表示できるようにするツールもあると便利だったので、自分用に作ってみました。

こうしてできあがったのがこのツールで、現在は Node.js 向けソースコードを GitHub で公開しています:
https://github.com/dotnsf/web-template


【ツールの使い方】
このツールを使う場合は、まずは Node.js の環境を用意する必要があります。必要に応じてインストールしてください:
https://nodejs.org/ja/


次に git でソースコードを clone して、"npm install" で動作に必要なライブラリをインストールします:
$ git clone https://github.com/dotnsf/web-template
$ cd web-template
$ npm install

そしてアプリケーションや API サーバーのソースコードを生成するためのマーメイドファイルを用意します。ソースコード内に mermaid_sample.md というファイルが含まれていますので、これをそのまま使っても構いませんし、自分で用意しても構いません。

ただ自分でマークダウンファイルを用意する場合は次の点に注意してください:
  • 各ノード(ページ)の表示名を定義する場合は ["~~"] という形で記述する(マーメイドのルールでは ("~~") など、丸括弧で括っても正しく判断されますが、このツールを使う場合は ["~~"] という四角括弧で表示名を括ってください。
  • 全てのノート(ページ)を表示するための URL パスが定義できているようにマーメイドファイルを用意してください。

例えば上で紹介したこのマーメイドファイル例:
```mermaid
  graph TD;
  Login["ログイン"] --"/"--> Top["トップ"];
  Top --"/info"--> A["商品情報"];
  Top --"/download"--> B["ダウンロード"];
  Top --"/about"--> C["○○について"];
```

は一番上にある Login["ログイン"] ページを表示するための URL がどこにも定義されていません(ので、このまま実行してもエラーになります)。マーメイドファイルのどこかに(例えば以下の例のように)このログインページへの URL パスを追加する必要があります(mermaid_sample.md ファイルは初めからこのようになっています):
```mermaid
  graph TD;
  Login["ログイン"] --"/"--> Top["トップ"];
  Top --"/info"--> A["商品情報"];
  Top --"/download"--> B["ダウンロード"];
  Top --"/about"--> C["○○について"];
  Top --"/login"--> Login;
```

最後にトップページからログインページに遷移するためのパス /login を定義する行を加えました。これで全てのノード(ページ)の URL パスが定義されたことになります。もし実際のアプリケーションではトップページからログインページへのリンクが不要であれば、生成されたトップページからログインページへのリンクボタンを手動で削除してください(具体的には以下で説明します)。

なお最初の状態のマーメイドサンプルファイル(mermaid_sample.md)の内容は以下のようになっています:
# mermaid

```mermaid
  graph TD;
      Login["ログイン"] --"/"--> Top["トップ"];
      Top --"/items"--> Items["アイテム一覧"];
      Items --"/item/:id"--> Item["アイテム詳細"];
      Top --"/users"--> Users["ユーザー一覧"];
      Users --"/user/:id"--> User["ユーザー詳細"];
      Top --"/about"--> About["サイト解説"];
      Top --"/login"--> Login;
```

「アイテム一覧(/items)」と「アイテム詳細(/item/:id)」、「ユーザー一覧(/users)」と「ユーザー詳細(/user/:id)」という組み合わせが定義されていることを覚えておいてください。

また、このフローをマーメイド対応のビューワで表示すると、このようになります:
2022110504


マーメイドファイルが用意できたら実際に動かしてみましょう。まず(1)のワイヤーフレームの全画面を表示できるようなウェブアプリを作ってみます。この場合はソースコード内の web.js を使って、以下のように実行します:
$ MERMAID=mermaid_sample.md node web

実行時に MERMAID という環境変数を指定しています。この環境変数にマーメイドファイル名を指定します(上の例では mermaid_sample.md ファイルを指定しています)。

マーメイドファイルに問題もなく、正しく実行された場合は一瞬で実行が完了し、web/ というフォルダに実行結果が生成されています。実際に生成された結果を確認するには web/ フォルダで "npm start" を実行します:
$ cd web
$ npm install
$ npm start

成功すると 8080 番ポートでウェブアプリケーションが待ち受けているので実際にアクセスして動作を確認してみましょう。まずログインページにアクセスするには(マーメイドファイル内で)/login へアクセスすることでログインページが表示されるよう定義していたので、ウェブブラウザを起動して "http://localhost:8080/login" にアクセスしてみます:
2022110505


本来であれば「ログイン」ページには ID とパスワードを入力するフィールドがあって、、というものだと思いますが、残念ながらそこまでは再現できていません。ただこのページには「トップ」と書かれたボタンがあり、このボタンをクリックすることで(マーメイドファイルの中で定義されていたように)トップページへ遷移するようにできています:
2022110506


トップページは↑のようになります。マーメイドファイルではトップページから「アイテム一覧(/items)」、「ユーザー一覧(/users)」、「サイト解説(/about)」、「ログイン(/login)」という4つのページへのリンクが定義されていたので、それらがそのまま4つのボタンになっています。正しく再現できています。

試しに「アイテム一覧」ボタンをクリックすると以下のようになります:
2022110507


先ほどまでのページとは少し違う画面になりました。これはアイテム一覧のページ(/items) がアイテム詳細(/item/:id) のページと繋がっていて、これらの URL パスのルール性から「個別のページ」ではなく「item の一覧」のページであると判定された結果です。その結果、表形式で item の一覧(id と名前だけですけど)が表示されています。そしてこれらのいずれかをクリックすると(例えば id = 0 の一番上のデータをクリックすると)/item/0 へ遷移し、詳細画面が表示されます(トップ画面からユーザー一覧ボタンをクリックした場合も同様の画面遷移となります):
2022110508

※(注意)
この「一覧」と「詳細」の両ページの存在を確認するには、マーメイドファイル内の URL パスで
- /(複数形)
- /(単数形)/:id
という2つの URL パスが定義されていることが条件なのですが、この「複数形」の表現には注意が必要です。

"user" の複数形は "users" で、"item" の複数形は "items" で何も問題はないのですが、例えば "category" の複数形は注意が必要です。英単語として正しくは "categories" となりますが、本ツールでは単に "s" を付けるだけの "categorys" と定義する必要があります。"information" も(正しくは複数形が存在しない英単語ですが、もし information の一覧ページを作りたいのであれば)"/informations" という URL パスで一覧ページを定義する必要がある、という点に注意してください。
※(注意ここまで)


一方、トップ画面から「サイト解説」や「ログイン」ボタンをクリックした場合は、遷移先が一般的なページ画面であると認識され、表形式ではなく(リンクがある場合は)ボタンが表示される画面となります(下図は「サイト解説」ボタンを押した場合):
2022110509


・・・と、このように web.js はマーメイドファイルを指定するだけで、その内容からここまでのページ遷移を実現するウェブアプリケーションが自動生成できました。実際に詳細なワイヤーフレーム画面を作りたい場合は生成されたフォルダの views サブフォルダや public サブフォルダに HTML テンプレートや CSS, JavaScript ができているので、これらをカスタマイズすることでデフォルトの生成画面を(HTML や CSS の知識だけで)カスタマイズできる、というものです。


次に同じマーメイドファイルから(2)の API を作ってみます。(1)でも紹介しましたが、今回使用するマーメイドファイルには「ユーザー(user)」と「アイテム(item)」という2つのデータを対象に一覧表示したり、詳細表示するページが含まれていました。ということは user と item の2つはデータベースに格納して読み書き更新削除する可能性があるデータ、という推測ができることになります。

この推測通りに動くかどうかを実際に使って確認してみます。元の web-template フォルダで今度は api.js を以下のように実行します:
$ MERMAID=mermaid_sample.md node api

この実行結果は api/ フォルダに出力されます。今度も(1)と同様に api/ フォルダに生成されたコードを実行してみます:
$ cd api
$ npm install
$ npm start

成功すると 8081 番ポートで API アプリケーションが待ち受けており、また /_doc/ という URL パスで Swagger ドキュメントが稼働しています。実際に Swagger にアクセスして動作を確認してみましょう。"http://localhost:8081/_doc" にアクセスしてみます:
2022110510

2022110511


↑のように、item および user の情報を CRUD できる API と、その Swagger ドキュメントが用意されました。データはメモリに格納するので再起動するまでの間であれば実際にデータを保存したり、変更したり、削除したり、読み出したりすることができます。マーメイドファイルからデータ操作が必要な要素を見つけ出して API 化する(ためのソースコードを出力する)、という機能が実現できていることがわかります。 プロトタイピング目的であればこれだけでもかなり使えるところまで設計を可視化することができると思っていますが、(1)で紹介した web.js でマーメイドファイルからページ遷移機能と、(2)で紹介した api.js で API サーバーを両方作った上で、一覧ページと詳細ページの部分だけ API 連携するようカスタマイズすれば、細かな見た目以外のかなりの部分を実現できるプロトタイピングがほぼノーコード/ローコードで実現できると感じています。


最後におまけの(3)について紹介します。(3)はカスタマイズしたマーメイドファイルが、想定していた通りの正しいフローになっているかどうかを確認するためのマーメイド・プレビューワーとしてのアプリケーションです。(1)・(2)同様に環境変数 MERMAID にマーメイドファイルを指定して、以下のように実行します:
$ MERMAID=mermaid_sample.md node preview

成功すると 8000 番ポートでプレビューワーのアプリケーションが待ち受けています。実際にアクセスして動作を確認してみましょう。"http://localhost:8000" にアクセスしてみます:
2022110512


マーメイドファイルのプレビューができました。これで正しい(自分の想定通りの)ワイヤーフレームがマーメイドファイルとして実現できているかどうかを(GitHub などにコミットしなくても)事前に確認することができます。おまけツールではありますが、(1)や(2)を実行する前に何度か使うことになる利用頻度の高いツールであるとも思っています。


【まとめ】
もともとはウェブアプリのデザインとワイヤーフレームを効率よく作るためのプロトタイピングツールを作ろうとしていたのですが、マーメイドファイルとの相性がよく、データ API まで作れるようになったことで、ノーコード/ローコードツールともいえるかな、という形になりました。実際にはここで紹介していないパラメータを使うことで CRUD API を本物のデータベースと接続して作ったり、UI 部分も(デフォルトでは Bootstrap ベースで作るのですが)Carbon や Material デザインにしたり、ということもできるようには作っています。 興味あるかたは公開されているソースコードや README を見て試してみてください。

Auth0 のプログラミング中にちとハマった内容を備忘録メモとして残しておきます。

【背景】
ウェブのサービスやアプリを作る際に、いわゆる IDaaS(ID as a Service : ユーザー管理機能の SaaS) を使うことが多くあります。自分がよく使っているのは Auth0AppID です。ユーザーのログイン/ログアウトだけでなく、作ろうとすると面倒なオンラインサインアップの機能などもまとめて API で提供されていて、ユーザー管理周りを任せてしまうことが多いです。特に Auth0 を Node.js で使う場合は専用のライブラリも提供されていて、これを使うことで簡単に連携サービスを作っていました。

先日、Auth0 を使ってサービスの開発をしている時に「ログアウト」がうまくできないことに気付きました。以前は以下のようなコードでログアウトできていたのですが、これだとエラーは発生しないものの、セッションなのかクッキーなのか、よくわかりませんが情報が残ってしまってログアウトできていませんでした(以下のコードだとログイン情報が残ったままトップページに遷移してしまう):
app.get( '/auth0/logout', function( req, res ){
  req.logout( function(){   //. ログアウトして、
    res.redirect( '/' );    //. ログアウト後にトップページ('/')に移動する
  });
});

あれ~、以前はこのコードで動いてたんだけどな・・・ と思ってよく調べてみると、2021 年12月にログアウト処理の仕様が変わっていたようでした:
Logout Redirects Migration Guide


このあたりを踏まえて、現状の Auth0 ログアウト実装方法を調べたので、以下に記載します。

【ログアウト実装準備】
まず準備段階として、Auth0 でログアウトを行う場合に「ログアウト後に移動する先の URL」をあらかじめ登録しておく必要があります。Auth0 ログイン後の Applications メニューから該当アプリを選択した先の "Allowed Logout URLs" という項目です:
2022090401


コールバック URL 同様に、ここにログアウト後に遷移する URL を事前に登録しておかないといけないようでした。自分の場合は開発時は localhost を使うので、これと本番時の URL (https://xxxxx.xxxxxxx.com)を合わせて(カンマ区切りで)以下のように指定しました:
http://localhost:8080,https://xxxxx.xxxxxxx.com

これでログアウト処理を実装するために必要な準備は完了です。


【ログアウト実装】
現在のログアウト仕様は以下で確認できました。結論としてはクライアント(ブラウザ)が https://(Auth0 ドメイン)/v2/logout に必要なパラメータを付与して GET リクエストを出すことで正しくログアウトできるようです:
https://auth0.com/docs/api/authentication#logout

サーバーサイドのプログラム上だと以下のようになります:
app.get( '/auth0/logout', function( req, res ){
  req.logout( function(){   //. ログアウトして、
    //res.redirect( '/' );    //. ログアウト後にトップページ('/')に移動する
    res.redirect( 'https://(Auth0 のドメイン)/v2/logout?client_id=(Auth0 クライアントID)&returnTo=http://localhost:8080' ); 
  });
});

このうち、Auth0 ドメインと Auth0 クライアント ID はアプリケーションを登録する際に同時に確認/取得できるものです。また returnTo パラメータを付けないと、ログアウト処理は行われるのですが、そのまま処理が終わってしまうので、事実上 returnTo パラメータも必須です。このパラメータにログアウト後に遷移する URL を指定します。この URL は上述の準備時に Allowed Logout URLs で指定した中に含まれている必要があります(事前に指定した Allowed Logout URLs に含まれていないとエラー)。


この変更で正しくログアウトできるようになることが確認できました。ほっ・・・


とある要件を実現するツールを作りました。同じことに悩む人がいた場合を想定して、ツールをソースごと公開することにしました。

某ウェブプラットフォームのサービス終了が決まり(わかる人はこれだけで何の話か推測されそうだけど・・)、現在稼働中のウェブアプリケーションを引っ越しすることになりました。引っ越しそのものはさほど難しくないのですが、問題は「サービスの URL が変わってしまう」ことでした。

図に示すとこのような感じです。これまで運用していたサービスの運用環境を A から B に引っ越しした結果、これまでの URL とは異なる URL で引き続き運用することになりました:
2022070601


これまで使っていたユーザーに対しても「サービスの URL が変わった」ことを知らせてあげたいのですが、具体的にどうするべきでしょうか?A で運用中の画面に「URL が変更になった」と注意書きを含めて、改めて B にアクセスしてもらうこともできます。が、もう少し気の利くやり方として「A にアクセスしたら自動的に B に転送させて、B で運用中の画面で URL が変更になった旨を記載しておく(そのままブックマークできるようにする)」という方法もあります。


この後者の方法を実現するためには A にアクセスした利用者に対して "301" という HTTP ステータスコードと、続けて変更先の URL を Location ヘッダに含めて返すことで実現できます。この HTTP ステータスコード 301 は "Moved Permanentaly" を意味していて「URL が(一時的ではなく)恒久的に変更になった」ことを示しています。続けて Location ヘッダに新しい URL を含めておくことでウェブブラウザ側で新しい URL に自動的に遷移してくれます。つまり「A にアクセスしたら自動的に B に移動させる」ことが実現できます(そして B 側で「URL が変わったので現在のページをブックマークして」といったメッセージを記しておく、といった対応になります)。ウェブページの引っ越しを行う場合の一般的な手段でもあります:
2022070602



問題となるのは、この「301 という HTTP ステータスコードを返す」機能です。A 側のサービスでそのような転送機能や転送の設定が提供されていればそれを使えばいいのですが、必ずしも提供されていないことも考えられます。そのようなケースに対応するため、今回「アプリケーションの機能として 301 HTTP ステータスコードと、引っ越し先 URL を返すアプリケーション」を作ったので、公開することにしました。これまで A で動いてたアプリケーションの代わりにこのアプリケーション(下図の app)をデプロイすることで、A へのアクセスがあった場合に、新しい B への URL に無条件で転送させることができるようになります:
2022070603


このような挙動を実現するための Node.js アプリケーションのソースコードを以下で公開しました:
https://github.com/dotnsf/301movedpermanently

動作確認する場合は、Node.js が導入済みの環境にソースコードをダウンロードするか git clone して、環境変数 URL に転送先の URL を指定して実行します。なおアプリケーションはデフォルトで 8080 番ポートで待ち受けますが、このポート番号を変えたい場合は環境変数 PORT に指定して実行してください:
$ git clone https://github.com/dotnsf/301movedpermanently

$ cd 301movedpermanently

$ npm install

$ URL=https://www.yahoo.co.jp/ node app (全てのリクエストを Yahoo! トップページに転送する場合)

起動後にアプリケーションにアクセスすると、全てのリクエストが環境変数 URL で指定した値(上の例だと https://www.yahoo.co.jp/)に転送されます。なおコード内ではメソッド/パス/パラメータに関係なく、全てのリクエストを GET リクエストに変換して転送しています。GET/HEAD メソッド以外のリクエストについては HTTP ステータス 308 を返して対応するケースもありますが、今回のような「サービスごと引っ越し」のケースでは新 URL のトップページに転送することが多いと思うので、今回は説明を控えます(下の青字部分が実行されます):
app.all( '*', function( req, res ){
  if( post_redirect ){
    var method = req.method;
    if( method == 'GET' || method == 'HEAD' ){
      //. https://developer.mozilla.org/ja/docs/Web/HTTP/Status/301
      res.status( 301 );
    }else{
      //. https://developer.mozilla.org/ja/docs/Web/HTTP/Status/308
      res.status( 308 );
    }
  }else{
    res.status( 301 );
  }

  res.set( 'Location', url );
  res.end();
});

古いサイト A が動いている間だけ有効な強制転送方法ですが、他に方法がない場合はこんな感じで古いサイトを訪ねた人を強制的に B へ転送する方法が有効だと考えています。

なお、docker イメージとしても公開しているので、移行元で docker が使える環境であればこちらを使っていただくのが手っ取り早いと思っています:
dotnsf/301movedpermanently


REST API やフロントエンドフレームワークを使ったウェブアプリケーションを作っていると、CORS (Cross-Origin Resource Sharing) に悩まされることが珍しくありません:
cors.001


CORS とはウェブブラウザ(フロントエンド)の JavaScript におけるセキュリティ制約の1つで、AJAX などを使って HTTP リクエストを行う場合、原則的には同一オリジンからのリクエストだけが許可され、異なるオリジンからのリクエストは CORS 制約によって失敗するようになっています。バックエンドからの HTTP リクエストに関しては一切制約はありませんが、フロントエンドからのリクエストに対してのみ適用されるものです。詳しくはこちらもどうぞ:
https://developer.mozilla.org/ja/docs/Web/HTTP/CORS


ただ REST API とフロントエンド・ウェブアプリケーションを作っている人が同一であったり、そういったリクエストが来るとわかっている前提で REST API を用意する場合など、API 提供側からもこの CORS 制約を無視したくなることがあります。そういった場合のために「REST API 側で特定のオリジンからの HTTP リクエストについては許可する」設定も可能です。具体的には許可するオリジン(https://allowed-origin.com)を指定して、
 Access-Control-Allow-Origin: https://allowed-origin.com
という HTTP レスポンスヘッダを返します。リクエスト元ではこのヘッダの内容を自分自身のオリジンと比較して、一致している場合のみ正常処理とみなす(一致していない場合はエラーとする)という処理が行われます。なお「全てのオリジンからのリクエストを許可する」という指定も可能で、その場合は例外的に
 Access-Control-Allow-Origin: *
という HTTP レスポンスヘッダを返すことで実現できるようになっています。

さて、実際にフロントエンド側を開発していると、以下のような問題に直面することがあります:
(1)開発中やテスト中は http://localhost:3000 で動かす
(2)開発終了後は https://xxxxx.com で実運用する
(3)REST API は(1)とも(2)とも異なる環境で稼働している(つまり CORS 設定しないと動かない)

開発中は自分の PC でコーディングするので http://localhost:3000 のようなローカルホストを使った動作確認となり、実運用中は運用のために用意した https://xxxxx.com というインターネット上のホストを使ってアクセスする、というケースです(特別に珍しいケースではないと思います)。開発(テスト)環境でも実運用環境でも REST API を動かす必要があるため、REST API 側ではいずれのケースでも動作するような CORS 設定ができると楽です(http://localhost:3000 からのリクエストを許可する是非はともかく)。しかし CORS の Access-Control-Allow-Origin ヘッダには1つのオリジン(それも * 以外の正規表現とかは使えずに)しか記述できない、という制約があります。つまり「http://localhost:3000 または https://xxxxx.com からのリクエストを許可する」という Access-Control-Allow-Origin の指定はできないのですが、これをうまく回避・実現する方法はないでしょうか?

1つの考えられる方法として「開発中だけは "Access-Control-Allow-Origin: *" を指定」して全てのオリジンからのリクエストを許可する、という方法も考えられます。が、これはあまりに無防備な設定でもあります。

そこで考えたのが以下の方法です。あらかじめ許可するオリジンを配列として用意した上で、リクエスト元が許可オリジン配列の中に含まれていた場合はそのオリジンを Access-Control-Allow-Origin ヘッダに動的に指定して返す、という方法です。Node.js + Express 環境向けに具体的に作るとこんな感じ:
// app.js
var express = require( 'express' ),
    app = express();

app.use( express.Router() );

var settings_cors = 'CORS' in process.env ? process.env.CORS : '';
app.all( '/*', function( req, res, next ){
  if( settings_cors ){
    var origin = req.headers.origin;
    if( origin ){
      var cors = settings_cors.split( " " ).join( "" ).split( "," );

      //. cors = [ "*" ] への対応が必要
      if( cors.indexOf( '*' ) > -1 ){
        res.setHeader( 'Access-Control-Allow-Origin', '*' );
        res.setHeader( 'Vary', 'Origin' );
      }else{
        if( cors.indexOf( origin ) > -1 ){
          res.setHeader( 'Access-Control-Allow-Origin', origin );
          res.setHeader( 'Vary', 'Origin' );
        }
      }
    }
  }
  next();
});

app.get( '/ping', function( req, res ){
  res.contentType( 'application/json; charset=utf-8' );

  res.write( JSON.stringify( { status: true, message: 'PONG' }, null, 2 ) );
  res.end();
});

var port = process.env.PORT || 8080;
app.listen( port );
console.log( "server starting on " + port + " ..." );


起動時の環境変数 CORS として許可オリジンの配列(以下の例では "http://localhost:3000" と "https://xxxxx.herokuapp.com" の2つ)をカンマ区切りで指定します:
(例)$ CORS=http://localhost:3000,https://xxxxx.herokuapp.com node app

このサンプル(8080 番ポートで起動)では GET /ping というリクエストに { status: true, message: 'PONG' } というレスポンスを返す REST API が定義されていますが、リクエスト元のオリジンが http://localhost:3000 か https://xxxxx.herokuapp.com のいずれかの場合は Access-Control-Allow-Origin ヘッダによって CORS の制約を回避して実行できます。

これでテスト環境、ステージング環境、本番環境などで CORS の設定を変えずに運用できる API サーバーが用意できそうです。


上記サンプルソースコードはこちらに用意しました:
https://github.com/dotnsf/multicors


運用中のウェブアプリケーションに対して、セキュリティ面を考慮して以下のようなリクエスト制限をかける必要が生じたとします:

・(例えば)10分間で 100 回のリクエストを許可する
・許可数を超えた場合はリクエストを処理しない


クラウドやホスティングサーバーで運用する場合は、クラウド/ホスティング側にそのような機能が提供されていることもあると思います。が、もしそのような機能が提供されていない条件下でこのような要件が生じた場合、アプリケーションの実装としてリクエスト制限を用意する必要が出てくるかもしれません。 今回のブログエントリで紹介するのは、Node.js アプリケーションにリクエスト制限をかける実装方法です。


といっても Node.js (バージョン 14 以上)で Express ライブラリを使っている場合であれば express-rate-limit という Express 向けミドルウェアを使うことで簡単に実装できます:
20220228


以下でサンプルを紹介しますが、サンプルコードはこちらに公開しています:
https://github.com/dotnsf/express-rate-limit-sample


例えば現行のコード(app_old.js)が以下のようになっていたとします:
//. app_old.js
var express = require( 'express' ),
    app = express();

app.get( '/', function( req, res ){
  res.contentType( 'application/json; charset=utf-8' );
  res.write( JSON.stringify( { status: true }, null, 2 ) );
  res.end();
});

var port = process.env.PORT || 8080;
app.listen( port );
console.log( "server starting on " + port + " ..." );

"GET /" リクエストに対して "{ status: true }" を返すだけの内容ですが、処理内容自体はもっと複雑でも構いません。

このアプリケーションに「10分間で 100 回」というリクエスト制限をかける場合は以下のようなコード(app_new.js)に変更します:
//. app_new.js
var express = require( 'express' ),
    app = express();

//. rate limit : 100 times per 10 minutes
var rate = require( 'express-rate-limit' );
var limit = rate({
  windowMs: 10*60*1000,
  max: 100,
  standardHeaders: true,
  legacyHeaders: false
});
app.use( limit );

app.get( '/', function( req, res ){
  res.contentType( 'application/json; charset=utf-8' );
  res.write( JSON.stringify( { status: true }, null, 2 ) );
  res.end();
});

var port = process.env.PORT || 8080;
app.listen( port );
console.log( "server starting on " + port + " ..." );

express-rate-limit ライブラリをインスタンス化して、属性を指定して app.use() でリクエスト処理前に実行されるようにしています。なお各属性値は以下のような意味です:
・windowMs: 制限をかける時間(ミリ秒)
・max: windowMs で定義した時間でのリクエスト処理上限数
・standardHeaders: "RateLimit-*" でリクエスト制限情報を HTTP ヘッダに含める※
・legacyHeaders: "X-RateLimit-*" でリクエスト制限情報を HTTP ヘッダに含める※

※上述のサンプルでは standardHeaders: true, legacyHeaders: false に指定している

これだけでアプリケーションレベルでリクエスト制限を実装できます。 ただし、この制限は「1インスタンスごとの制限」である点に注意が必要です。クラウド的な言い方だと「1コンテナ」あたりでこの制限が有効になりますが、複数インスタンスで運用した場合、例えば可用性を高める目的で3つのコンテナを起動して運用した場合は、事実上設定値の3倍のリクエスト処理を受け付けることになる、という点に注意が必要です。



このページのトップヘ