인증 구현 — 세션과 토큰 기반 인증
Next.js에서 인증을 직접 구현하는 방법을 설명합니다. JWT와 세션 방식의 비교, jose 라이브러리를 사용한 토큰 서명·검증, Server Action 기반 로그인 흐름, httpOnly 쿠키 설정을 코드와 함께 다룹니다.
지난 글에서 Middleware의 matcher 설정으로 실행 경로를 제어하는 방법을 알아봤다. 이번에는 인증 자체를 어떻게 구현하는지 살펴본다. Next.js는 인증 라이브러리를 강제하지 않으므로 직접 구현하거나 Auth.js 같은 라이브러리를 쓸 수 있다. 이번 글에서는 직접 구현 방식을 다루고, 다음 글에서 Auth.js를 소개한다.
두 가지 인증 전략
인증 방식은 크게 **JWT(Stateless)**와 세션(Stateful) 두 가지로 나뉜다.
Next.js에서는 두 방식의 하이브리드가 자주 사용된다. 세션 정보를 암호화된 JWT에 담아 쿠키에 저장하면 Edge Runtime의 Middleware에서 DB 조회 없이 검증할 수 있다. Auth.js(NextAuth)가 이 방식을 사용한다.
jose — Edge 호환 JWT 라이브러리
Node.js의 jsonwebtoken 패키지는 Edge Runtime에서 동작하지 않는다. Middleware에서 JWT를 검증하려면 Web Crypto API를 사용하는 jose 라이브러리가 필요하다.
npm install jose
로그인 Server Action
// app/login/actions.ts
'use server'
import { cookies } from 'next/headers'
import { redirect } from 'next/navigation'
import { signToken } from '@/lib/auth'
import { db } from '@/lib/db'
import bcrypt from 'bcryptjs'
export async function loginAction(formData: FormData) {
const email = formData.get('email') as string
const password = formData.get('password') as string
// 1. 사용자 조회
const user = await db.user.findUnique({ where: { email } })
if (!user) {
return { error: '이메일 또는 비밀번호가 올바르지 않습니다.' }
}
// 2. 비밀번호 검증
const isValid = await bcrypt.compare(password, user.passwordHash)
if (!isValid) {
return { error: '이메일 또는 비밀번호가 올바르지 않습니다.' }
}
// 3. JWT 발급
const token = await signToken({
sub: user.id,
email: user.email,
role: user.role,
})
// 4. httpOnly 쿠키에 저장
const cookieStore = await cookies()
cookieStore.set('auth-token', token, {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'lax',
maxAge: 60 * 60 * 24 * 7, // 7일
path: '/',
})
redirect('/dashboard')
}
httpOnly: true는 JavaScript에서 쿠키에 접근할 수 없도록 막아 XSS 공격으로부터 토큰을 보호한다. sameSite: 'lax'는 CSRF 공격을 방어한다.
Middleware에서 JWT 검증
// middleware.ts
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
import { verifyToken } from '@/lib/auth'
export async function middleware(request: NextRequest) {
const token = request.cookies.get('auth-token')?.value
if (!token) {
return NextResponse.redirect(new URL('/login', request.url))
}
try {
// Edge Runtime에서 jose로 JWT 검증
await verifyToken(token)
return NextResponse.next()
} catch {
// 토큰 만료·위조 시 로그아웃 처리
const response = NextResponse.redirect(new URL('/login', request.url))
response.cookies.delete('auth-token')
return response
}
}
export const config = {
matcher: ['/dashboard/:path*', '/admin/:path*'],
}
Server Component에서 세션 읽기
페이지 컴포넌트에서 현재 사용자 정보가 필요하다면 cookies()로 토큰을 읽어 검증한다.
// lib/session.ts
import { cookies } from 'next/headers'
import { verifyToken } from './auth'
export async function getSession() {
const cookieStore = await cookies()
const token = cookieStore.get('auth-token')?.value
if (!token) return null
try {
const { payload } = await verifyToken(token)
return payload as { sub: string; email: string; role: string }
} catch {
return null
}
}
// app/dashboard/page.tsx
import { redirect } from 'next/navigation'
import { getSession } from '@/lib/session'
export default async function DashboardPage() {
const session = await getSession()
if (!session) {
redirect('/login')
}
return (
<div>
<h1>안녕하세요, {session.email}!</h1>
</div>
)
}
getSession()을 Server Component에서 직접 호출하는 방식은 Middleware 검증과 이중으로 보호하는 Defense in Depth 패턴이다.
로그아웃
// app/logout/actions.ts
'use server'
import { cookies } from 'next/headers'
import { redirect } from 'next/navigation'
export async function logoutAction() {
const cookieStore = await cookies()
cookieStore.delete('auth-token')
redirect('/login')
}
Server Action이므로 <form action={logoutAction}> 형태로 폼과 연결하면 된다.
비밀번호 해싱
절대 비밀번호를 평문으로 저장하지 말 것. bcryptjs는 Node.js 환경(Server Action, Route Handler)에서 사용한다.
import bcrypt from 'bcryptjs'
// 회원가입 시 해싱
const hash = await bcrypt.hash(password, 12) // salt rounds 10~12 권장
// 로그인 시 검증
const isValid = await bcrypt.compare(plainPassword, storedHash)
Edge Runtime(Middleware)에서는 bcryptjs를 사용할 수 없다. 비밀번호 검증은 Route Handler나 Server Action에서 수행하고, Middleware에서는 JWT 검증만 담당한다.
환경 변수 관리
# .env.local
JWT_SECRET=최소_32자_이상의_무작위_문자열
JWT 시크릿은 최소 32자 이상, 가능하면 64자 이상의 랜덤 문자열을 사용한다. 운영 환경에서는 시크릿 관리 서비스(AWS Secrets Manager, Vault 등)를 활용한다.
지난 글: Middleware matcher — 실행 범위 제어
다음 글: Auth.js(NextAuth) — 소셜 로그인과 세션 관리
읽어주셔서 감사합니다. 😊