Dev Study
← 解説「JWT — トークンによる認証」に戻る

サンプルコードで身につける: JWT — トークンによる認証

解説で学んだ概念を、5つの具体例で確認します。素朴な例から実務寄りの例へと進みます。

1JWTの3部構造

JWT が header.payload.signature の3つをドットでつないだ文字列であることを図で示す例です。payload に本人情報、signature に署名が入り、ドット区切りという見た目だけ先に掴みます。

// JWT = header . payload . signature をドットで連結した1つの文字列
//
//   eyJhbGciOiJIUzI1NiJ9 . eyJzdWIiOiIxIn0 . dBjftJeZ4CV...
//   └─ header ─────────┘  └─ payload ───┘  └─ signature ┘
//
//   header    … 署名アルゴリズム  例 { "alg": "HS256" }
//   payload   … 本人情報やロール  例 { "sub": "1", "role": "admin" }
//   signature … 秘密鍵で作った署名(改ざん検知用)
//
// header と payload は base64url エンコードされているだけ(暗号化ではない)

2payloadは暗号化されていない

JWT の payload が「署名されているだけで誰でも読める」ことをプレーン TypeScript で再現した例です。中身を覗けてしまう以上、パスワードなどの秘密情報を payload に入れてはいけない理由が体感できます。

TypeScript

3有効期限(exp)の検証

JWT の verify() が署名検証に加えて必ず行う「有効期限のチェック」をプレーン TypeScript で再現した例です。exp を過ぎたトークンは弾くため、漏れたトークンの悪用可能時間を短く抑えられます。

TypeScript

4JwtModule の登録と発行

@nestjs/jwt の JwtModule を秘密鍵と有効期限つきで登録し、sign() でトークンを発行する例です。秘密鍵は環境変数から読み、exp を必ず付けるのが実務の基本設定です。

import { Module, Injectable } from "@nestjs/common";
import { JwtModule, JwtService } from "@nestjs/jwt";

@Module({
  imports: [
    JwtModule.register({
      secret: process.env.JWT_SECRET, // 鍵は必ず環境変数から
      signOptions: { expiresIn: "15m" }, // 有効期限を必ず設定
    }),
  ],
})
export class AuthModule {}

@Injectable()
export class TokenService {
  constructor(private readonly jwt: JwtService) {}

  issue(user: { id: number; role: string }): string {
    // payload に秘密情報は入れない(誰でも読めるため)
    return this.jwt.sign({ sub: user.id, role: user.role });
  }
}

5リフレッシュトークンの使い分け

短命なアクセストークンと長命なリフレッシュトークンを組み合わせる実務パターンの例です。アクセストークンは短くして漏えい被害を抑え、切れたらリフレッシュトークンで再発行する、という二段構えにします。

import { Injectable } from "@nestjs/common";
import { JwtService } from "@nestjs/jwt";

@Injectable()
export class TokenService {
  constructor(private readonly jwt: JwtService) {}

  // ログイン時: 2種類のトークンを返す
  issueTokens(userId: number) {
    return {
      // アクセス: 短命。漏れても被害が及ぶ時間を短くする
      accessToken: this.jwt.sign({ sub: userId }, { expiresIn: "15m" }),
      // リフレッシュ: 長命。再発行専用で、別の鍵・別の保存先にする
      refreshToken: this.jwt.sign(
        { sub: userId, type: "refresh" },
        { expiresIn: "7d" },
      ),
    };
  }

  // /auth/refresh: リフレッシュを検証して新しいアクセスを発行
  refresh(refreshToken: string) {
    const payload = this.jwt.verify(refreshToken); // 失敗なら例外
    return { accessToken: this.jwt.sign({ sub: payload.sub }, { expiresIn: "15m" }) };
  }
}