Nest.js ブログをステップバイステップで追加:認可
Wenhao Wang
Dev Intern · Leapcell

このチュートリアルでは、ブログにユーザー認証および認可機能を追加します。実装する機能は以下のとおりです。
- ユーザー登録およびログイン機能。
- ユーザーログイン状態を管理するためのセッションクッキーメカニズム。
- サービス再起動後もログイン状態が失われないようにするためのRedisを使用したセッション永続化。
- ログインユーザーのみがアクセスできるように、投稿作成機能の保護。
早速始めましょう。
1. 認証方法の選択
Web開発において、最も一般的な認証方法は、「トークンベース(例:JWT)」と「セッションベース(Cookie)」の2つです。
- JWT(JSON Web Tokens): サーバーはユーザーの状態を保存しません。ユーザーがログインすると、サーバーは暗号化されたトークンを生成し、クライアントに返します。クライアントは、後続のリクエストにこのトークンを含め、サーバーはその正当性を検証します。この方法は、ステートレスなRESTful APIやシングルページアプリケーション(SPA)に最適です。
- セッション-Cookie: ユーザーがログインすると、サーバーはセッションを作成し、セッションIDをCookie経由でブラウザに返します。ブラウザは、後続のリクエストにこのCookieを自動的に含めます。サーバーは、CookieのセッションIDを使用して対応するセッション情報を見つけ、それによってユーザーを識別します。
このチュートリアルでは、セッション-Cookie法を選択します。その理由は、私たちのブログがサーバーサイドレンダリング(EJS)を行う従来のアプリケーションであるためです。このモデルでは、ブラウザとサーバーが密接に結合されており、Cookieベースのセッション管理が最も直接的で、古典的で、安全な方法となります。Nest.jsもこのアプローチを優れた組み込みサポートで提供しています。
2. ユーザーモジュールの作成
前回のチュートリアルでposts
モジュールを作成したのと同様に、まずユーザーを管理するモジュールが必要です。
Nest CLIを使用して、必要なファイルを迅速に生成します。
nest generate module users nest generate controller users nest generate service users
次に、データベースのusers
テーブルにマッピングするために、src/users
ディレクトリにユーザーエンティティファイルuser.entity.ts
を作成します。
// src/users/user.entity.ts import { Entity, Column, PrimaryGeneratedColumn } from 'typeorm'; @Entity() export class User { @PrimaryGeneratedColumn('uuid') id: string; @Column({ unique: true }) username: string; @Column() password: string; // 格納されるパスワードは暗号化されたハッシュになります }
その後、User
エンティティを操作できるように、UsersModule
(src/users/users.module.ts
)にTypeOrmModule
を登録します。
// src/users/users.module.ts import { Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; import { UsersController } from './users.controller'; import { UsersService } from './users.service'; import { User } from './user.entity'; @Module({ imports: [TypeOrmModule.forFeature([User])], controllers: [UsersController], providers: [UsersService], exports: [UsersService], // 他のモジュールで使用するためにUsersServiceをエクスポートします }) export class UsersModule {}
最後に、LeapcellまたはローカルPostgreSQLデータベースで以下のSQLステートメントを実行して、user
テーブルを作成します。
CREATE TABLE "user" ( "id" UUID PRIMARY KEY DEFAULT gen_random_uuid(), "username" VARCHAR UNIQUE NOT NULL, "password" VARCHAR NOT NULL );
3. 登録とパスワード暗号化の実装
ユーザーパスワードは、データベースに平文で格納してはいけません。パスワードのハッシュ化と暗号化にはbcrypt
ライブラリを使用します。
まず、必要な依存関係をインストールします。
npm install bcrypt npm install -D @types/bcrypt
次に、ユーザーの作成と検索ロジックを追加するためにsrc/users/users.service.ts
を更新します。
// src/users/users.service.ts import { Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { Repository } from 'typeorm'; import { User } from './user.entity'; import * as bcrypt from 'bcrypt'; @Injectable() export class UsersService { constructor( @InjectRepository(User) private usersRepository: Repository<User> ) {} async create(user: Partial<User>): Promise<User> { const saltRounds = 10; const hashedPassword = await bcrypt.hash(user.password || '', saltRounds); const newUser = this.usersRepository.create({ username: user.username, password: hashedPassword, }); return this.usersRepository.save(newUser); } async findOne(username: string): Promise<User | null> { return this.usersRepository.findOneBy({ username }); } }
4. セッションと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
Authモジュールの作成
ログイン、ログアウト、戦略などのすべての認証関連ロジックを、独立したauth
モジュールに配置します。
nest generate module auth nest generate service auth
ユーザーパスワードの検証ロジックを追加するためにsrc/auth/auth.service.ts
を変更します。
// src/auth/auth.service.ts import { Injectable } from '@nestjs/common'; import { UsersService } from '../users/users.service'; import * as bcrypt from 'bcrypt'; @Injectable() export class AuthService { constructor(private usersService: UsersService) {} async validateUser(username: string, pass: string): Promise<any> { const user = await this.usersService.findOne(username); if (user && (await bcrypt.compare(pass, user.password))) { const { password, ...result } = user; return result; // 検証成功、パスワードなしのユーザーオブジェクトを返します } return null; // 検証失敗 } }
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; } }
Authモジュールの設定
作成したサービスと戦略をauth.module.ts
にまとめます。
// 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], providers: [AuthService, LocalStrategy], }) export class AuthModule {}
5. セッションにRedisを使用する
デフォルトでは、express-session
はセッションをサーバーのメモリに保存します。これは、サーバーが再起動すると、すべてのユーザーログイン状態が失われることを意味します。これを解決するために、セッションを永続化するために、高性能インメモリデータベースであるRedisを使用します。
Redisがない場合 現時点でRedisサービスが利用できない場合は、この手順をスキップして構いません。
express-session
は自動的にメモリストレージを使用するようにフォールバックされます。これはローカル開発では問題ありませんが、本番環境には適さないことに注意してください。
Redis関連の依存関係をインストールします。
npm install redis connect-redis
src/main.ts
ファイルを開いて、セッションミドルウェアを設定し、Redisに接続します。
// src/main.ts import { NestFactory } from '@nestjs/core'; import { NestExpressApplication } from '@nestjs/platform-express'; import { join } from 'path'; import { AppModule } from './app.module'; 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 Client の初期化 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(async (id: string, done) => { // ここでUsersServiceにfindByIdメソッドを追加する必要があります // const user = await usersService.findById(id); // 簡単にするため、一時的にIDだけのオブジェクトを返します done(null, { id }); }); app.useStaticAssets(join(__dirname, '..', 'public')); app.setBaseViewsDir(join(__dirname, '..', 'views')); app.setViewEngine('ejs'); await app.listen(3000); } bootstrap();
注意: deserializeUser
を正しく機能させるためには、UsersService
にfindById
メソッドを追加する必要があります。
6. ログインおよび登録ページの作成
ユーザーが操作するためのインターフェースを提供する必要があります。views
フォルダにlogin.ejs
とregister.ejs
を作成します。
-
register.ejs
<%- include('_header', { title: 'Register' }) %> <form action="/users/register" method="POST" class="post-form"> <h2>Register</h2> <div class="form-group"> <label for="username">Username</label> <input type="text" id="username" name="username" required /> </div> <div class="form-group"> <label for="password">Password</label> <input type="password" id="password" name="password" required /> </div> <button type="submit">Register</button> </form> <%- include('_footer') %>
-
login.ejs
<%- include('_header', { title: 'Login' }) %> <form action="/auth/login" method="POST" class="post-form"> <h2>Login</h2> <div class="form-group"> <label for="username">Username</label> <input type="text" id="username" name="username" required /> </div> <div class="form-group"> <label for="password">Password</label> <input type="password" id="password" name="password" required /> </div> <button type="submit">Login</button> </form> <%- include('_footer') %>
7. ルートとコントローラーロジックの実装
登録およびログインリクエストを処理するルートを作成しましょう。
登録ルート
src/users/users.controller.ts
を更新します。
// src/users/users.controller.ts import { Controller, Get, Post, Render, Body, Res } from '@nestjs/common'; import { UsersService } from './users.service'; import { Response } from 'express'; @Controller('users') export class UsersController { constructor(private readonly usersService: UsersService) {} @Get('register') @Render('register') showRegisterForm() { return; } @Post('register') async register(@Body() body: any, @Res() res: Response) { // 簡単にするため、ここでは複雑な検証は行いません await this.usersService.create(body); res.redirect('/auth/login'); // 登録成功後にログインページにリダイレクトします } }
ログインおよびログアウト ルート
ログインとログアウトを処理する新しいauth.controller.ts
を作成します。
nest generate controller auth
src/auth/auth.controller.ts
を編集します。
// 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')) @Post('login') async login(@Request() req, @Res() res: Response) { // 検証成功後、Passport はユーザーオブジェクトを req.user にアタッチします res.redirect('/posts'); } @Get('logout') logout(@Request() req, @Res() res: Response) { req.logout((err) => { if (err) { console.log(err); } res.redirect('/'); }); } }
最後に、app.module.ts
にUsersModule
とAuthModule
をインポートすることを忘れないでください。
// src/app.module.ts // ... imports import { UsersModule } from './users/users.module'; import { AuthModule } from './auth/auth.module'; @Module({ imports: [ // ... TypeOrmModule.forRoot(...) PostsModule, UsersModule, AuthModule, ], // ... }) export class AppModule {}
8. ルートの保護と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(); return request.isAuthenticated(); // Passport によって追加されるメソッドで、セッションにユーザーが存在するかどうかを確認します } }
ガードの適用
src/posts/posts.controller.ts
を開き、保護が必要なルートに@UseGuards(AuthenticatedGuard)
デコレータを適用します。
// src/posts/posts.controller.ts import { Controller, Get, Render, Param, Post, Body, Res, UseGuards } from '@nestjs/common'; import { AuthenticatedGuard } from 'src/auth/authenticated.guard'; // ...その他のインポート @Controller('posts') export class PostsController { // ...コンストラクター // ... findAll() @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'); } // ... findOne() }
これで、ログインしていないユーザーが/posts/new
にアクセスしようとすると、自動的にインターセプトされるようになります。
フロントエンドUIの更新
最後に、ユーザーのログイン状態に基づいて異なるボタンを表示するようにUIを更新しましょう。これには、EJSテンプレートをレンダリングする際にユーザーのログイン状態をフロントエンドに渡す必要があります。これにはapp.controller.ts
とposts.controller.ts
に小さな変更が必要です。
簡単な方法は、@Render
を使用するすべてのルートでreq.user
を明示的に渡すことです。
ログイン/登録およびログアウト/新規投稿のリンクを追加するために_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.username || '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>
posts.controller.ts
のroot
メソッドで、user
情報をビューに渡す必要があります。
// src/posts/posts.controller.ts // ... import { Request } from 'express'; // Request をインポート @Controller('posts') export class PostsController { // ... @Get() @Render('index') async root(@Request() req) { // Request オブジェクトを注入 const posts = await this.postsService.findAll(); return { posts, user: req.user }; // ユーザーオブジェクトをテンプレートに渡します } // ...他の @Render ルートにも同様の変更を加えます @Get(':id') @Render('post') async post(@Param('id') id: string, @Request() req) { const post = await this.postsService.findOne(id); return { post, user: req.user }; } }
9. 実行とテスト
アプリケーションを再起動します。
npm run start:dev
http://localhost:3000
にアクセスします。
- 右上隅に「Login」と「Register」ボタンが表示されます。
- 「Register」をクリックして、新しいユーザーを作成します。
- 登録が成功すると、ログインページにリダイレクトされます。作成したアカウントでログインします。
- ログインが成功すると、右上隅のボタンが「New Post」と「Logout」に変わります。これで新しい投稿を作成できるようになります。
これで、ブログに完全なユーザー認証システムを正常に追加しました。