티스토리 뷰
SvelteKit으로 시작하는 SSR 백엔드-프론트 기초 100단계
11단계 — 세션을 DB로 관리하기 (쿠키 하드코딩 탈출, “서비스 인증”으로 업그레이드)
10단계까지는 “흐름 이해”를 위해 role을 쿠키에 넣는 식으로 단순화했죠.
근데 이 방식은 실제 서비스에서는 위험해요. (쿠키 변조/신뢰 문제)
그래서 이번 단계에서 드디어 이렇게 바꿉니다.
- ✅ 쿠키에는 session_id(랜덤 토큰) 만 저장
- ✅ 서버는 DB에서 세션 조회 → 사용자 조회 → locals.user 세팅
- ✅ 로그아웃하면 DB 세션 삭제
- ✅ role도 DB에서 가져옴 (이제 쿠키 role 삭제)
이 단계 끝나면 “아, 이게 진짜 로그인이지…” 하는 느낌이 옵니다.
11단계 목표
- MySQL + Prisma에 User, Session 테이블 추가
- 로그인 시 Session row 생성 + 쿠키에 session_id 저장
- hooks.server.ts에서 session_id로 세션/유저 조회 후 locals.user 세팅
- 로그아웃 시 세션 삭제 + 쿠키 삭제
- 기존의 requireLogin, requireRole 그대로 동작하게 만들기
0) 전제 조건 (이 글이 그대로 실행되는 기준)
- 5단계에서 MySQL + Prisma 연결 완료
- DATABASE_URL="mysql://app:app@localhost:3306/sveltekit"
- src/lib/server/prisma.ts 존재
1) Prisma 스키마 업데이트 (User / Session 추가)
prisma/schema.prisma를 아래처럼 업데이트하세요.
(기존 Post는 유지하고, 아래 두 모델을 추가합니다.)
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "mysql"
url = env("DATABASE_URL")
}
model User {
id Int @id @default(autoincrement())
username String @unique
password String // 지금은 학습용으로 평문. 다음 단계에서 bcrypt로 바꿉니다.
role Role @default(USER)
sessions Session[]
createdAt DateTime @default(now())
}
model Session {
id String @id // 랜덤 토큰을 그대로 PK로 씁니다 (session_id)
userId Int
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
expiresAt DateTime
createdAt DateTime @default(now())
@@index([userId])
@@index([expiresAt])
}
model Post {
id Int @id @default(autoincrement())
title String
createdAt DateTime @default(now())
}
enum Role {
USER
ADMIN
}
마이그레이션 실행:
npx prisma migrate dev --name add_user_session
2) 유저 시드(초기 데이터) 만들기
일단 로그인 가능한 계정이 있어야 하니까, 간단 시드 스크립트를 하나 만들게요.
2-1) prisma/seed.ts 생성
prisma/seed.ts
import { PrismaClient, Role } from '@prisma/client';
const prisma = new PrismaClient();
async function main() {
// 중복 생성 방지: username 기준 upsert
await prisma.user.upsert({
where: { username: 'admin' },
update: { password: '1234', role: Role.ADMIN },
create: { username: 'admin', password: '1234', role: Role.ADMIN }
});
await prisma.user.upsert({
where: { username: 'user' },
update: { password: '1234', role: Role.USER },
create: { username: 'user', password: '1234', role: Role.USER }
});
console.log('✅ Seed done: admin/1234, user/1234');
}
main()
.catch((e) => {
console.error(e);
process.exit(1);
})
.finally(async () => {
await prisma.$disconnect();
});
2-2) package.json에 seed 설정 추가
package.json에 아래를 추가하세요.
{
"prisma": {
"seed": "ts-node prisma/seed.ts"
}
}
그리고 시드 실행:
npm install -D ts-node
npx prisma db seed
이제 DB에 admin/user 계정이 들어갔습니다.
3) 세션 저장소 만들기 (server 전용)
세션 생성/조회/삭제 로직을 서버에 모아두면 진짜 편합니다.
src/lib/server/session.repo.ts 생성:
import { prisma } from '$lib/server/prisma';
import crypto from 'crypto';
const SESSION_DAYS = 7;
export function createSessionId(): string {
// URL-safe 토큰
return crypto.randomBytes(32).toString('hex');
}
export function getSessionExpiry(): Date {
const expires = new Date();
expires.setDate(expires.getDate() + SESSION_DAYS);
return expires;
}
export async function createSession(userId: number) {
const id = createSessionId();
const expiresAt = getSessionExpiry();
await prisma.session.create({
data: { id, userId, expiresAt }
});
return { id, expiresAt };
}
export async function findValidSession(sessionId: string) {
const now = new Date();
return prisma.session.findFirst({
where: {
id: sessionId,
expiresAt: { gt: now }
},
include: {
user: true
}
});
}
export async function deleteSession(sessionId: string) {
await prisma.session.delete({ where: { id: sessionId } }).catch(() => {
// 이미 없으면 무시
});
}
// (선택) 만료 세션 청소: 나중에 크론/배치로 돌리면 좋아요
export async function deleteExpiredSessions() {
const now = new Date();
await prisma.session.deleteMany({
where: { expiresAt: { lte: now } }
});
}
4) hooks에서 “쿠키 session_id → DB 조회 → locals.user 세팅”으로 변경
이제 10단계에서 쓰던 user_id, user_role 쿠키는 버립니다.
session_id만 씁니다.
src/hooks.server.ts 수정:
import type { Handle } from '@sveltejs/kit';
import { findValidSession } from '$lib/server/session.repo';
export const handle: Handle = async ({ event, resolve }) => {
const sessionId = event.cookies.get('session_id');
if (!sessionId) {
event.locals.user = null;
return resolve(event);
}
const session = await findValidSession(sessionId);
if (!session) {
// 세션이 없거나 만료면 쿠키도 정리
event.cookies.delete('session_id', { path: '/' });
event.locals.user = null;
return resolve(event);
}
// ✅ 이제 role은 DB에서 온다
event.locals.user = {
id: String(session.user.id),
role: session.user.role // 'ADMIN' | 'USER'
};
return resolve(event);
};
5) locals 타입 업데이트 (role은 그대로, id는 문자열로 유지)
src/app.d.ts (이미 10단계에서 만들었으면 그대로 유지해도 OK)
declare namespace App {
interface Locals {
user: {
id: string;
role: 'ADMIN' | 'USER';
} | null;
}
}
6) 로그인 로직을 “DB 유저 조회 + 세션 생성”으로 변경
src/routes/login/+page.server.ts를 DB 기반으로 바꿉니다.
import type { Actions, PageServerLoad } from './$types';
import { fail, redirect } from '@sveltejs/kit';
import { prisma } from '$lib/server/prisma';
import { createSession } from '$lib/server/session.repo';
export const load: PageServerLoad = async ({ locals }) => {
if (locals.user) throw redirect(302, '/');
return {};
};
export const actions: Actions = {
default: async ({ request, cookies, url }) => {
const data = await request.formData();
const username = String(data.get('username') ?? '').trim();
const password = String(data.get('password') ?? '').trim();
if (!username || !password) {
return fail(400, { error: '아이디/비밀번호를 입력해주세요.' });
}
const user = await prisma.user.findUnique({ where: { username } });
// ⚠️ 학습용: 지금은 평문 비교 (다음 단계에서 bcrypt로 교체)
if (!user || user.password !== password) {
return fail(400, { error: '아이디 또는 비밀번호가 틀렸습니다.' });
}
const session = await createSession(user.id);
cookies.set('session_id', session.id, {
path: '/',
httpOnly: true,
sameSite: 'lax',
secure: false // 로컬 개발이라 false, 배포시 true 권장(HTTPS)
// maxAge를 주고 싶으면 expiresAt 기반으로 계산해서 넣어도 됩니다
});
// (선택) 원래 가려던 곳으로 보내기
const redirectTo = url.searchParams.get('redirect') ?? '/';
throw redirect(302, redirectTo);
}
};
7) 로그아웃을 “세션 삭제 + 쿠키 삭제”로 변경
src/routes/logout/+page.server.ts
import type { Actions } from './$types';
import { redirect } from '@sveltejs/kit';
import { deleteSession } from '$lib/server/session.repo';
export const actions: Actions = {
default: async ({ cookies }) => {
const sessionId = cookies.get('session_id');
if (sessionId) {
await deleteSession(sessionId);
}
cookies.delete('session_id', { path: '/' });
throw redirect(302, '/login');
}
};
레이아웃에서 로그아웃 버튼이 POST로 /logout을 때리는 구조(8단계) 그대로면, 그대로 작동합니다.
8) 기존 권한 가드(requireRole)는 그대로 쓰면 된다
src/lib/server/auth.ts (10단계에서 만든 거 그대로 OK)
import { redirect, error } from '@sveltejs/kit';
import type { RequestEvent } from '@sveltejs/kit';
export function requireLogin(event: RequestEvent) {
if (!event.locals.user) throw redirect(302, '/login');
return event.locals.user;
}
export function requireRole(event: RequestEvent, role: 'ADMIN' | 'USER') {
const user = requireLogin(event);
if (user.role !== role) throw error(403, '권한이 없습니다.');
return user;
}
/admin 페이지도 수정할 필요 없습니다.
왜냐면 이제 locals.user.role이 DB에서 오니까요.
9) 동작 확인 체크리스트
- 마이그레이션 & 시드
npx prisma migrate dev --name add_user_session
npx prisma db seed
- 개발 서버 실행
npm run dev
- 로그인 테스트
- /login
- admin / 1234 → /admin 접근 가능
- user / 1234 → /admin 접근하면 403
- 브라우저 쿠키 확인
- Application → Cookies
- 이제 session_id 하나만 있어야 정상
- 로그아웃
- session_id 사라지고 /login으로 이동
오늘 단계에서 주니어가 꼭 가져가야 할 감각 (진짜 중요한 한 문장)
쿠키는 “증거”가 아니라 “열쇠”만 들고 있고,
서버(DB)가 진짜 신원을 판단한다.
이게 서비스 인증의 기본기예요.
SvelteKit, SSR, 세션인증, Prisma, MySQL, hooksServer, locals, RBAC, 주니어개발자, 백엔드기초
'study > 백엔드' 카테고리의 다른 글
| SvelteKit으로 시작하는 SSR 백엔드-프론트 기초 100단계 (0) | 2026.03.10 |
|---|---|
| SvelteKit으로 시작하는 SSR 백엔드-프론트 기초 100단계 (0) | 2026.03.05 |
| SvelteKit으로 시작하는 SSR 백엔드-프론트 기초 100단계 (0) | 2026.02.25 |
| SvelteKit으로 시작하는 SSR 백엔드-프론트 기초 100단계 (0) | 2026.02.09 |
| SvelteKit으로 시작하는 SSR 백엔드-프론트 기초 100단계 (0) | 2026.01.29 |
- Total
- Today
- Yesterday
- Python
- 딥러닝
- LangChain
- 압박면접
- CI/CD
- Express
- JWT
- flax
- NestJS
- rag
- PostgreSQL
- JAX
- 백엔드개발
- REACT
- Next.js
- DevOps
- kotlin
- 쿠버네티스
- fastapi
- Prisma
- SEO최적화
- llm
- 개발블로그
- seo 최적화 10개
- 웹개발
- Redis
- node.js
- Docker
- ai철학
- nextJS
| 일 | 월 | 화 | 수 | 목 | 금 | 토 |
|---|---|---|---|---|---|---|
| 1 | 2 | 3 | 4 | |||
| 5 | 6 | 7 | 8 | 9 | 10 | 11 |
| 12 | 13 | 14 | 15 | 16 | 17 | 18 |
| 19 | 20 | 21 | 22 | 23 | 24 | 25 |
| 26 | 27 | 28 | 29 | 30 |

