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-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 })], // セッションを有効にする providers: [AuthService, LocalStrategy], exports: [AuthService], }) export class AuthModule {}
セッションの保存にRedisを使用する
デフォルトでは、express-session
はセッションをサーバーのメモリに保存します。これは、サーバーを再起動すると、すべてのユーザーのログイン状態が失われることを意味します。この問題を解決するために、高性能インメモリデータベースであるRedisを使用してセッションを永続化します。
Redisがない場合はどうなりますか?
LeapcellでRedisインスタンスを作成できます。Leapcellはバックエンドアプリケーションに必要なほとんどのツールを提供します!
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 * as session from 'express-session'; import * as passport from 'passport'; import { createClient } from 'redis'; import RedisStore from 'connect-redis'; async function bootstrap() { const app = await NestFactory.create<NestExpressApplication>(AppModule); // Redisクライアントの初期化 const redisClient = createClient({ // Redisにパスワードがある場合や別のホストにある場合は、ここで設定を変更してください // url: 'redis://:password@hostname:port' url: 'redis://localhost:6379', }); await redisClient.connect().catch(console.error); // RedisStoreの初期化 const redisStore = new RedisStore({ client: redisClient, prefix: 'blog-session:', }); app.use( session({ store: redisStore, // ストレージにRedisを使用 secret: 'your-secret-key', // ランダムで複雑な文字列に置き換えてください resave: false, saveUninitialized: false, cookie: { httpOnly: true, maxAge: 1000 * 60 * 60 * 24 * 7, // 7日間 }, }) ); app.use(passport.initialize()); app.use(passport.session()); // Passportセッションのシリアライズとデシリアライズ // ユーザーが正常にログインすると、ユーザー情報はシリアライズされてセッションに保存されます passport.serializeUser((user: any, done) => { done(null, user.id); }); // 各リクエストで、Passportはセッション内のIDを介してユーザーを見つけ、それをデシリアライズします passport.deserializeUser((id: string, done) => { // 実際のアプリケーションでは、ここでデータベースでユーザーを検索する必要があります // const user = await usersService.findById(id); // 簡単にするために、ここでは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 { Response } from 'express'; @Controller('auth') export class AuthController { @Get('login') @Render('login') showLoginForm() { return; } @UseGuards(AuthGuard('local')) // AuthGuardを'local'ストラテジーで使用 @Post('login') async login(@Request() req, @Res() res: Response) { // コード実行がここに来たということは、LocalStrategyのvalidateメソッドが正常に実行されたことを意味します // Passportは自動的にセッションを作成し、ユーザーオブジェクトを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()は、Passportがセッションにユーザーが存在するかどうかを確認するために追加したメソッドです 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'; // パスは調整が必要な場合があります import { PostsService } from './posts.service'; import { Response } from 'express'; @Controller('posts') export class PostsController { constructor(private readonly postsService: PostsService) {} // ... findAll()とfindOne() @UseGuards(AuthenticatedGuard) // <--- ガードを適用 @Get('new') @Render('new-post') newPostForm() { return; } @UseGuards(AuthenticatedGuard) // <--- ガードを適用 @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を更新して、ユーザーのログイン状態に基づいて異なるボタンを表示しましょう。EJSテンプレートをレンダリングする際に、req.user
(ユーザーのログイン状態)をフロントエンドに渡す必要があります。
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>
上記のコードを機能させるには、コントローラーを更新して、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 findAll(@Request() req) { const posts = await this.postsService.findAll(); return { posts, user: req.user }; // テンプレートにuserを渡す } // ... @Get(':id') @Render('post') async findOne(@Param('id') id: string, @Request() req) { const post = await this.postsService.findOne(id); return { post, user: req.user }; // テンプレートにuserを渡す } }
実行とテスト
これで、アプリケーションを再起動します。
npm run start:dev
http://localhost:3000
にアクセスします。
- 画面右上に「Login」と「Register」ボタンが表示されます。
- 「Register」をクリックして新しいユーザーを作成します。
- 登録が成功したら、ログインページにリダイレクトされます。作成したアカウントでログインします。
- ログインに成功すると、ホームページにリダイレクトされ、画面右上に「Welcome, User」、「New Post」、「Logout」ボタンが表示されます。
- この時点で、「New Post」をクリックして新しい記事を作成できます。ログアウトしてから
/posts/new
にアクセスしようとすると、アクセスが拒否されます。
これで、ブログに完全なユーザー認証システムを追加しました。友人があなたのブログをいじくり回す心配はもうありません!