세션 관리는 사용자의 상태를 여러 HTTP 요청에 걸쳐 유지하는 메커니즘이다. 서버 측 세션(쿠키 + 세션 저장소)과 클라이언트 측 토큰(JWT) 방식이 대표적이다.
세션 vs JWT 비교
| 항목 | 서버 세션 | JWT |
|---|
| 상태 | 서버에 저장 (Stateful) | 클라이언트 (Stateless) |
| 무효화 | 즉시 가능 | 만료 전까지 어려움 |
| 확장성 | Redis 등 공유 저장소 필요 | 서버 간 공유 용이 |
| 크기 | 작음 (ID만 전송) | 클 수 있음 |
| 저장 위치 | HttpOnly 쿠키 | 쿠키 or localStorage |
Express.js 세션 구현
typescript
import express from 'express';
import session from 'express-session';
import RedisStore from 'connect-redis';
import { createClient } from 'redis';
const redisClient = createClient({ url: 'redis://localhost:6379' });
await redisClient.connect();
app.use(session({
store: new RedisStore({ client: redisClient }),
secret: process.env.SESSION_SECRET!,
resave: false,
saveUninitialized: false,
cookie: {
httpOnly: true, // XSS 방지
secure: true, // HTTPS만
sameSite: 'strict', // CSRF 방지
maxAge: 7 * 24 * 60 * 60 * 1000, // 7일
},
}));
// 로그인
app.post('/login', async (req, res) => {
const user = await verifyCredentials(req.body);
if (!user) return res.status(401).json({ error: '인증 실패' });
req.session.userId = user.id;
req.session.role = user.role;
// 세션 고정 공격 방지 (Session Fixation)
req.session.regenerate((err) => {
if (err) return res.status(500).json({ error: '세션 오류' });
req.session.userId = user.id;
res.json({ ok: true });
});
});
// 로그아웃
app.post('/logout', (req, res) => {
req.session.destroy((err) => {
res.clearCookie('connect.sid');
res.json({ ok: true });
});
});
세션 보안 체크리스트
필수 보안 조치:
✓ HttpOnly 쿠키 (JavaScript 접근 차단)
✓ Secure 플래그 (HTTPS 전용)
✓ SameSite=Strict (CSRF 방지)
✓ 적절한 만료 시간
✓ 로그인 시 세션 재생성 (Session Fixation 방지)
✓ 로그아웃 시 서버 세션 삭제
✓ 동시 세션 제한 (선택적)
✓ 비활성 세션 자동 만료
관련 개념