バックエンド側 node.js アクセスログ/認証チェック/ミドルウェア

2023-11-04

少しでも期間が空くと思い出すのに頭が痛くなるほどのパクリ17発目!

パクリ元

内容

以下一覧を完了させて ECS リリースさせることでバックエンド側を終わりとする。順序は適宜入れ替える。

  • 開発環境構築
  • プロジェクト作成・最適なフォルダ構成
  • (認証・認可) フロントエンドとの連携が必要なので今回省略。
  • 認証チェック(含めた前処理)     ←←← 今回ココから。セッションチェックでなく『認証チェック(含めた前処理)』に変更
  • ログ処理
  • APIによってはIPチェックやHeaderチェック
  • バリデーション
  • DB参照・更新
  • API取得
  • API取得待ち合わせ
  • 長時間処理を非同期処理で
  • メール送信
  • CSVアップロード
  • ファイルダウンロード
  • EXCEL/PDF作成
  • 環境ファイルによる振り分け
  • テスト
  • AWS環境へデプロイ

nodemon修正

前回の nodemon 指定だと更新が反映されなかったので修正する。

~/nodetest/package.json
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "dev": "nodemon src/index.ts --ext '*'"
  },

認証チェック(含めた前処理)

セッションにおいては、「セッションを試してみる」的なページでカウントアップするようなのが多いけどそんなん使わんわな普通。また、セッションでなく cookie 側に持たせて認証を行うのも流行ってきているようなのでセッションチェックではなく認証チェックとした。

通常業務ロジックまでの前処理でやるような、Laravelでいうミドルウェア的な処理を検討する。パクリ元一つ目を参考に node のミドルウェアについて(ルーティング含め next() に流す)確認。

とりあえず前処理として必要な、「リクエストから JSON の読み取り」「リクエストから cookie の読み取り」ができるミドルウェアを指定しておく。

リクエストから JSON の読み取りとオブジェクトへの変換

~/nodetest/src/index.ts
app.use(express.json());

パクリ元2つ目から、上記ミドルウェアを追加しておくと、リクエストボディにある {“name":"test"} という json を req.body.name と指定して取得できるとのこと。

リクエストから cookie の読み取り

パクリ元3つ目より、cookie-parser というミドルウェアをインストール。

~/nodetest$ npm install --save cookie-parser
~/nodetest$ npm install --save-dev @types/cookie-parser
~/nodetest/src/index.ts
import cookieParser from 'cookie-parser';
・・・
app.use(cookieParser());

これで req.cookies['クッキー名’]で取得できる。

アクセスログ

次にパクリ元4つ目を参考にアクセスログを追加する。とりあえず監査用にもログ取っとかなきゃ不安だし。

~/nodetest$ npm install --save log4js
~/nodetest/config/log4js.json
{
    "appenders": { 
        "console": { "type": "console" },
        "app": {
            "type": "dateFile",
            "filename": "./log/app.log",
            "pattern": "yyyyMMdd",
            "alwaysIncludePattern": true,
			"keepFileExt": true,
			"backups": 7
        },
        "access": {
            "type": "dateFile", 
            "filename": "./log/access.log",
            "pattern": "yyyyMMdd",
            "alwaysIncludePattern": true,
            "keepFileExt": true,
			"backups": 7
        }
    },
    "categories": { 
		"default": {
			"appenders": ["console"],
			"level": "ALL"
		},
		"app": {
			"appenders": ["console", "app"],
			"level": "INFO"
		},
		"access": {
			"appenders": ["console", "access"],
			"level": "INFO"
		}
    }
}
~/nodetest/src/index.ts
import log4js from 'log4js';
・・・
log4js.configure('./config/log4js.json');
app.use(log4js.connectLogger(log4js.getLogger('access'), {level: 'auto'}));

以上でとりあえずアクセスログが作成される。また、logger を使用してログに自由に追記できるようになる。

認証チェック

つぎに認証チェック部分。とりあえずここではOK/NGだけが欲しいだけなので認証方法としてはセッションIDでも JWT でもどっちでもいいこととするが、パクリ元5つ目を参考にミドルウェアとして適用。また、エラー時の型を決めておきたいので type も適用。ここら辺は Promise だったらどうだ、のような記事もあったが経験がないので適当にエラー型を作って済ます。

~/nodetest/src/types/error.ts
export type error = { 
  result: string, 
  status: number, 
  message: string 
};
~/nodetest/src/types/authenticationError.ts
import {error} from '../types/error';

export const authenticationError: error = {
  result: "failure",
  status: 401, 
  message: "authentication error" 
};
~/nodetest/src/middleware/authenticate.ts
import type { Request, Response, NextFunction } from 'express';
import {authenticationError} from '../types/authenticationError';

export const authenticate = (req: Request, res: Response, next: NextFunction) => {
    if (req.cookies['id']/** TODO 本当の認証チェック */) {
        next()
    } else {
        next(authenticationError)
    }
}
~/nodetest/src/middleware/errorHandler.ts
import type { Request, Response, NextFunction } from 'express';
import {error} from '../types/error';

export const errorHandler = (err:error, req: Request, res: Response, next: NextFunction) => {
    res.status(err.status).json({
        result: err.result,
        message: err.message,
    });
}
~/nodetest/src/index.ts
import {authenticate} from './middleware/authenticate';
import {errorHandler} from './middleware/errorHandler';
・・・
app.use('/users', authenticate, users);
・・・
app.use(errorHandler)

以上で /users にアクセスしたときに認証チェックが走ることになり、cookie に id が存在する場合は認証OKの処理となる。url の箇所の . とか .. とかイヤだわぁ、、後で直すことになるんかな知らんけど。

~/nodetest$ curl http://localhost:3001/users
{"result":"failure","message":"authentication error"}
~/nodetest$ curl -b "id=a" http://localhost:3001/users
{"result":"success"}

IPチェック とか ヘッダーチェック

ついでに API によっては IP チェックとかヘッダに何かあるかとかあるような場合もやってみる。IP アドレスは req.id 、リクエストヘッダは req.headers.*** で取得できる模様。

~/nodetest/src/middleware/checkIpHeader.ts
import type { Request, Response, NextFunction } from 'express';
import {authenticationError} from '../types/authenticationError';
import log4js from 'log4js';

export const checkIpHeader = (req: Request, res: Response, next: NextFunction) => {
    const logger = log4js.getLogger('app');
    if (req.ip === "::ffff:127.0.0.1") {
        if (req.headers.test) {
            next()
        } else {
            logger.error("wrong header");
            next(authenticationError)
        }
    } else {
        logger.error("wrong ip:" + req.ip);
        next(authenticationError)
    }
}
~/nodetest/src/index.ts
import {checkIpHeader} from './middleware/checkIpHeader';
・・・
app.use('/users', [checkIpHeader, authenticate], users);

複数のミドルウェアをかます場合は 配列で指定する。

レスポンスヘッダの追加/削除

後はよくあるのだが、レスポンスヘッダのサーバ情報を消したりする。

~/nodetest$ curl --dump-header - http://localhost:3001/users
・・・
X-Powered-By: Express
・・・

上記 X-Powered-By: Express は不要なので消す。クラウドの場合はもう少し上のレイヤーで消すのかも知らんが、、まあいいや。

~/nodetest/src/middleware/addResponseHeader.ts
import type { Request, Response, NextFunction } from 'express';

export const addResponseHeader = (req: Request, res: Response, next: NextFunction) => {
    res.removeHeader("X-Powered-By");
    next();
}
~/nodetest/src/index.ts
import {addResponseHeader} from './middleware/addResponseHeader';
・・・
app.use(addResponseHeader);

404 対応

パクリ元6つ目を参考に、URL が存在しない場合を対応。

~/nodetest/src/index.ts
// ルーティングの後に
app.all("*", (req, res) => {
    res.send(404)
})

現状の index.ts

~/nodetest/src/index.ts
import express from 'express';
import type { Express, Request, Response, NextFunction } from 'express';
import cookieParser from 'cookie-parser';
import log4js from 'log4js';

import {checkIpHeader} from './middleware/checkIpHeader';
import {authenticate} from './middleware/authenticate';
import {errorHandler} from './middleware/errorHandler';
import {addResponseHeader} from './middleware/addResponseHeader';

import users from './routes/users';

// express
const app: Express = express();
const port = 3001;

// base
app.use(express.json());
app.use(cookieParser());

// log
log4js.configure('./config/log4js.json');
app.use(log4js.connectLogger(log4js.getLogger('access'), {level: 'auto'}));

// response header
app.use(addResponseHeader);

// routing
app.use('/users', [/*checkIpHeader, */authenticate], users);

// 404
app.all("*", (req, res) => {
    res.send(404)
})

// error handler
app.use(errorHandler)

app.listen(port, () => console.log(`Example app listening on port ${port}!`));

現状のフォルダ構成

だんだん laravel に似てきたな。。何が正解か分からんままにやっているが、

nodetest
┣━ config
┃  ┗━ log4js.json
┣━ dist
┣━ log
┃  ┣━ access.yyyymmdd.log
┃  ┗━ app.yyyymmdd.log
┣━ node_modules
┣━ src
┃  ┣━ middleware
┃  ┃   ┣━ addResponseHeader.ts
┃  ┃   ┣━ authenticate.ts
┃  ┃   ┣━ checkIpHeader.ts
┃  ┃   ┗━ errorHandler.ts
┃  ┣━ routes
┃  ┃   ┗━ users.ts
┃  ┣━ types
┃  ┃   ┣━ authenticationError.ts
┃  ┃   ┗━ error.ts
┃  ┗━ index.ts
┣━ package-lock.json
┣━ package.json
┗━ tsconfig.json

雑感

先はまだまだ長い。