035.2 レッスン 1
Certificate: |
Web開発の要点 |
---|---|
Version: |
1.0 |
Topic: |
035 NodeJSサーバプログラミング |
Objective: |
035.2 NodeJS Expressの基本 |
Lesson: |
1 of 2 |
はじめに
Express.js(または単にExpress)は、Node.js上で動作する人気のフレームワークで、Webアプリケーションのクライアントからのリクエストを処理するHTTPサーバを記述するために使用されます。Expressは、HTTPで送信されたパラメータを読み取るさまざまな方法をサポートしています。
初期サーバスクリプト
Expressの基本的な機能である、リクエストの受信と処理を実演するために、サーバにいくつかの情報をリクエストするアプリケーションをシミュレートしてみましょう。具体的には、例のサーバが下記の処理を行います。
-
クライアントから送信されたメッセージを単純に返す
echo
関数を提供します。 -
リクエストに応じて、クライアントにIPアドレスを通知します。
-
既知のクライアントを識別するためにクッキーを使用します。
まず最初に、サーバとして動作するJavaScriptファイルを作成します。 npm
を使って、 myserver
というディレクトリを作成し、その中にJavaScriptのファイルを置きます。
$ mkdir myserver $ cd myserver/ $ npm init
エントリポイントには、任意のファイル名を使用できます。ここでは、デフォルトのファイル名である index.js
を使用します。以下のリストは、サーバのエントリーポイントとして使用する基本的な index.js
ファイルを示しています。
const express = require('express')
const app = express()
const host = "myserver"
const port = 8080
app.get('/', (req, res) => {
res.send('Request received')
})
app.listen(port, host, () => {
console.log(`Server ready at http://${host}:${port}`)
})
このスクリプトの最初の行では、サーバの設定に必要ないくつかの重要な定数が定義されています。最初の2つ、 express
と app
は、組み込まれている express
モジュールと、アプリケーションを実行するこのモジュールのインスタンスに対応しています。ここでは、サーバが実行するアクションを app
オブジェクトに追加します。
他の2つの定数、 host
と port
は、サーバに関連するホストと通信ポートを定義します。
公開されているホストがある場合は、 host
の値として myserver
の代わりにその名前を使います。ホスト名を指定しない場合、Expressはアプリケーションが動作するコンピュータである localhost
をデフォルトとします。この場合、外部のクライアントはプログラムにアクセスすることができません。これは、テストのためには良いかもしれませんが、本番環境ではほとんど意味がありません。
ポートが指定されていないと、サーバが起動しません。
このスクリプトでは、 app
オブジェクトに2つのプロシージャのみをアタッチしています。HTTPの GET
でクライアントからのリクエストに答える app.get()
アクションと、 app.listen()
コールで、サーバを起動してホストとポートを割り当てる必要があります。
サーバを起動するには、 node
コマンドを実行し、引数としてスクリプト名を指定します。
$ node i express $ node index.js
Server ready at http://myserver:8080
というメッセージが表示されると、サーバはHTTPクライアントからのリクエストを受信する準備ができたことになります。リクエストは、サーバが動作しているコンピュータ上のブラウザからでも、サーバにアクセス可能な他のマシンからでも可能です。
ここで紹介するすべてのトランザクションの詳細は、開発者コンソール用のウィンドウを開くとブラウザに表示されます。また、HTTP通信には curl
コマンドを使用することができ、より簡単に接続の詳細を検査することができます。シェルのコマンドラインに慣れていない場合は、HTMLフォームを作成してサーバにリクエストを送信することができます。
次の例では、コマンドラインで curl
コマンドを使用して、新しくデプロイされたサーバに HTTPリクエストを行う方法を示しています。
$ curl http://myserver:8080 -v * Trying 192.168.1.225:8080... * TCP_NODELAY set * Connected to myserver (192.168.1.225) port 8080 (#0) > GET / HTTP/1.1 > Host: myserver:8080 > User-Agent: curl/7.68.0 >Accept: / > * Mark bundle as not supporting multiuse < HTTP/1.1 200 OK < X-Powered-By: Express < Content-Type: text/html; charset=utf-8 < Content-Length: 16 < ETag: W/"10-1WVvDtVyAF0vX9evlsFlfiJTT5c" < Date: Fri, 02 Jul 2021 14:35:11 GMT < Connection: keep-alive < * Connection #0 to host myserver left intact Request received
curl
コマンドの -v
オプションは、すべてのリクエストヘッダーとレスポンスヘッダー、およびその他のデバッグ情報を表示します。 >
で始まる行は、クライアントが送信したリクエストヘッダーを示し、 <
で始まる行は、サーバが送信したレスポンスヘッダーを示します。また、 *
で始まる行は、 curl
自身が生成する情報です。レスポンスの内容は最後にのみ表示され、この場合は Request received
の行になります。
サービスのURLは、この場合、サーバのホスト名とポート ( http://myserver:8080
) を含んでおり、 curl
コマンドの引数として与えられました。ディレクトリやファイル名が指定されていないため、これらのデフォルトはルートディレクトリ /
になります。スラッシュは > GET / HTTP/1.1
行のリクエストファイルとして現れ、出力ではホスト名とポートが続きます。
curl
コマンドは、HTTP接続ヘッダーを表示するだけでなく、異なるHTTPメソッドや異なるフォーマットでデータをサーバに送信することができるので、アプリケーションの開発を支援します。この柔軟性により、問題のデバッグやサーバへの新機能の実装が容易になります。
ルート
クライアントがサーバにリクエストできる内容は、 index.js
ファイルで定義された ルート に依存します。ルートは、HTTPメソッドを指定し、クライアントがリクエストできる パス (正確にはURI)を定義します。
今のところ、サーバには1つのルートしか設定されていません。
app.get('/', (req, res) => {
res.send('Request received')
})
単にプレーンテキストのメッセージをクライアントに返すだけの非常にシンプルなルートですが、ほとんどのルートを構成するのに使われる最も重要なコンポーネントを確認するには十分です。
-
ルートが提供するHTTPメソッドです。この例では、HTTPの
GET
メソッドは、app
オブジェクトのget
プロパティで示されます。 -
ルートが提供するパスです。クライアントがリクエストにパスを指定しない場合、サーバはルートディレクトリを使用します。ルートディレクトリは、Webサーバが使用するために確保されたベースディレクトリです。この章の後の例では、パス
/echo
を使用していますが、これはmyserver:8080/echo
へのリクエストに相当します。 -
The function executed when the server receives a request on this route, usually written in abbreviated form as an arrow function because the syntax
=>
points to the definition of the nameless function. Thereq
parameter (short for “request”) andres
parameter (short for “response”) give details about the connection, passed to the function by the app instance itself. ==== POSTメソッド
テストサーバの機能を拡張するために、HTTPの POST
メソッド用のルートを定義する方法を見てみましょう。このメソッドは、クライアントが、リクエストヘッダーに含まれている以上のデータをサーバに送信する必要がある場合に使用します。 curl
コマンドの --data
オプションは、自動的に POST
メソッドを起動し、 POST
を介してサーバに送信されるコンテンツを含んでいます。以下の出力の POST / HTTP/1.1
の行は、 POST
メソッドが使用されたことを示しています。しかし、サーバでは GET
メソッドしか定義されていないため、 curl
を使用して POST
でリクエストを送信するとエラーが発生します。
$ curl http://myserver:8080/echo --data message="This is the POST request body" -v * Trying 192.168.1.225:8080... * TCP_NODELAY set * Connected to myserver (192.168.1.225) port 8080 (#0) > POST / HTTP/1.1 > Host: myserver:8080 > User-Agent: curl/7.68.0 >Accept: / > Content-Length: 37 > Content-Type: application/x-www-form-urlencoded > * upload completely sent off: 37 out of 37 bytes * Mark bundle as not supporting multiuse < HTTP/1.1 404 Not Found < X-Powered-By: Express < Content-Security-Policy: default-src 'none' < X-Content-Type-Options: nosniff < Content-Type: text/html; charset=utf-8 < Content-Length: 140 < Date: Sat, 03 Jul 2021 02:22:45 GMT < Connection: keep-alive < <!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8"> <title>Error</title> </head> <body> <pre>Cannot POST /</pre> </body> </html> * Connection #0 to host myserver left intact
前述の例では、 --data message="This is the POST request body"
というパラメータを指定して curl
を実行すると、message
という名前のテキストフィールドに This is the POST request body
を入力してフォームを送信することに相当します。
サーバは /
パスに対して1つのルートしか設定されておらず、そのルートは HTTP GET
メソッドにしか応答しないため、応答ヘッダーには HTTP/1.1 404 Not Found
という行が含まれています。また、Expressでは自動的に短いHTMLレスポンスが生成され、警告として Cannot POST
が表示されました。
curl
を使って POST
リクエストを生成する方法を見てきましたが、今度はそのリクエストをうまく処理できるExpressプログラムを書いてみましょう。
まず、リクエストヘッダーの Content-Type
フィールドには、クライアントから送信されたデータが application/x-www-form-urlencoded
形式であることが示されています。Expressはこの形式をデフォルトでは認識しないので、 express.urlencoded
モジュールを使用する必要があります。このモジュールをインクルードすると、ハンドラ関数のパラメータとして渡される req
オブジェクトに req.body.message
プロパティが設定され、これがクライアントから送信された message
フィールドに対応します。このモジュールは app.use
でロードされます。これはルートの宣言の前に置く必要があります。
const express = require('express')
const app = express()
const host = "myserver"
const port = 8080
app.use(express.urlencoded({extended:true}))
これができれば、既存のルートで app.get
を app.post
に変更するだけで、 POST
経由のリクエストに対応し、リクエストボディを復元することができます。
app.post('/', (req, res) => {
res.send(req.body.message)
})
ExpressはリクエストヘッダーでHTTPメソッドを識別し、適切なルートを使用するため、ルートを置き換えるのではなく、単純にこの新しいルートを追加することも可能です。このサーバには複数の機能を追加したいので、 /echo
や /ip
のように、それぞれを独自のパスで区切っておくと便利です。
パスと関数ハンドラー
どのHTTPメソッドがリクエストに応答するかを定義した後は、リソースの具体的なパスと、それを処理してクライアントへのレスポンスを生成する関数を定義する必要があります。
サーバの echo
機能を拡張するために、 POST
メソッドを使って、 /echo
というパスを持つルートを定義することができます。
app.post('/echo', (req, res) => {
res.send(req.body.message)
})
ハンドラ関数の req
パラメータには、プロパティとして格納されているすべてのリクエストの詳細が含まれています。リクエストボディの message
フィールドの内容は req.body.message
プロパティに格納されています。この例では、単にこのフィールドが res.send(req.body.message)
コールによってクライアントに送り返されます.
変更した内容が反映されるのは、サーバが再起動してからであることを忘れないでください。この章の例では、ターミナルウィンドウからサーバを実行しているので、ターミナル上で キーボードで[Ctrl]+c を押すことでサーバをシャットダウンすることができます。その後、 node index.js
コマンドでサーバを再起動します。先ほど示した curl
リクエストに対するクライアントからのレスポンスが成功しました。
$ curl http://myserver:8080/echo --data message="This is the POST request body" This is the POST request body
GETリクエストで情報を渡したり返したりする他の方法
例題のような短いテキストメッセージしか送信しない場合は、HTTPの POST
メソッドを使うのは過剰かもしれません。このような場合には、クエスチョンマークで始まる クエリ文字列 でデータを送信することができます。例えば、HTTPの GET
メソッドのリクエストパスに、?message=This+is+the+message
という文字列を含めることができます。クエリー文字列で使用されるフィールドは、サーバが req.query
プロパティで利用できます。したがって、 req.query.message
プロパティには、 message
という名前のフィールドが用意されています。
HTTPの GET
メソッドでデータを送信するもう一つの方法は、Expressの ルートパラメータ を使うことです。
app.get('/echo/:message', (req, res) => {
res.send(req.params.message)
})
この例のルートは、パス /echo/:message
を使用した GET
メソッドのリクエストにマッチします。 :message
は、クライアントがそのラベルで送信した用語のプレースホルダーです。これらのパラメータは req.params
プロパティでアクセスできます。この新しいルートにより、サーバの echo
関数をクライアントからより簡潔に要求することができます。
$ curl http://myserver:8080/echo/hello hello
他の状況では、サーバがリクエストを処理するために必要な情報は、クライアントから明示的に提供される必要はありません。例えば、サーバはクライアントのパブリックIPアドレスを取得する別の方法を持っています。この情報は、デフォルトでは req
オブジェクトの req.ip
プロパティに含まれています。
app.get('/ip', (req, res) => {
res.send(req.ip)
})
これで、クライアントは /ip
パスを GET
メソッドでリクエストして、自分のパブリックIPアドレスを見つけることができます。
$ curl http://myserver:8080/ip 187.34.178.12
クライアントは req
オブジェクトの他のプロパティ、特に req.headers
で得られるリクエストヘッダーを変更することができます。例えば、 req.headers.user-agent
プロパティは、どのプログラムがリクエストを行っているかを識別します。あまり一般的ではありませんが、クライアントはこのフィールドの内容を変更することができますので、サーバはこのフィールドを使って特定のクライアントを確実に識別するべきではありません。アプリケーションに悪影響を及ぼす可能性のある境界やフォーマットの不一致を避けるために、クライアントから明示的に提供されたデータを検証することは、さらに重要です。
レスポンスの調整
これまでの例で見てきたように、 res
パラメータはクライアントにレスポンスを返す役割を担っています。さらに、 res
オブジェクトは、レスポンスの他の側面を変更することができます。お気づきかもしれませんが、これまでに実装したレスポンスは単なる簡潔なプレーンテキストのメッセージですが、レスポンスの Content-Type
ヘッダーには text/html; charset=utf-8
が使用されています。これはプレーンテキストのレスポンスを受け付けることを妨げるものではありませんが、レスポンスヘッダーのこのフィールドを res.type('text/plain')
という設定で text/plain
に再定義することで、より正しくなります。
他のタイプのレスポンス調整には、サーバが以前にリクエストを行ったクライアントを識別できるようにする クッキー の使用が含まれます。クッキーは、リクエストを特定のユーザーに関連付けるプライベートセッションの作成など、高度な機能を実現するために重要ですが、ここでは、以前にサーバにアクセスしたことのあるクライアントを特定するためにクッキーを使用する簡単な例を見てみましょう。
Expressのモジュール化されたデザインを考慮すると、クッキー管理機能は、スクリプトで使用する前に、 npm
コマンドでインストールする必要があります。
$ npm install cookie-parser
インストール後、クッキーの管理をサーバスクリプトに含める必要があります。以下の定義をファイルの先頭付近に記述してください。`
const cookieParser = require('cookie-parser')
app.use(cookieParser())
クッキーの使い方を説明するために、スクリプト内にすでに存在する /
のルートパスを使って、ルートのハンドラ関数を修正してみましょう。 req
オブジェクトは req.cookies
プロパティを持ち、ここにリクエストヘッダーで送信されたクッキーが保存されます。一方、 res
オブジェクトは res.cookie()
メソッドを持ち、クライアントに送信する新しいクッキーを作成します。以下の例のハンドラ関数は、 known
という名前のクッキーがリクエストに存在するかどうかをチェックします。そのようなクッキーが存在しない場合、サーバは初めての訪問者であると仮定して、 res.cookie('known', '1')
の呼び出しによりその名前のクッキーを送ります。このクッキーには何らかのコンテンツがあるはずなので、任意に値 1
を割り当てますが、サーバはその値を参照しません。このアプリケーションでは、クッキーが存在するだけで、クライアントが以前にこのルートをリクエストしたことがあると仮定しています。
app.get('/', (req, res) => {
res.type('text/plain')
if ( req.cookies.known === undefined ){
res.cookie('known', '1')
res.send('Welcome, new visitor!')
}
else
res.send('Welcome back, visitor');
})
デフォルトでは、 curl
はトランザクションでクッキーを使用しません。しかし、保存したり( -c cookies.txt
)、保存したクッキーを送信したり( -b cookies.txt
)するオプションがあります。
$ curl http://myserver:8080/ -c cookies.txt -b cookies.txt -v * Trying 192.168.1.225:8080... * TCP_NODELAY set * Connected to myserver (192.168.1.225) port 8080 (#0) > GET / HTTP/1.1 > Host: myserver:8080 > User-Agent: curl/7.68.0 >Accept: / > * Mark bundle as not supporting multiuse < HTTP/1.1 200 OK < X-Powered-By: Express < Content-Type: text/plain; charset=utf-8 * Added cookie known="1" for domain myserver, path /, expire 0 < Set-Cookie: known=1; Path=/ < Content-Length: 21 < ETag: W/"15-l7qrxcqicl4xv6EfA5fZFWCFrgY" < Date: Sat, 03 Jul 2021 23:45:03 GMT < Connection: keep-alive < * Connection #0 to host myserver left intact Welcome, new visitor!
このコマンドは、サーバにクッキーが実装されてから初めてのアクセスだったので、クライアントはリクエストに含めるクッキーを持っていませんでした。予想通り、サーバはリクエストに含まれるクッキーを識別できなかったため、出力の Set-Cookie: known=1; Path=/
行で示されるように、レスポンス・ヘッダーにクッキーを含めました。 curl
でクッキーを有効にしているので、新しいリクエストはクッキー known=1
をリクエストヘッダーに含め、サーバがクッキーの存在を識別できるようにします。
$ curl http://myserver:8080/ -c cookies.txt -b cookies.txt -v * Trying 192.168.1.225:8080... * TCP_NODELAY set * Connected to myserver (192.168.1.225) port 8080 (#0) > GET / HTTP/1.1 > Host: myserver:8080 > User-Agent: curl/7.68.0 >Accept: / > Cookie: known=1 > * Mark bundle as not supporting multiuse < HTTP/1.1 200 OK < X-Powered-By: Express < Content-Type: text/plain; charset=utf-8 < Content-Length: 21 < ETag: W/"15-ATq2flQYtLMYIUpJwwpb5SjV9Ww" < Date: Sat, 03 Jul 2021 23:45:47 GMT < Connection: keep-alive < * Connection #0 to host myserver left intact Welcome back, visitor
Cookieのセキュリティ
開発者は、リクエストを行うクライアントを識別するためにクッキーを使用する場合、潜在的な脆弱性に注意する必要があります。攻撃者は、 クロスサイトスクリプティング (XSS)や クロスサイトリクエストフォージェリ (CSRF)などの手法を用いて、クライアントからクッキーを盗み出し、それによってクライアントになりすましてサーバにリクエストを行うことができます。一般的にこの種の攻撃は、検証されていないコメント欄や綿密に作成されたURLを利用して、悪意のあるJavaScriptコードをページに挿入します。本物のクライアントがこのコードを実行すると、有効なCookieをコピーして保存したり、別の宛先に転送したりすることができます。
そのため、特にプロフェッショナルなアプリケーションでは、 ミドルウェア として知られる、より専門的なExpressの機能をインストールして使用することが重要です。 express-session
または cookie-session
モジュールは、セッションとクッキーの管理をより完全かつ安全に制御します。これらのコンポーネントは、クッキーが元の発行者から転用されるのを防ぐための特別な制御を可能にします。
演習
-
HTTPの
GET
メソッドのクエリ文字列で送られてきたcomment
フィールドの内容をハンドラ関数で読み取るにはどうすればいいですか? -
HTTPの
GET
メソッドと/agent
パスを使って、user-agent
ヘッダーの内容をクライアントに送り返すルートを書いてみましょう。 -
Express.jsには ルートパラメータ という機能があり、
/user/:name
のようなパスを使って、クライアントから送られてきたname
パラメータを受け取ることができます。ルートのハンドラ関数内でname
パラメータにアクセスするにはどうすれば良いですか?
発展演習
-
あるサーバのホスト名が
myserver
である場合、以下のフォームの投稿を受け取るのはどのExpressルートでしょうか?<form action="/contact/feedback" method="post"> ... </form>
-
サーバの開発中に、クライアントがHTTPの
POST
メソッドでコンテンツを正しく送信していることを確認しても、プログラマがreq.body
プロパティを読み取ることができません。この問題の原因として考えられることは何ですか? -
サーバにパス
/user/:name
のルートが設定されていて、クライアントが/user/
へのリクエストを行うとどうなりますか?
まとめ
このレッスンでは、HTTPリクエストを受信して処理するExpressスクリプトの書き方を説明しますした。Express は ルート の概念を用いてクライアントが利用できるリソースを定義するため、あらゆる種類のWebアプリケーション用のサーバを非常に柔軟に構築することができます。このレッスンでは、以下の概念と手順について説明しました。
-
HTTP
GET
および HTTPPOST
メソッドを使用するルート -
フォームデータを
request
オブジェクトに格納する方法 -
ルートパラメータの使用方法
-
レスポンスヘッダーのカスタマイズ
-
基本的なクッキーの管理
演習の回答
-
HTTPの
GET
メソッドのクエリ文字列で送られてきたcomment
フィールドの内容をハンドラ関数で読み取るにはどうすればいいですか?comment
フィールドは、req.query.comment
プロパティで利用できます。 -
HTTPの
GET
メソッドと/agent
パスを使って、user-agent
ヘッダーの内容をクライアントに送り返すルートを書いてみましょう。app.get('/agent', (req, res) => { res.send(req.headers.user-agent) })
-
Express.jsには ルートパラメータ という機能があり、
/user/:name
のようなパスを使って、クライアントから送られてきたname
パラメータを受け取ることができます。ルートのハンドラ関数内でname
パラメータにアクセスするにはどうすれば良いですか?name
パラメータは、req.params.name
プロパティでアクセスできます。
発展演習の回答
-
あるサーバのホスト名が
myserver
である場合、以下のフォームの投稿を受け取るのはどのExpressルートでしょうか?<form action="/contact/feedback" method="post"> ... </form>
app.post('/contact/feedback', (req, res) => { ... })
-
サーバの開発中に、クライアントがHTTPの
POST
メソッドでコンテンツを正しく送信していることを確認しても、プログラマがreq.body
プロパティを読み取ることができません。この問題の原因として考えられることは何ですか?プログラマが
express.urlencoded
モジュールを含まなかったため、Expressがリクエストのボディを抽出できませんでした。 -
サーバにパス
/user/:name
のルートが設定されていて、クライアントが/user/
へのリクエストを行うとどうなりますか?このルートでは
:name
パラメータをクライアントから提供してもらう必要があるため、サーバは404 Not Found
レスポンスを発行します。