2022年4月18日 Node.js express csrf対策
csurfのパッケージを使用してcsrf対策をします。
注: パッケージ名はcsurf
となる点に注目してください。csrf
というパッケージも存在します。
csurf
http://expressjs.com/en/resources/middleware/csurf.html
https://github.com/pillarjs/csrf
実装
前提
- テンプレートは
ejs
を使用します。 - Expressのバージョン14.6.0以降を使用します。(
body-parser
は標準搭載されているのでインストールは不要) - 今回はcsurfの実装をメインなのでexpress等、前段階で必要なパッケージのインストールは割愛します。
csurfはクッキーもしくはセッションを利用します。
今回はセッションで行う方法をアウトプットします。
まず必要なパッケージをインストールしましょう。
yarn add csurf
yarn add express-session
ログイン機能への実装例です。ログインはRouterを使ってモジュール化しています。(express-sessionのミドルウェアは本体のapp.jsに実装しているためここでは記述はありませんが割愛します)
login.js
const router = require("express").Router(); const db = require("../models"); const bcrypt = require("bcrypt"); const csrf = require("csurf"); const csrfProtection = csrf(); router.get("/", csrfProtection, (req, res) => { res.render("login", { csrfToken: req.csrfToken() }); }); router.post("/", csrfProtection, async (req, res) => { const email = req.body.email; const authentication = await db.User.findOne({ where: { email: email } }); const normalPassword = req.body.password; const hashedPassword = authentication?.password; bcrypt.compare(normalPassword, hashedPassword, (error, passwordEqual) => { if (authentication && passwordEqual) { req.session.userId = authentication.id; req.session.userName = authentication.name; if (req.session.url) { res.redirect(req.session.url); } else { res.redirect("/index"); } } else { res.redirect("/login"); } }); }); module.exports = router;
const csrf = require('csurf');
でインストールしています。const csrfProtection = csrf()
でCSRF対策のためのミドルウェアを作ります。今回はクッキーではなくセッションにトークンを保持してCSRF対策をしています。引数を渡さないとデフォルトでセッションを設定した事になるのでここでは引数はなしです。(クッキーを使用する場合は引数で設定します)csrfProtection
をCSRF対策を施したい各ルートに対して埋め込みます。- トークンは
req.csrfToken()
で取得できるので、これをテンプレートにパラメータとして渡してフォームに埋め込むためにres.render
の第二引数に持たせています。
login.ejs
<!DOCTYPE html> <html lang="jp"> <head> <%- include("./_share/meta.ejs") %> <title>Document</title> </head> <body> <h1>ログイン画面</h1> <form action="/login" method="post"> <input type="hidden" name="_csrf" value="<%= csrfToken %>" /> <p>メールアドレス</p> <input type="text" name="email"> <p>パスワード</p> <input type="password" name="password"> <input type="submit" value="ログイン"> <a href="/signup">ユーザー登録画面へ</a> <a href="/index">一覧ページへ</a> <a href="/passwordReset">パスワードをお忘れの方はこちら</a> </body> </html>
formタグの一つ下のinputタグに注目してください。
type=”hidden”
とすることで見えないフォームを生成しています。value=”<%= csrfToken %>”
ここで先程、res.render
の第二引数で持たせたトークンをフォームに埋め込んでいます。
こちらを検証ツールで確認してみます。
トークンの値を変更するとpostリクエストはエラーになり、実装したフォームからでしかpostリクエストは通らないよう実装でき、csrf対策ができている事を確認できるかと思います。
参考
2022年3月27日 Node.js CSRFパッケージ
CSRF対策
今回は2の対策をしていきます。
パッケージ導入
yarn add csrf
- ミドルウェアとして組み込みではなく、必要な箇所へ実装していくものです。
具体的な実装方法
secretとtokenの生成と検証
const Tokens = require('csrf'); const tokens = new Tokens(); (async function () { // 発行 const secret = await tokens.secret(); const token = tokens.create(secret); // 検証 if (!tokens.verify(secret, token)) { throw new Error("invalid token!"); } })();
- secretはサーバー保持(セッション)
- tokenはクライアント返却(クッキー)
この2つの組み合わせが正しくないとNGだという仕組みがCSRF対策です。
データ登録の直前に検証をします。
例えば登録→確認→完了という遷移をする際に、登録→確認の遷移時(登録の内容をサーバにリクエストする時)にそのリクエストが正当なものか?を検証しても意味ない(あくまで、確認→完了の遷移の際(データ登録時)にそのリクエストが正当である事を確かめる必要がある)
Tokensクラス コンストラクタ
引数 options
- saltLength 内部利用するsalt長さ。デフォルト 8。
- secretLength secret長さ。デフォルト18。
戻り値
- tokensインスタンス
TokensクラスAPI
secret()
- 構文
tokens.secret(callback)
サーバー保存するsecretの生成- 引数 callback
(err, secret) ⇒ {}
secret生成後に呼び出される関数 - 戻り値 引数がない場合はPromise
create()
- 構文
tokens.create(secret)
クライアントへ戻す tokenの生成 - 引数 secret secret()で生成されたsecret文字列
- 戻り値 token文字列
verify()
- 構文
tokens.verify(secret, token)
与えられたsecretとtokenの組み合わせが正しいか検証 - 引数 secret 生成したsecret token 生成したtoken
- 戻り値 組み合わせが正しい場合 true それ以外 false
- 引数 callback
参考
2022年3月25日 Node.js Express ルーティング
ルーティングとは
リクエストに対して処理の振り分けを行う仕組みの事です。
expressの基本機能です。
ルーティングの実装
app.
関数名
METHOD ⇨ リクエストメソッド(get, post, put, deleteなど)
path ⇨ 振り分けたいURL
callback ⇨ pathマッチした時の処理
GETメソッドで /home/indexにアクセスするには
const express = require("express"); const app = express(); app get("/home/index", (req, res, next) => { res.status(200).send("OK"); }); app.listen(3000);
- ミドルウェアと似ていますが
next()
がいらなく、responseに対して何かを返す必要があります。 - sendメソッドでOKという文字列を返しています。
ルーティングのモジュール化
ある程度の機能単位でモジュール化する実装方法
express.Router()を使ってモジュール化します。
モジュール化した先のファイル
const router = require("express").Router(); router.get("/index", (req, res, next) => { res.status(200).send("OK"); }); module.exports = router;
読み込みファイル
const express = require("express"); const app = express(); app use("/index", require("./routes/index.js"); app.listen(3000);
- useメソッドの第二引数でモジュールでエクスポートしたものを受け取っています。
パスパラメータ
サーバー側にデータを渡す方法です。
- urlのパスの中に変数を埋め込みます。
- http://localhost3000/shops/123だとしたら123がパスパラメータに当たります。(ID)
const express = require("express"); const app = express(); app.get("/shops/:id, (req, res, next) => { res.status(200).send(req.params.id); }); app.listen(3000);
- getメソッド第一引数でパスパラメータを
<PARAM_NAME>
で指定します。 - 取得されたパラメータは
req.params
から変数として取り出します。 - 取り出す変数名は
/shops/:id
の:パラメータ名
で取得可能です。
node expressで使えるHTMLのテンプレートエンジンについて
- HTMLの雛形に対して動的に値やHTMLタグを埋め込む仕組みです。
- HTMLの雛形に動的なデータを埋め込んでHTMLを返します。
ejs
種類は他にもあり記法が変わります。
私はRailsのerbをよく使っていたので記法はejsが直感的だったのでejsを使います。
<body> <h1><%= title %></h1> </body>
こういった記法を用います。
基本構文
種類
値出力(エスケープ)
基本これを使います。XXS対策にもなります。
<%= %>
値出力(アンエスケープ)
インクルードの時のみ使用
<%- %>
コード実行
<% %>
コメント
<%# %>
インクルードとは
テンプレートの中にテンプレートを読み込ませる仕組みです。
<%- include(path [, locals]) %>
引数
- path インクルードしたいテンプレートのファイルパス
- locals インクルードするテンプレートに引き渡すデータ
戻り値
- 生成されたHTML
参考
2022年3月25日 Node.js Express
ストリームインスタンス
Node.jsによって提供される多くのストリームオブジェクトがあります。
例えば、HTTPサーバーへのリクエストとprocess.stdout
はどちらもストリームインスタンスです。
pipeイベント
読み取り可能なストリームでstream.pipe()メソッドが呼び出されたときに発行され、この書き込み可能なオブジェクトを宛先のセットに追加します。
const writer = getWritableStreamSomehow(); const reader = getReadableStreamSomehow(); writer.on('pipe', (src) => { console.log('Something is piping into the writer.'); assert.equal(src, reader); }); reader.pipe(writer);
process.stdoutプロパティ
stdoutに接続されたストリームを返します。fd 1がファイルを参照している場合を除き、net Socket(デュプレックスストリーム)です。ファイルを参照している場合は、書き込み可能なストリームです。
例として、process.stdiinをprocess.stdoutにコピーするには以下のようになります。
process.stdin.pipe(process.stdout);
process.env
Node.jsにおける環境変数はprocess.envというオブジェクトに格納されます。
このオブジェクトは最初から存在しており、環境変数が入っています。
ターミナルでnodeを入力しprocess.envを入力すると中身が見れます。
expressの可能不可能
できる事
- リクエストのルーティング
- レスポンスの基本的な整形
できない事
- クッキー
- セッション
- 認証認可
- DB接続
※できない事はミドルウェアを導入して利用できるようにします。
ミドルウェアとは
リクエストレスポンスに対して任意の追加処理を行う関数です。
- ベースはexpressが処理をして足りないものをミドルウェアが補います。
できる事
等です。
ミドルウェアの実装
通常の処理
function (req, res, next) { // 処理 next(); }
- 引数は三つです。
- 次のミドルウェアに
next();
をつけて渡すようにする(しないと返ってこない処理になります)
エラー処理
function (err, req, res, next) { // 処理 next(); }
- 第一引数にエラーが含まれるので引数は4つです。
ミドルウェアの組み込み
const express require("express"); const app = express(); app.use((req, res, next) => { }); app.listen(3000);
- app.use()の引き渡す事で組み込めます。
利用時の注意点
- 全てのリクエストに対して処理されます。
- コードの上から順に実行されるので記述箇所に注意する。
参考
2022年3月24日 Node.js入門 非同期ファイル読み書き
同期処理を行うとNode.jsの良さがなくなってしまうため利用は避けるべきです。
非同期処理の方法を見ていきます。
非同期ファイル読み込みをする場合
ファイルサイズが小さく後続処理も簡易な場合に利用します。
Promise 非同期処理の読み書き時にコールバックが多くなる問題の解消
util.promisifyメソッド
- utilを
require
します。 - util.promisifyメソッドを使用します。(Node.js v8以降の機能です)
- util.promisifyメソッドはコールバックを渡すとPromise化されたオブジェクトが取得できます。
- async awaitも使えるようになります。
util.promisifyメソッドを使った例
const fs = require("fs"); const path = require("path"); const util = require("util"); const readFile = util.promisify(fs.readFile); const writeFile = util.promisify(fs.writeFile); readFile(path.join(__dirname, "sample.txt"), "utf-8") .then((data) => { return writeFile(path.join(__dirname, "sample.txt"), "utf-8") }) .then(() => { console.log("OK"); }) .catch((error) => { console.log('error'); }); // 結果 OK
- util.promisifyメソッドがPromiseのオブジェクトを返しているためthenメソッドで
fulfilled
の結果が受け取れています。 - プロミスチェーンで最初のthenメソッドでfulfilled状態で返るため、二つ目のthenメソッドに処理が移っています。
async awaitで書き換えた例
const fs = require("fs"); const path = require("path"); const util = require("util"); const readFile = util.promisify(fs.readFile); const writeFile = util.promisify(fs.writeFile); (async function() { try { const data = await readFile(path.join(__dirname, "sample.txt"), "utf-8"); await writeFile(path.join(__dirname, "sample.txt"), "utf-8"); console.log(writeFile); console.log("OK"); } catch(error) { console.log(error.message); } })(); // 結果 OK
- async functionを即時実行関数で定義しています。
- async function直下でのawait式の挙動が得られるため非同期処理を同期処理のようにtry...catch構文を使用できています。
ストリーム
ストリームは大量のデータを小分けにして繰り返し処理を行います。一度に大量のデータを読み込まない事が特徴であり、メリットです。(リソースを一気に使ってしまわないようにできます)
※ファイルサイズが大きく後続処理が複雑な場合に利用します。
ストリームの種類
種類 | 説明 | 実装例 |
---|---|---|
Readable Stream | 読み込みができるストリーム | fs.ReadStream |
Writable Stream | 書き込みができるストリーム | fs.WriteStream |
Duplex Stream | 読み書き両方ができるストリーム | net.Socket |
Transform Stream | 読み書きのタイミングでデータ加工ができるストリーム | crypto.Chipher cryoto.Dechipher zlib.Gzip zlib.Gunzip |
ReadableStream ファイル読み込み
イベント
- data 小さなデータ一個読み込んだ時に発火
- end 全てのデータが読み終わった時に発火
メソッド
- pause() 読み込みを一時的に停止
- resume() データ読み込み開始, 再開
- pipe(stream) 次のストリームへデータを渡す
参考
2022年3月24日 Node.js入門 EventEmitter
Node.jsにおけるイベント機能の中心的存在
オブジェクトを持つイベントを管理する機能がまとめられている
- イベントが発生したとき実行する処理をあらかじめ設定します。
- イベントが発生したとき実行する処理を設定から削除します。
- 実際にイベント発生させます。
基本機能
イベント発火時の処理を設定する
EventEmitter.on(name, listener) EventEmitter.once(name, listener)
イベント発火時の処理を削除する
EventEmitter.off(name, listener)
イベントを発火する
EventEmitter.emit(name, args)
- onceは一度きりの実行のイベントに使用します。
- 削除する際は第二引数にコールバック関数を渡す必要があります。
- 一度発火したイベントはoffメソッドで止めることはできません。ロジックで止める必要があります。
利用方法
EventEmitterを継承したクラスの利用
//インスタンス作成 const obj = new Child(); //イベントの設定 obj.on('tick', (message) => { console.log(message); }); //イベントの発火 obj.emit('tick', "Hello world");
- emitメソッドがイベント発生させます。
- 第一引数のtickという同じイベント名が実行されるのでonメソッドのイベントが発生します。
- 第二引数以降に渡された値がイベント処理側に渡されます。
実用例
clock.js
const EventEmitter = require("events"); const Clock = class extends EventEmitter { constructor() { super(); this.interval = 2000; this.timer = null; } start() { if(this.timer) { this.stop(); } this.timer = global.setInterval(() => { this.emit('tick'); }, this.interval); } stop() { if(!this.timer) { return; } global.clearInterval(this.timer); this.timer = null; } }; module.exports = Clock;
index.js
const Clock = require("./clock.js"); let i = 0; const clock = new Clock(); clock.on('tick', () => { console.log(++i) if(i > 3) { clock.stop(); } }); clock.start();
処理結果
- clock.jsでイベントエミッターを設定してエクスポートしています。
- index.jsで
require
を行っています。
イベント発火時emit()実行時のポイント
- emit()で呼び出される処理は同期呼び出しです。
- on()に設定された順に呼び出します。
const EventEmitter = require("events"); const ee = new EventEmitter; ee.on("event", () => { console.log("1st");}); ee.on("event", () => { console.log("2st");}); ee.emit("event"); //結果 1st 2st
- EventEmitterを
require
しインスタンス化しています。 - onメソッドでイベントを登録して第一引数にイベント名を指定して第二引数にイベントの処理を記述しています。
- emitメソッドの引数と同名のイベントが発火しています。
- onメソッドで定義したイベントが順番に出力されています。
参考
2022年3月24日 Node.js入門 イベントループ
イベントループ
処理実行順のことです。
フェーズとキュー
6つフェーズと2つのキューがあります。
フェーズ
- Times
- pending callback
- idle
- poll
- check
- close callback
キュー
- nextTickQueue
- microTaskQueue
フェーズのみの場合はフェーズを順番に処理して回っていきます。
フェーズとキューがある場合はまずキューが実行されキューが終えたらフェーズが移行しまたキューに移行しにいくことを繰り返していきます。
キュー⇨フェーズ⇨キューの順番です。
処理内容
フェーズ | 処理内容 |
---|---|
Timers | setTimeout(),setinterval() |
pending callback | I/O完了処理、I/O例外処理 |
idole | Node.js内部処理 |
poll | I/O処理 |
check | setImmediate() |
close callback | I/O切断処理 |
キュー | 処理内容 |
---|---|
nextTickQueue | nextTick() |
microTaskQueue | Promise |
画像本教材から引用
参考
Udemy Node.js入門