Ch 03. JWT 토큰의 여행 — 세션과 토큰 전략
여권을 상상해보세요. 여권에는 이름, 생년월일, 국적, 유효기간이 적혀 있습니다. 입국심사관은 여권을 보고 당신이 누구인지 확인합니다. 서버에 전화해서 확인하지 않습니다. 여권 자체가 신원을 증명합니다.
JWT는 디지털 여권입니다.
JWT 구조: header.payload.signature
JWT는 점(.)으로 구분된 세 부분으로 이루어집니다.
eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxIiwibmFtZSI6Iuq5jOyKpOuLpCIsImV4cCI6MTcwMDAwMDAwMH0.abc123xyz header payload signature
각 부분은 Base64로 인코딩되어 있습니다. 디코딩하면 내용을 볼 수 있습니다.
Header: 토큰 타입과 서명 알고리즘을 담습니다.
{ "alg": "HS256", "typ": "JWT"}```text**Payload**: 실제 데이터(클레임)를 담습니다. 사용자 ID, 이름, 권한, 만료시간 등이 들어갑니다.```json{ "sub": "1", "name": "홍길동", "email": "[email protected]", "role": "user", "exp": 1700000000}```text**Signature**: 헤더와 페이로드를 서버의 비밀 키로 서명한 값입니다. 이 서명이 있어야 위변조를 탐지할 수 있습니다.중요한 점이 있습니다. Payload는 Base64로 인코딩된 것이지, 암호화된 것이 아닙니다. 누구나 디코딩해서 내용을 볼 수 있습니다. 따라서 **Payload에 비밀번호나 민감한 정보를 넣으면 안 됩니다.** Signature 덕분에 위변조는 불가능하지만, 내용은 공개됩니다.### NextAuth의 세션 전략`auth.ts`에서 `session.strategy`로 두 가지 중 하나를 선택합니다.#### database 전략세션을 데이터베이스에 저장합니다. 요청마다 DB에서 세션을 조회합니다. 즉시 무효화가 가능하고, 세션 데이터를 자유롭게 저장할 수 있습니다. 데이터베이스 설정이 필요합니다.```typescript// 파일: auth.tsexport const { handlers, signIn, signOut, auth } = NextAuth({ // ... session: { strategy: "database", }, // database 전략 사용 시 어댑터 필요 // adapter: PrismaAdapter(prisma),});```text#### jwt 전략세션을 JWT로 관리합니다. 서버가 상태를 저장하지 않습니다. 요청마다 JWT 서명만 검증합니다. 빠르고 확장이 쉽습니다.```typescript// 파일: auth.tsexport const { handlers, signIn, signOut, auth } = NextAuth({ // ... session: { strategy: "jwt", maxAge: 30 * 24 * 60 * 60, // 30일 (초 단위) },});```text대부분의 경우 `jwt` 전략이 더 간단하고 효율적입니다. 이 책에서는 `jwt` 전략을 사용합니다.### JWT에 커스텀 데이터 추가하기기본 JWT에는 이메일, 이름 정도가 담깁니다. 권한(role)이나 사용자 ID 같은 추가 정보가 필요하다면 `callbacks`를 사용합니다.```typescript// 파일: auth.tsimport NextAuth from "next-auth";import Credentials from "next-auth/providers/credentials";export const { handlers, signIn, signOut, auth } = NextAuth({ providers: [ Credentials({ credentials: { email: { label: "이메일", type: "email" }, password: { label: "비밀번호", type: "password" }, }, async authorize(credentials) { // DB에서 사용자 조회 (예시) const user = await getUserByEmail(credentials.email as string); if (!user) return null; return { id: user.id, name: user.name, email: user.email, role: user.role, // 커스텀 필드 }; }, }), ], callbacks: { // JWT 생성/갱신 시 호출 async jwt({ token, user }) { if (user) { // 최초 로그인 시 user 객체가 존재 token.id = user.id; token.role = (user as { role?: string }).role; } return token; }, // 세션 객체 생성 시 호출 async session({ session, token }) { if (token) { session.user.id = token.id as string; session.user.role = token.role as string; } return session; }, }, session: { strategy: "jwt" },});```text`jwt` 콜백에서 토큰에 데이터를 추가하고, `session` 콜백에서 세션 객체에 그 데이터를 노출합니다. 이렇게 하면 `auth()`로 세션을 가져올 때 `session.user.role`처럼 접근할 수 있습니다.TypeScript를 사용한다면 타입을 확장해야 합니다.```typescript// 파일: types/next-auth.d.tsimport "next-auth";declare module "next-auth" { interface User { role?: string; } interface Session { user: { id: string; role?: string; } & DefaultSession["user"]; }}declare module "next-auth/jwt" { interface JWT { id?: string; role?: string; }}```text### Edge Runtime에서의 제약Next.js의 미들웨어는 **Edge Runtime**에서 실행됩니다. Edge Runtime은 Node.js보다 훨씬 가벼운 환경으로, V8 엔진 위에서 동작하지만 Node.js 표준 라이브러리(`fs`, `crypto` 등)를 사용할 수 없습니다.NextAuth의 `auth()` 함수 내부에는 Node.js 전용 코드가 포함될 수 있어, 미들웨어에서 `auth()`를 직접 호출하면 오류가 발생할 수 있습니다. 이 제약을 해결하는 방법은 다음 챕터에서 자세히 다룹니다.### 토큰 만료와 갱신JWT는 `exp` 클레임에 만료 시간이 있습니다. 만료된 토큰은 서명이 유효해도 거부됩니다. NextAuth는 세션의 `maxAge` 설정에 따라 토큰 만료 시간을 자동으로 설정합니다.기본값은 30일입니다. 보안이 중요한 서비스라면 짧게(예: 1시간), 편의성이 중요하다면 길게(예: 90일) 설정합니다.```typescriptsession: { strategy: "jwt", maxAge: 7 * 24 * 60 * 60, // 7일 updateAge: 24 * 60 * 60, // 24시간마다 갱신},
updateAge는 토큰이 갱신되는 주기입니다. 사용자가 활발하게 사용 중이라면 만료 시간이 자동으로 연장됩니다.
다음 챕터에서는 모든 요청의 입구인 middleware.ts를 작성합니다.