Node.js アプリケーション用の迅速かつ直感的で合理化された認証ソリューションを模索する中で、機能を損なうことなく迅速な実装が必要なシナリオに遭遇しました。
ユーザーのサインアップとログインから、忘れたパスワードの管理、ユーザー データの更新、さらにはアカウントの削除に至るまで、これらの重要なユーザー インタラクションをシームレスにナビゲートする包括的なソリューションを探しました。
したがって、私の記事はまさにそれを提示することを目的としています。それは、認証とキャッシュを実装するための明確な方法論を統合し、堅牢で効率的なユーザー フローを確保する一貫したアプローチです。
ここでは、基本的なインストール手順とモデルの作成を省略し、認証とユーザー フローの複雑さに直接焦点を当てます。記事全体に構成ファイルを取得するために必要なリンクをすべて記載し、セットアップに必要なリソースへのシームレスなアクセスを保証します。
この実装では、Knex、Express、Redis とともに Node.js バージョン 20.11.1 を利用します。さらに、PostgreSQL をデータベースとして利用し、シームレスな管理のために Docker を使用してコンテナ化および調整されます。
アプリケーションの名前はuser-flow-boilerplate
になります。そのフォルダーを作成し、その中でnpm init -y
を実行して基本的なpackage.json
を生成しましょう。
{ "name": "user-flow-boilerplate", "version": "1.0.0", "description": "", "main": "index.js", "scripts": { "test": "echo \"Error: no test specified\" && exit 1" }, "keywords": [], "author": "", "license": "ISC" }
初期パッケージ.json
次のステップでは、必要な依存関係を追加します。
依存関係: npm i -S bcrypt body-parser cors dotenv express jsonwebtoken knex pg redis validator
devDependency :
npm i -D @babel/core @babel/eslint-parser @babel/plugin-transform-class-properties @babel/plugin-transform-runtime @babel/preset-env @babel/preset-typescript @faker-js/faker @types/bcrypt @types/body-parser @types/cors @types/express @types/jest @types/jsonwebtoken @types/node @types/node-cron @types/validator @typescript-eslint/eslint-plugin @typescript-eslint/parser babel-jest cross-env eslint eslint-config-prettier eslint-plugin-prettier jest nodemon npm-run-all prettier ts-jest ts-loader ts-node tsconfig-paths tslint typescript webpack webpack-cli webpack-node-externals
そして、アプリケーションを構築して実行するスクリプトを追加します。
"scripts": { "start": "NODE_ENV=production node dist/bundle.js", "build": "NODE_ENV=production webpack --config webpack.config.js", "dev": "cross-env NODE_ENV=development && npm-run-all -p dev:*", "dev:build": "webpack --config webpack.config.js --watch", "dev:start": "nodemon --watch dist --exec node dist/bundle.js", "test": "NODE_ENV=test jest --config ./jest.config.js", "lint": "eslint ./src -c .eslintrc.json" },
アプリケーションをスムーズに起動するには、 src
フォルダーを作成し、その中に最初のエントリ ポイント ファイル、 index.ts
を配置することが重要です。
require('dotenv').config(); import process from 'process'; import express from 'express'; import bodyParser from 'body-parser'; import cors from 'cors'; const app = express(); const PORT = process.env.PORT || 9999; app.use(bodyParser.json()); app.use(cors()); app.get('/api/v1/health', (req, res) => res.status(200).json({ message: 'OK' })); (async () => { try { app.listen(PORT, async () => { console.log(`Server is running on port ${PORT}`); }); } catch (error) { console.error('Failed to start server:', error); process.exit(1); } })();
エントリポイントファイル
開発には、 typscript
、 lint
、 jest
、 bable
、 prettier
、 nodemon
の設定が必要です。これらのファイルはすべて、「Express で Postgres と Knex を使用した Node.js サーバーの作成」の記事で説明しました。
すべての設定を構成し、エントリ ポイントを作成した後、 npm run dev
を実行するとサーバーが起動され、次のような出力が表示されるはずです。
./src/index.ts 1.7 KiB [built] [code generated] external "dotenv" 42 bytes [built] [code generated] external "process" 42 bytes [built] [code generated] external "express" 42 bytes [built] [code generated] external "body-parser" 42 bytes [built] [code generated] external "cors" 42 bytes [built] [code generated] webpack 5.90.3 compiled successfully in 751 ms [nodemon] restarting due to changes... [nodemon] starting `node dist/bundle.js` Server is running on port 9999
次に、次の場所に移動しますGET
リクエストを追加し、 cmd + E
を押して (Mac の場合、キーは OS によって異なります)、それにhealth
という名前を付けます。
URL: {{BASE_URI}}/health
に Enter を追加します。 BASE_URI
の場合、コレクション全体で使用する新しい変数を追加します: http://localhost:9999/api/v1
その後、「送信」ボタンをクリックするだけで、応答本文が表示されるはずです。
{ "message": "OK" }
先に進む前に、データベースを稼働させておくことが重要です。これをdocker-compose
で起動することで実現します。データベースにアクセスして管理するには、次のようなさまざまな開発プラットフォームを利用できます。
個人的には、使用することを好みます
必要なキー、パスワード、テスト名を含む.env
ファイルが必要です。
PORT=9999 WEB_HOST="localhost" # DB DB_HOST="localhost" DB_PORT=5432 DB_NAME="user_flow_boilerplate" DB_USER="username_123" DB_PASSWORD="SomeParole999" # User DEFAULT_PASSWORD="SomeParole999" JWT_SECRET="6f1d7e9b9ba56476ae2f4bdebf667d88eeee6e6c98c68f392ed39f7cf6e51c5a" # Test User TEST_EMAIL="[email protected]" TEST_USERNAME="test_username" TEST_PASSWORD="SomeParole999" # Redis REDIS_HOST="localhost" REDIS_PORT=6379 REDIS_DB=0 REDIS_PASSWORD="SomeParole999"
データベース、Redis、およびシードのテスト値への接続用の .env
心配しないでください。より本物の方法で説明するために、 JWT_SECRET
ランダムに生成しました。そこで、プロジェクトのルートにdocker-compose.yml
ファイルを作成しましょう。
version: '3.6' volumes: data: services: database: build: context: . dockerfile: postgres.dockerfile image: postgres:latest container_name: postgres environment: TZ: Europe/Madrid POSTGRES_DB: ${DB_NAME} POSTGRES_USER: ${DB_USER} POSTGRES_PASSWORD: ${DB_PASSWORD} networks: - default volumes: - data:/var/lib/postgresql/data ports: - "5432:5432" restart: unless-stopped redis: image: redis:latest container_name: redis command: redis-server --requirepass ${REDIS_PASSWORD} networks: - default ports: - "6379:6379" restart: unless-stopped
サービスを含む docker-compose ファイル
迅速な接続のために、Docker で 2 つのサービスを起動します。このプロセスを合理化して、データベースまたは Redis への素早いアクセスを容易にし、データを効率的に取得できるようにしました。そこで、これらのサービスdocker-compose up
を実行しましょう。 docker ps
の後の出力を確認できるようにする必要があります。
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES e4bef95de1dd postgres:latest "docker-entrypoint.s…" About a minute ago Up About a minute 0.0.0.0:5432->5432/tcp postgres 365e3a68351a redis:latest "docker-entrypoint.s…" About a minute ago Up About a minute 0.0.0.0:6379->6379/tcp redis
ここで、アプリケーションの型を保存するsrc/@types/index.ts
ファイルを作成する必要があります。
export enum Role { Admin = 'admin', Blogger = 'blogger', } export type UserSession = { id: number; }; export type DatabaseDate = { created_at: Date; updated_at: Date; }; export type DefaultUserData = { role: Role; }; export interface User extends DatabaseDate { id: number; email: string; username: string; password: string; role: Role; }
サービスの種類
現時点では、接続、移行、およびシードのために、プロジェクトとデータベース フォルダーのルートにknexfile.ts
が必要です。
これらの環境変数を使用しているデータベースにユーザーを移行してシードする方法については、Express で Postgres と Knex を使用した Node.js サーバーの作成の記事にかなり詳細な説明を残しました。
移行を具体的にチェックして、同じ認識を持っていることを確認したいと思います。サービスはすでに開始されており、データベースへの接続を確認できるようにする必要があります。
docker exec -it postgres psql -U username_123 user_flow_boilerplate
接続が良好であれば、 psql
コンソールが表示されます。接続に問題がなければ、そこにテーブルを移行できるはずです。 knex migrate:latest
を実行します。次に、データベース内のusers
テーブルに新しく追加された列を観察する必要があります。
偽のデータknex seed:run
をシードして、テーブルを再度確認してみましょう。
これで、データベースを操作し、必要に応じてユーザーを追加、削除、更新できるようになりました。
最後に、設定や準備のことを忘れて、特にユーザー フローに集中できるようになります。そのためにはルーターを作成する必要があります。そのルータによって、次の操作を処理する必要があります: login
、 logout
、 signup
、 delete_user
、 update_user
。
そのために、 src/routes/index.ts
に次のコードを追加します。
import { Router } from 'express'; import { authRouter } from 'src/routes/authRouter'; import { healthController } from 'src/controllers/healthController'; import { sessionController } from 'src/controllers/sessionController'; import { authMiddleware } from 'src/middlewares/authMiddleware'; import { userRouter } from 'src/routes/userRouter'; export const router = Router({ mergeParams: true }); router.get('/health', healthController); router.use('/auth', authRouter); router.get('/session', authMiddleware, sessionController); router.use('/user', authMiddleware, userRouter); router.use((_, res) => { return res.status(404).json({ message: 'Not Found' }); });
ルートファイル
ご覧のとおり、最初に、すでに確認した/health
ルートを追加しました。それでは、エントリ ポイントを更新して、そこにこれらのルートを適用しましょう。まず、以前のget
を削除します。
-> REMOVE -> app.get('/api/v1/health', (req, res) => res.status(200).json({ message: 'OK' }));
ファイルの先頭に次の内容を追加します。
import { router } from 'src/routes'; // ... app.use(cors()); app.use('/api/v1', router);
次のコードを使用して、 health
チェック用の最初のコントローラーsrc/controllers/healthController.ts
を作成します。
import { Request, Response } from 'express'; export const healthController = (_: Request, res: Response) => res.status(200).send('ok');
ヘルスコントローラー
さて、ルーターに戻り、ルートにさらに何を追加する必要があるかを確認してみましょう。さらに 2 つのファイル、 authRouter.ts
とuserRouter.ts
を追加する必要があります。
import { Router } from 'express'; import { signUpController } from 'src/controllers/auth/signUpController'; import { loginController } from 'src/controllers/auth/loginController'; export const authRouter = Router(); authRouter.post('/signup', signUpController); authRouter.post('/login', loginController);
認証ルーター
import { Router } from 'express'; import { updateUserController } from 'src/controllers/user/updateUserController'; import { deleteUserController } from 'src/controllers/user/deleteUserController'; import { logoutController } from 'src/controllers/user/logoutController'; import { updatePasswordController } from 'src/controllers/user/updatePasswordController'; export const userRouter = Router(); userRouter.patch('/', updateUserController); userRouter.delete('/', deleteUserController); userRouter.post('/logout', logoutController); userRouter.post('/update-password', updatePasswordController);
ユーザールーター
読みやすさと、分離された機能を維持する責任を考慮して、このロジックを分割しました。これらすべてのルートには、ロジックを処理するコントローラーが必要です。
認証ルートと正常性ルートには認証ミドルウェアが必要ないため、これらのルートは保護されませんが、一致しない場合はステータス 404 が返されます。
router.get('/health', healthController); router.use('/auth', authRouter);
すべてのルートが決定したので、ユーザー モデルを設定する必要があります。
ユーザー モデルの基本モデルを利用し、そこから CRUD メソッドを再利用します。モデルの作成については以前別の記事で説明しましたが、src/models/Model.ts
に作成
import { database } from 'root/database'; export abstract class Model { protected static tableName?: string; protected static get table() { if (!this.tableName) { throw new Error('The table name must be defined for the model.'); } return database(this.tableName); } public static async insert<Payload>(data: Payload): Promise<{ id: number; }> { const [result] = await this.table.insert(data).returning('id'); return result; } public static async updateOneById<Payload>( id: number, data: Payload ): Promise<{ id: number; }> { const [result] = await this.table.where({ id }).update(data).returning('id'); return result; } public static async delete(id: number): Promise<number> { return this.table.where({ id }).del(); } public static async findOneById<Result>(id: number): Promise<Result> { return this.table.where('id', id).first(); } public static async findOneBy<Payload, Result>(data: Payload): Promise<Result> { return this.table.where(data as string).first(); } }
ベースモデル
基本モデルでは、同じフォルダーにUserModel.ts
を作成できる必要があります。
import { Model } from 'src/models/Model'; import { Role, User, DefaultUserData } from 'src/@types'; export class UserModel extends Model { static tableName = 'users'; public static async create<Payload>(data: Payload) { return super.insert<Payload & DefaultUserData>({ ...data, role: data.role || Role.Blogger, }); } public static findByEmail(email: string): Promise<User | null> { return this.findOneBy< { email: string; }, User >({ email }); } public static findByUsername(username: string): Promise<User | null> { return this.findOneBy< { username: string; }, User >({ username }); } }
ユーザーモデル
ユーザーのモデルでは、ペイロードから提供されない場合はデフォルトでrole
を設定します。モデルの準備ができたので、コントローラーとミドルウェア内でモデルを利用できるようになります。
Node.js アプリケーションの認証ミドルウェアは、受信リクエストを認証し、それらのリクエストが有効で承認されたユーザーからのものであることを確認する役割を果たします。
通常、受信リクエストをインターセプトし、認証トークンまたは資格情報を抽出し、事前定義された認証メカニズム (この場合は JWT (JSON Web トークン) など) に対してその有効性を検証します。
認証プロセスが成功すると、ミドルウェアはリクエストがリクエスト/レスポンス サイクル内の次のハンドラーに進むことを許可します。ただし、認証が失敗した場合は、適切な HTTP ステータス コード (例: 401 Unauthorized) で応答し、オプションでエラー メッセージを表示します。
フォルダーsrc/middlewares
を作成し、そこに次のコードを含むファイルauthMiddleware.ts
を追加します。
import { jwt } from 'src/utils/jwt'; import { Redis } from 'src/redis'; import type { Request, Response, NextFunction } from 'express'; import type { UserSession } from 'src/@types'; export async function authMiddleware(req: Request, res: Response, next: NextFunction) { const authHeader = req.headers['authorization']; const token = authHeader && authHeader.split(' ')[1]; const JWT_SECRET = process.env.JWT_SECRET; if (!token) return res.sendStatus(401); if (!JWT_SECRET) { console.error('JWT_SECRET Not Found'); return res.sendStatus(500); } if (!token) return res.status(401).json({ error: 'Token not provided' }); try { const userSession = await jwt.verify<UserSession>(token); if (!userSession) { return res.sendStatus(401); } const storedToken = await Redis.getSession(userSession.id); if (!storedToken || storedToken !== token) { return res.sendStatus(401); } req.user = userSession; next(); } catch (error) { console.error('JWT_ERROR', error); return res.sendStatus(401); } }
認証ミドルウェアファイル
認証ミドルウェアは、リクエスト ヘッダーから JWT トークンを抽出し、JWT ライブラリを使用してその有効性を検証し、トークンが Redis に保存されているトークンと一致するかどうかを確認します。
トークンが有効で、保存されているトークンと一致する場合、ミドルウェアはリクエスト オブジェクト ( req.user
) に認証されたユーザー セッションを設定し、 next()
関数を呼び出して次のミドルウェアまたはルート ハンドラーに制御を渡します。それ以外の場合は、認証の失敗を示す 401 ステータス コードで応答します。
jwt のユーティリティを確認してみましょう。次のコードを使用してsrc/utils/jwt.ts
ファイルを作成します。
require('dotenv').config(); import jsonwebtoken from 'jsonwebtoken'; const JWT_SECRET = process.env.JWT_SECRET as string; export const jwt = { verify: <Result>(token: string): Promise<Result> => { if (!JWT_SECRET) { throw new Error('JWT_SECRET not found in environment variables!'); } return new Promise((resolve, reject) => { jsonwebtoken.verify(token, JWT_SECRET, (error, decoded) => { if (error) { reject(error); } else { resolve(decoded as Result); } }); }); }, sign: (payload: string | object | Buffer): Promise<string> => { if (!JWT_SECRET) { throw new Error('JWT_SECRET not found in environment variables!'); } return new Promise((resolve, reject) => { try { resolve(jsonwebtoken.sign(payload, JWT_SECRET)); } catch (error) { reject(error); } }); }, };
JWTユーティリティファイル
このユーティリティは、Node.js アプリケーション内で JSON Web トークンを処理する際に重要な役割を果たします。 jwt
オブジェクトは、 jsonwebtoken
ライブラリを利用して、JWT の署名と検証の両方の関数をエクスポートします。これらの関数は、アプリケーションに認証メカニズムを実装するために不可欠な JWT の作成と検証を容易にします。
ユーティリティは、JWT を処理する機能をカプセル化し、環境変数管理のベスト プラクティスに準拠しながら、Node.js アプリケーション内で安全な認証メカニズムを確保します。
データベース、キャッシュ、メッセージ ブローカーとして使用されます。キャッシュ、セッション管理、リアルタイム分析、メッセージング キュー、リーダーボードなど、さまざまなユースケースで一般的に使用されます。
Redis からのトークンの確認は、JWT トークンのセキュリティと検証の追加レイヤーとして機能します。設定を見てみましょう。そのためには、次のコードを使用してファイルsrc/redis/index.ts
を作成します。
require('dotenv').config({ path: '../../.env', }); import process from 'process'; import * as redis from 'redis'; const client = redis.createClient({ url: `redis://:${process.env.REDIS_PASSWORD}@${process.env.REDIS_HOST}:${process.env.REDIS_PORT}`, }); client.on('error', error => console.error('Redis Client Error', error)); const connect = async () => { try { await client.connect(); console.log('Connected to Redis'); } catch (err) { console.error(`Could not connect to Redis: ${err}`); process.exit(1); } }; class Redis { public static setSession(userId: number, token: string) { if (!userId) throw new Error('userId is required'); if (!token) throw new Error('token is required'); try { return client.set(`session:${userId}`, token); } catch (error) { console.error(error); } } public static getSession(userId: number) { if (!userId) throw new Error('userId is required'); return client.get(`session:${userId}`); } public static deleteSession(userId: string) { if (!userId) throw new Error('userId is required'); try { return client.del(`session:${userId}`); } catch (error) { console.error(error); } } } export { client, connect, Redis };
Redis セッション ストア
Redis により、ユーザー セッション トークンを保存および管理します。認証ミドルウェアでは、JWT トークンの信頼性を検証した後、トークンが存在するかどうか、および対応するユーザー セッションの Redis に保存されているトークンと一致するかどうかを確認します。これにより、有効で許可されたユーザーのみが保護されたルートにアクセスできるようになります。
Redis は、ユーザー セッション トークンを維持するためのキーと値のストアとして使用されます。ユーザーがログインまたは認証すると、セッション トークンが Redis に保存されます。これにより、後続の認証チェック中にセッション トークンを効率的かつ高速に取得できるようになります。
Redis は効率的なセッション管理のために認証ミドルウェアで利用され、Redis 関連ファイルは構成と Redis サーバーへの接続を処理し、アプリケーションの他の部分で Redis と対話するための機能を提供します。
この設定により、ユーザー セッション トークンが Redis に保存および管理されるため、安全で信頼性の高い認証メカニズムが保証されます。
最後の部分では、エントリ ポイントで Redis に接続する必要があります。
// all imports import * as Redis from 'src/redis'; const app = express(); const PORT = process.env.PORT || 9999; // middlewares (async () => { try { await Redis.connect(); app.listen(PORT, async () => { console.log(`Server is running on port ${PORT}`); }); } catch (error) { console.error('Failed to start server:', error); process.exit(1); } })();
Redis に接続する
認証の準備が完了したら、コントローラーに焦点を移すことができます。
ルート内のコントローラーは、懸念事項を分離し、コードの保守性を促進することで、アプリケーションのロジックを整理するのに役立ちます。ヘルスチェック用のコントローラーはすでに作成されています。次に、ユーザーの操作を処理するコントローラーの作成に進みます。
最初に使用するコントローラーはsessionController.ts
で、次のコードを含むsrc/controllers
内に存在する必要があります。
import { Request, Response } from 'express'; import { UserModel } from 'src/models/UserModel'; import type { User } from 'src/@types'; export const sessionController = async (req: Request, res: Response) => { if (!req.user) return res.sendStatus(401); try { const user = await UserModel.findOneById<User>(req.user.id); if (user) { return res.status(200).json(user); } else { return res.sendStatus(401); } } catch (error) { return res.sendStatus(500); } };
セッションコントローラー
このコントローラーは、セッション関連のエンドポイントを処理する目的を果たし、現在認証されているユーザーに関する情報を取得する役割を果たします。このコントローラーが必要なのは次の理由です。
ユーザー セッション情報:このコントローラーを使用すると、アプリケーションはユーザー プロフィールやその他の関連データなど、ユーザーのセッションに関する情報を取得できます。この情報は、ユーザー エクスペリエンスをカスタマイズしたり、ユーザーのプロファイルに基づいてパーソナライズされたコンテンツを提供したりするのに役立ちます。
認証と認可: req.user
存在するかどうかを確認することで、コントローラーは認証されたユーザーのみがエンドポイントにアクセスできるようにします。これにより、認証と認可のルールが適用され、許可されたユーザーのみが機密ユーザー データにアクセスできるようになります。
ユーザー プロファイルの取得:コントローラーは ( UserModel
を使用して) データベースにクエリを実行し、セッション ID に基づいてユーザーの情報を取得します。これにより、アプリケーションはユーザー固有のデータを動的にフェッチし、各ユーザーに合わせたエクスペリエンスを提供できるようになります。この部分は Redis キャッシュによって確実に改善できます。
import { Request, Response } from 'express'; import { UserModel } from 'src/models/UserModel'; import { Redis } from 'src/redis'; import type { User } from 'src/@types'; export const sessionController = async (req: Request, res: Response) => { if (!req.user) return res.sendStatus(401); try { const cachedProfile = await Redis.getSession(req.user.id); if (cachedProfile) { return res.status(200).json(JSON.parse(cachedProfile)); } else { const user = await UserModel.findOneById<User>(req.user.id); if (user) { await Redis.setSession(req.user.id, JSON.stringify(user), CACHE_EXPIRATION); return res.status(200).json(user); } else { return res.sendStatus(401); } } } catch (error) { console.error('Error retrieving user profile:', error); return res.sendStatus(500); } };
Redis がセッションを設定したセッション コントローラー ファイル
定数CACHE_EXPIRATION
を定義して、キャッシュの有効期限を秒単位で指定します。この例では、3600 秒 (1 時間) に設定されています。キャッシュされたデータは定期的に更新されるため、古いデータがユーザーに提供されるのを防ぎ、キャッシュ内のデータの整合性が維持されます。
アプリケーションでの新規ユーザーのサインアップ プロセスを管理するsignUpController
作成に進む前に、スキーマを確認してみましょう。
私たちの場合、データベース内の既存の電子メールでサインアップしようとするとき、ユーザーが存在するかどうかを明示的に明らかにしないことで、ユーザーのプライバシーを優先します。代わりに、 Invalid email or password
という一般的なメッセージをクライアントに通知します。
このアプローチにより、クライアントは既存のユーザーに関する不必要な情報を開示することなく、有効な資格情報を送信することができます。
次に、 src/controllers/auth/signUpController.ts
を作成し、次のコードを追加しましょう。
import bcrypt from 'bcrypt'; import { jwt } from 'src/utils/jwt'; import { Request, Response } from 'express'; import { validate } from 'src/helpers/validation/validate'; import { userSchema } from 'src/helpers/validation/schemas/userSchema'; import { UserModel } from 'src/models/UserModel'; import { Redis } from 'src/redis'; import type { User } from 'src/@types'; import { getRandomString } from 'src/utils/getRandomString'; type Payload = Omit<User, 'id' | 'created_at' | 'updated_at' | 'role'>; export async function signUpController(req: Request, res: Response) { const { email, password }: Payload = req.body; const validation = validate<Payload>(req.body, userSchema); if (!validation.isValid) { return res.status(400).send(`Invalid ${validation.invalidKey}`); } try { const user = await UserModel.findOneBy({ email }); if (user) { return res.status(400).json({ message: 'Invalid email or password' }); } const hashedPassword = (await bcrypt.hash(password, 10)) as string; const username = `${email.split('@')[0]}${getRandomString(5)}`; const createdUser = await UserModel.create<Payload>({ email, password: hashedPassword, username, }); const token = await jwt.sign({ id: createdUser.id, }); await Redis.setSession(createdUser.id, token); res.status(200).json({ token, }); } catch (error) { return res.sendStatus(500); } }
サインアップコントローラー
コントローラーは、通常はサインアップ フォームから、ユーザーの電子メールとパスワードを含むリクエストを受け取ります。受信データを事前定義されたuserSchema
と照合して検証し、必要な形式を満たしていることを確認します。
検証が成功し、既存のユーザーと有効なフィールドがないことが示された場合、コントローラーはbcrypt.hash
を使用してパスワードをハッシュし、 username
生成し、 UserModel.create
を使用してユーザーを作成します。
最後に、 jwt
使用してtoken
を生成し、 Redis
にsession
データを設定して、 token
ユーザーに送り返します。
ここで、ログイン コントローラーの作成に焦点を当てましょう。ファイルsrc/controllers/auth/loginController.ts
を作成します。
require('dotenv').config({ path: '../../.env', }); import bcrypt from 'bcrypt'; import { Request, Response } from 'express'; import { jwt } from 'src/utils/jwt'; import { UserModel } from 'src/models/UserModel'; import { Redis } from 'src/redis'; export async function loginController(req: Request, res: Response) { const { email, password } = req.body; if (!email || !password) { return res.status(400).json({ message: 'Invalid email or password' }); } try { const user = await UserModel.findByEmail(email); if (user) { const isValidPassword = await bcrypt.compare(password, user.password); if (!isValidPassword) { return res.status(400).json({ message: 'Invalid email or password' }); } const token: string = await jwt.sign({ id: user.id, }); await Redis.setSession(user.id, token); res.status(200).json({ token }); } else { return res.status(400).json({ message: 'Invalid email or password' }); } } catch (error) { console.error(error); return res.sendStatus(500); } }
ログインコントローラー
基本的に、提供されたフィールドを検証してから、ユーザーの存在を確認することから始めます。ユーザーが見つからない場合は、 signupController
の動作と同様にInvalid email or password
メッセージとともに 400 ステータス コードで応答します。
ユーザーが存在する場合は、 bcrypt.compare
を使用して、指定されたパスワードとデータベースに保存されているハッシュ化されたパスワードの比較が行われます。
パスワードが一致しない場合は、「電子メールまたはパスワードが無効です」というおなじみのメッセージが表示されます。最後に、認証が成功すると、トークンを生成し、Redis でセッションを設定し、トークンをクライアントに送り返します。
保護されたコントローラーを確認してみましょう。保護されたコントローラーは、ミドルウェアから取得した user_id の存在に依存します。これらのコントローラー内の操作では、一貫してこの user_id に依存します。リクエストにauthorization
ヘッダーがない場合は、 401
ステータス コードで応答する必要があります。
const authHeader = req.headers['authorization'];
次のコードを使用してファイルsrc/controllers/user/logoutController.ts
を作成します。
import type { Request, Response } from 'express'; import { Redis } from 'src/redis'; export async function logoutController(req: Request, res: Response) { try { await Redis.deleteSession(req.user.id); return res.sendStatus(200); } catch (error) { return res.sendStatus(500); } }
ログアウトコントローラー
このlogoutController
、システムからユーザーをログアウトする役割を果たします。リクエストを受信すると、Redis クライアントと対話して、 user.id
に関連付けられたセッションを削除します。操作が成功すると、ログアウトの成功を示すステータス コード200
が返されます。
ただし、プロセス中にエラーが発生した場合は、内部サーバー エラーを知らせるために500
ステータス コードで応答します。
次に、ユーザーデータの削除について説明します。
src/controllers/user/deleteUserController.ts
を作成し、次のコードを追加します。
import { Request, Response } from 'express'; import { UserModel } from 'src/models/UserModel'; import { Redis } from 'src/redis'; export const deleteUserController = async (req: Request, res: Response) => { const user_id = req.user.id; try { await Redis.deleteSession(user_id); await UserModel.delete(user_id); return res.sendStatus(200); } catch (error) { return res.sendStatus(500); } };
ユーザーコントローラーの削除
リクエストを受信すると、通常は認証ミドルウェアから取得されるリクエスト オブジェクトからユーザー ID を抽出します。
その後、Redis クライアントを使用して、このuser_id
に関連付けられたセッションを Redis から削除します。その後、 UserModel
のdelete
メソッドを呼び出して、データベースからユーザーのデータを削除します。
セッションとユーザー データの両方の削除が成功すると、削除が成功したことを示す200
ステータス コードで応答します。削除プロセス中にエラーが発生した場合、内部サーバー エラーを示す500
ステータス コードで応答します。
システム内のユーザー データを更新するにはsrc/controllers/user/updateUserController.ts
を作成し、次のコードをファイルに追加します。
import { Request, Response } from 'express'; import { UserModel } from 'src/models/UserModel'; import { filterObject } from 'src/utils/filterObject'; type Payload = { first_name?: string; last_name?: string; username?: string; }; export const updateUserController = async (req: Request, res: Response) => { const { first_name, last_name, username } = req.body; const payload: Payload = filterObject({ first_name, last_name, username, }); try { const existingUserName = await UserModel.findByUsername(username); if (existingUserName) { return res.status(400).json({ error: 'Invalid username', }); } const updatedUser = await UserModel.updateOneById<typeof payload>(req.user.id, payload); res.status(200).json(updatedUser); } catch (error) { res.sendStatus(500); } };
ユーザーコントローラーの更新
リクエストを受信すると、リクエスト本文からフィールドfirst_name
、 last_name
、およびusername
を抽出します。次に、 filterObject
ユーティリティ関数を使用してこれらのフィールドをフィルタリングし、有効なフィールドのみがペイロードに含まれるようにします。
次に、指定されたusername
データベースにすでに存在するかどうかを確認します。存在する場合、コントローラは400
ステータス コードと、無効なusername
を示すエラー メッセージで応答します。 username
が一意である場合、コントローラーはUserModel
のupdateOneById
メソッドを使用してデータベース内のユーザー データの更新を開始します。
更新が成功すると、ステータス コード200
と更新されたユーザー データが返されます。更新プロセス中にエラーが発生した場合、コントローラーは内部サーバー エラーを示す500
ステータス コードで応答します。
最後の方法はパスワードの更新です。ユーザー データの更新とほぼ同じ考え方ですが、新しいパスワードをハッシュします。リストsrc/controllers/user/updatePasswordController.ts
から最後のコントローラーを作成し、コードを追加します。
import { Request, Response } from 'express'; import { UserModel } from 'src/models/UserModel'; import bcrypt from 'bcrypt'; export const updatePasswordController = async (req: Request, res: Response) => { try { const { password } = req.body; if (!password) return res.sendStatus(400); const hashedPassword = (await bcrypt.hash(password, 10)) as string; const user = await UserModel.updateOneById(req.user.id, { password: hashedPassword }); return res.status(200).json({ id: user.id }); } catch (error) { return res.sendStatus(500); } };
パスワードコントローラーの更新
リクエストを受信すると、リクエスト本文から新しいパスワードを抽出します。次に、リクエスト本文にパスワードが指定されているかどうかを確認します。そうでない場合は、不正なリクエストを示す400
ステータス コードで応答します。次に、ソルト係数 10 のbcrypt
ライブラリを使用して新しいパスワードをハッシュします。
ハッシュされたパスワードは、 UserModel
のupdateOneById
メソッドを使用してデータベースに安全に保存され、 user.id
に関連付けられます。パスワードの更新が成功すると、コントローラーはステータス コード200
とユーザーの ID を含む JSON オブジェクトで応答します。
パスワード更新プロセス中にエラーが発生した場合、コントローラは他のコントローラと同様に内部サーバー エラーを示す500
ステータス コードで応答します。
検証ヘルパーとユーティリティを確認して設定してください。
サインアップ エンドポイントを確認してみましょう。
明らかなように、セッションを取得するためにヘッダーで使用されるトークンを取得しました。
ヘッダー内の認証トークンをサーバーに送信すると、それに応じてサーバーはデータベースから取得したユーザー データを提供しました。
セキュリティ機能と Redis キャッシュを自由に調べて実験してください。基本モデルが整備されていれば、パスワードを忘れたユーザーのアカウント回復などの追加機能を詳しく調べることができます。ただし、このトピックは将来の記事のために保留されます。
ルーティングとユーザー認証フローをスケーラブルな方法で管理するのは困難な場合があります。ルートを保護するためにミドルウェアを実装しましたが、サービスのパフォーマンスと信頼性を強化するために利用できる追加の戦略もあります。
エラー処理は依然として重要な側面であり、より包括的な対応が必要であるため、より明確なエラー メッセージを提供することでユーザー エクスペリエンスがさらに向上します。ただし、プライマリ認証フローは正常に実装されており、ユーザーはサインアップ、アカウントへのアクセス、セッション データの取得、ユーザー情報の更新、アカウントの削除を行うことができます。
この旅が洞察力に富み、ユーザー認証に関する貴重な知識を得られたことを願っています。
ここでも公開されています