Nest.jsブログをステップバイステップで追加:認証
Takashi Yamamoto
Infrastructure Engineer · Leapcell

しかし、現在のログインは一度きりのイベントです。サーバーはユーザーのログイン状態を記憶しません。ページがリフレッシュされるたびに、ユーザーはゲストに戻ります。
この記事では、Passport.js と Session を使用して、ブログの真のユーザーログイン状態管理を実装します。これにより、アクセスするためにログインが必要なページや機能が保護され、ユーザーのログインステータスに基づいてインターフェイスが動的に更新されます。
セッションとPassport.jsの設定
ログインロジックとセッション管理を処理するために、人気のNode.js認証ライブラリであるpassport
と、セッションを管理するためのexpress-session
を使用します。
関連する依存関係をインストールします。
npm install @nestjs/passport passport passport-local express-session npm install -D @types/passport @types/passport-local @types/express-session
Passport Local Strategyの実装
Passportは、さまざまな認証方法を処理するために「ストラテジー」を使用します。ここでは、前回の記事で作成したユーザー名とパスワード検証ロジックを使用する「ローカルストラテジー」を実装します。
src/auth
ディレクトリにlocal.strategy.ts
を作成します。
// src/auth/local.strategy.ts import { Strategy } from 'passport-local'; import { PassportStrategy } from '@nestjs/passport'; import { Injectable, UnauthorizedException } from '@nestjs/common'; import { AuthService } from './auth.service'; @Injectable() export class LocalStrategy extends PassportStrategy(Strategy) { constructor(private authService: AuthService) { super(); } async validate(username: string, password: string): Promise<any> { const user = await this.authService.validateUser(username, password); if (!user) { throw new UnauthorizedException(); } return user; } }
validate
メソッドはPassportによって自動的に呼び出されます。ログインフォームからのusername
とpassword
を受け取り、以前に作成したauthService.validateUser
を使用してそれらを検証します。
Authモジュールの更新
次に、auth.module.ts
でPassportModule
とLocalStrategy
を登録します。
// src/auth/auth.module.ts import { Module } from '@nestjs/common'; import { AuthService } from './auth.service'; import { UsersModule } from '../users/users.module'; import { PassportModule } from '@nestjs/passport'; import { LocalStrategy } from './local.strategy'; @Module({ imports: [UsersModule, PassportModule.register({ session: true })], // Enable session controllers: [AuthController], providers: [AuthService, LocalStrategy], exports: [AuthService], }) export class AuthModule {}
Redisを使用してセッションを保存する
デフォルトでは、express-session
はセッションをサーバーのメモリに保存します。これは、サーバーが再起動すると、すべてのユーザーのログイン状態が失われることを意味します。この問題を解決するために、セッションを永続化するために高性能なインメモリデータベースであるRedisを使用します。
Redisがない場合はどうなりますか?
LeapcellでRedisインスタンスを作成できます。Leapcellはバックエンドアプリケーションに必要なほとんどのツールを提供します
インターフェイスの「Redisの作成」ボタンをクリックして、新しいRedisインスタンスを作成します。
Redisの詳細ページにはオンラインCLIがあり、Redisコマンドを直接実行できます。
利用可能なRedisサービスがない場合、express-session
は自動的にメモリ内ストレージの使用にフォールバックします。ただし、これは本番環境ではベストプラクティスではなく、問題につながる可能性があります。
Redis関連の依存関係をインストールします。
npm install redis connect-redis
ここで、src/main.ts
ファイルを開いてセッションミドルウェアを設定し、Redisに接続します。
// src/main.ts import { NestFactory } from '@nestjs/core'; import { AppModule } from './app.module'; import { NestExpressApplication } from '@nestjs/platform-express'; import { join } from 'path'; import session from 'express-session'; import passport from 'passport'; import { createClient } from 'redis'; import { RedisStore } from 'connect-redis'; async function bootstrap() { const app = await NestFactory.create<NestExpressApplication>(AppModule); // Initialize Redis Client const redisClient = createClient({ // If your Redis has a password or is on another host, modify the configuration here // url: 'redis://:password@hostname:port' url: 'redis://localhost:6379', }); await redisClient.connect().catch(console.error); // Initialize RedisStore const redisStore = new RedisStore({ client: redisClient, prefix: 'blog-session:', }); app.use( session({ store: redisStore, // Use Redis for storage secret: 'your-secret-key', // Replace with a random, complex string resave: false, saveUninitialized: false, cookie: { httpOnly: true, maxAge: 1000 * 60 * 60 * 24 * 7, // 7 days }, }) ); app.use(passport.initialize()); app.use(passport.session()); // Passport Session Serialization and Deserialization // When a user logs in successfully, user information is serialized and stored in the session passport.serializeUser((user: any, done) => { done(null, user.id); }); // For each request, Passport finds the user via the ID in the session and deserializes it passport.deserializeUser((id: string, done) => { // In a real application, you should find the user in the database here // const user = await usersService.findById(id); // For simplicity, we will temporarily return an object containing just the id done(null, { id: id }); }); app.useStaticAssets(join(__dirname, '..', 'public')); app.setBaseViewsDir(join(__dirname, '..', 'views')); app.setViewEngine('ejs'); await app.listen(3000); } bootstrap();
serializeUser
: このメソッドは、ユーザーが正常にログインした後に呼び出されます。セッションにどのユーザー情報が保存されるかを決定します。ここでは、user.id
のみを保存します。deserializeUser
: 後続のリクエストごとに、Passportはセッションからuser.id
を読み込み、それを使用して完全なユーザー情報を取得し、そのユーザー情報をrequest.user
にアタッチします。
実際のログインおよびログアウトルートの実装
設定が完了したので、auth.controller.ts
を更新して、Passportを使用してログインとログアウトを処理できるようにします。
// src/auth/auth.controller.ts import { Controller, Get, Post, Render, Request, Res, UseGuards } from '@nestjs/common'; import { AuthGuard } from '@nestjs/passport'; import type { Response } from 'express'; @Controller('auth') export class AuthController { @Get('login') @Render('login') showLoginForm() { return; } @UseGuards(AuthGuard('local')) // Use the AuthGuard with the 'local' strategy @Post('login') async login(@Request() req, @Res() res: Response) { // When code execution reaches this point, it means the LocalStrategy's validate method has executed successfully // Passport automatically creates a session and attaches the user object to req.user res.redirect('/posts'); } @Get('logout') logout(@Request() req, @Res() res: Response) { req.logout((err) => { if (err) { console.log(err); } res.redirect('/'); }); } }
@UseGuards(AuthGuard('local'))
デコレータは、/auth/login
へのPOSTリクエストを自動的にインターセプトし、LocalStrategy
を実行します。検証が成功すると、セッションが確立され、login
メソッドの本体が呼び出されます。失敗した場合、自動的に401 Unauthorized
エラーがスローされます。
ルートの保護とUIの更新
ログインメカニズムができたので、最後のステップは、それを使用して「投稿の作成」機能を保護し、ログインステータスに基づいて異なるUIを表示することです。
認証ガードの作成
Nest.jsのガードは、指定されたリクエストを処理すべきかどうかを決定するクラスです。ユーザーがログインしているかどうかを確認するためにAuthenticatedGuard
を作成します。
src/auth
ディレクトリにauthenticated.guard.ts
を作成します。
// src/auth/authenticated.guard.ts import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common'; @Injectable() export class AuthenticatedGuard implements CanActivate { async canActivate(context: ExecutionContext): Promise<boolean> { const request = context.switchToHttp().getRequest(); // request.isAuthenticated() is a method added by Passport to check if a user exists in the session return request.isAuthenticated(); } }
ガードの適用
src/posts/posts.controller.ts
を開き、保護が必要なルートに@UseGuards(AuthenticatedGuard)
デコレータを適用します。
// src/posts/posts.controller.ts import { Controller, Get, Render, Post, Body, Res, UseGuards, Param } from '@nestjs/common'; import { AuthenticatedGuard } from '../auth/authenticated.guard'; // Path may need adjustment import { PostsService } from './posts.service'; import type { Response } from 'express'; @Controller('posts') export class PostsController { constructor(private readonly postsService: PostsService) {} // ... findAll() and findOne() @UseGuards(AuthenticatedGuard) // <--- Apply the guard @Get('new') @Render('new-post') newPostForm() { return; } @UseGuards(AuthenticatedGuard) // <--- Apply the guard @Post() async create(@Body() body: { title: string; content: string }, @Res() res: Response) { await this.postsService.create(body); res.redirect('/posts'); } // ... }
これで、ログインしていないユーザーが/posts/new
にアクセスしようとすると、自動的にインターセプトされます(デフォルトでは403 Forbidden
エラーが返されます)。
フロントエンドUIの更新
最後に、ユーザーのログインステータスに基づいて異なるボタンを表示するようにUIを更新しましょう。req.user
ログインステータスをEJSテンプレートのレンダリング時にフロントエンドに渡す必要があります。
views/_header.ejs
を編集して、ログイン/登録およびログアウト/新規投稿のリンクを追加します。
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title><%= title %></title> <link rel="stylesheet" href="/css/style.css" /> </head> <body> <header> <h1><a href="/">My Blog</a></h1> <div class="user-actions"> <% if (user) { %> <span>Welcome, User</span> <a href="/posts/new" class="new-post-btn">New Post</a> <a href="/auth/logout">Logout</a> <% } else { %> <a href="/auth/login">Login</a> <a href="/users/register">Register</a> <% } %> </div> </header> <main></main> </body> </html>
上記コードを機能させるには、EJSテンプレートにuser
情報を渡すようにコントローラーを更新する必要があります。
posts.controller.ts
のすべてのビューをレンダリングするメソッドを修正します。
// src/posts/posts.controller.ts import { Controller, Get, Render, Param, /*...,*/ Request } from '@nestjs/common'; // ... @Controller('posts') export class PostsController { // ... @Get() @Render('index') async root(@Request() req) { const posts = await this.postsService.findAll(); return { posts, user: req.user }; // Pass user to the template } // ... @Get(':id') @Render('post') async findOne(@Param('id') id: string, @Request() req) { const post = await this.postsService.findOne(id); return { post, user: req.user }; // Pass user to the template } }
views/_header.ejs
を使用する他のコントローラ(auth.controller.ts
やusers.controller.ts
など)も、同様の調整が必要になります。
実行とテスト
これで、アプリケーションを再起動します。
npm run start:dev
http://localhost:3000
にアクセスします。
- 右上隅に「Login」と「Register」ボタンが表示されます。
- 「Register」をクリックして新しいユーザーを作成します。
- 登録が成功したら、ログインページにリダイレクトされます。作成したアカウントでログインします。
- ログインに成功すると、ホームページにリダイレクトされ、右上隅に「Welcome, User」、「New Post」、「Logout」ボタンが表示されます。
- この時点で、「New Post」をクリックして新しい記事を作成できます。ログアウトしてから
/posts/new
にアクセスしようとすると、拒否されます。
これで、ブログに完全なユーザー認証システムが追加されました。友達があなたのブログをいじくり回す心配はもうありません!