728x90
반응형
1. 세션이란
세션은 서버에서 사용자의 상태를 유지하기 위한 메커니즘입니다. HTTP는 무상태(stateless) 프로토콜이므로, 세션을 통해 로그인 상태, 장바구니 등의 정보를 유지합니다.
1.1 세션 vs 토큰
세션 (Stateful):
- 서버에 상태 저장
- 세션 ID만 클라이언트에 저장 (쿠키)
- 서버 메모리/DB 필요
- 확장 시 세션 공유 필요
토큰 (Stateless):
- 클라이언트에 상태 저장
- JWT 등 자체 포함 토큰
- 서버 저장소 불필요
- 토큰 취소 어려움
2. express-session 기본 사용
npm install express-session
2.1 기본 설정
const express = require('express');
const session = require('express-session');
const app = express();
app.use(session({
secret: 'your-secret-key', // 세션 암호화 키
resave: false, // 변경 없어도 저장할지
saveUninitialized: false, // 초기화 안된 세션 저장할지
cookie: {
secure: false, // HTTPS에서만 쿠키 전송
httpOnly: true, // JavaScript 접근 금지
maxAge: 1000 * 60 * 60 * 24 // 1일
}
}));
// 세션 사용
app.get('/set', (req, res) => {
req.session.username = 'hong';
req.session.visits = (req.session.visits || 0) + 1;
res.json({ message: 'Session set', visits: req.session.visits });
});
app.get('/get', (req, res) => {
res.json({
username: req.session.username,
visits: req.session.visits
});
});
app.get('/destroy', (req, res) => {
req.session.destroy((err) => {
if (err) {
return res.status(500).json({ error: 'Failed to destroy session' });
}
res.clearCookie('connect.sid');
res.json({ message: 'Session destroyed' });
});
});
app.listen(3000);
2.2 세션 옵션
app.use(session({
secret: 'my-secret',
name: 'sessionId', // 쿠키 이름 (기본: connect.sid)
resave: false,
saveUninitialized: false,
rolling: true, // 요청마다 만료 시간 갱신
cookie: {
path: '/', // 쿠키 경로
httpOnly: true, // JS 접근 금지
secure: process.env.NODE_ENV === 'production',
sameSite: 'strict', // CSRF 방지
maxAge: 24 * 60 * 60 * 1000, // 1일
domain: 'example.com' // 쿠키 도메인
}
}));
3. 세션 스토어
3.1 메모리 스토어 (기본)
개발용으로만 사용합니다. 프로덕션에서는 사용하지 않습니다.
// 기본 설정은 메모리 스토어 사용
app.use(session({
secret: 'secret',
resave: false,
saveUninitialized: false
}));
3.2 Redis 스토어
npm install connect-redis redis
const session = require('express-session');
const RedisStore = require('connect-redis').default;
const { createClient } = require('redis');
const redisClient = createClient({
url: 'redis://localhost:6379'
});
redisClient.connect().catch(console.error);
app.use(session({
store: new RedisStore({
client: redisClient,
prefix: 'sess:'
}),
secret: 'your-secret',
resave: false,
saveUninitialized: false,
cookie: {
secure: false,
maxAge: 1000 * 60 * 60 * 24 // 1일
}
}));
3.3 MongoDB 스토어
npm install connect-mongo
const session = require('express-session');
const MongoStore = require('connect-mongo');
app.use(session({
store: MongoStore.create({
mongoUrl: 'mongodb://localhost:27017/sessions',
ttl: 24 * 60 * 60, // 1일 (초 단위)
autoRemove: 'native',
crypto: {
secret: 'encryption-secret'
}
}),
secret: 'your-secret',
resave: false,
saveUninitialized: false
}));
3.4 MySQL/PostgreSQL 스토어
npm install express-mysql-session
# 또는
npm install connect-pg-simple
// MySQL
const session = require('express-session');
const MySQLStore = require('express-mysql-session')(session);
const sessionStore = new MySQLStore({
host: 'localhost',
port: 3306,
user: 'root',
password: 'password',
database: 'sessions'
});
app.use(session({
store: sessionStore,
secret: 'your-secret',
resave: false,
saveUninitialized: false
}));
반응형
4. 인증 시스템 구현
const express = require('express');
const session = require('express-session');
const bcrypt = require('bcrypt');
const app = express();
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
app.use(session({
secret: 'your-secret-key',
resave: false,
saveUninitialized: false,
cookie: {
maxAge: 1000 * 60 * 60 * 24 // 1일
}
}));
// 사용자 저장소
const users = new Map();
// 회원가입
app.post('/register', async (req, res) => {
const { email, password, name } = req.body;
if (users.has(email)) {
return res.status(400).json({ error: 'Email already exists' });
}
const hashedPassword = await bcrypt.hash(password, 10);
const user = {
id: Date.now().toString(),
email,
password: hashedPassword,
name
};
users.set(email, user);
res.status(201).json({ message: 'User registered' });
});
// 로그인
app.post('/login', async (req, res) => {
const { email, password } = req.body;
const user = users.get(email);
if (!user) {
return res.status(401).json({ error: 'Invalid credentials' });
}
const isValid = await bcrypt.compare(password, user.password);
if (!isValid) {
return res.status(401).json({ error: 'Invalid credentials' });
}
// 세션에 사용자 정보 저장
req.session.userId = user.id;
req.session.email = user.email;
req.session.name = user.name;
// 세션 재생성 (세션 고정 공격 방지)
req.session.regenerate((err) => {
if (err) {
return res.status(500).json({ error: 'Session error' });
}
req.session.userId = user.id;
req.session.email = user.email;
req.session.name = user.name;
res.json({
message: 'Login successful',
user: { id: user.id, email: user.email, name: user.name }
});
});
});
// 인증 미들웨어
function isAuthenticated(req, res, next) {
if (req.session.userId) {
return next();
}
res.status(401).json({ error: 'Not authenticated' });
}
// 프로필
app.get('/profile', isAuthenticated, (req, res) => {
res.json({
user: {
id: req.session.userId,
email: req.session.email,
name: req.session.name
}
});
});
// 로그아웃
app.post('/logout', (req, res) => {
req.session.destroy((err) => {
if (err) {
return res.status(500).json({ error: 'Logout failed' });
}
res.clearCookie('connect.sid');
res.json({ message: 'Logged out' });
});
});
app.listen(3000);
5. 세션 보안
5.1 보안 설정
app.use(session({
secret: process.env.SESSION_SECRET, // 환경 변수 사용
name: '__Host-session', // 보안 쿠키 프리픽스
resave: false,
saveUninitialized: false,
cookie: {
secure: true, // HTTPS 필수
httpOnly: true, // XSS 방지
sameSite: 'strict', // CSRF 방지
maxAge: 15 * 60 * 1000, // 15분 (짧게 유지)
path: '/',
domain: 'example.com'
}
}));
// 프록시 뒤에서 실행 시
app.set('trust proxy', 1);
5.2 세션 재생성
// 로그인 시 세션 재생성 (세션 고정 공격 방지)
app.post('/login', async (req, res) => {
// 인증 로직...
const oldSession = req.session;
req.session.regenerate((err) => {
if (err) {
return res.status(500).json({ error: 'Session error' });
}
// 필요한 데이터만 새 세션에 복사
req.session.userId = user.id;
res.json({ message: 'Login successful' });
});
});
5.3 세션 타임아웃
const SESSION_TIMEOUT = 15 * 60 * 1000; // 15분
// 활동 추적 미들웨어
app.use((req, res, next) => {
if (req.session.userId) {
const now = Date.now();
const lastActivity = req.session.lastActivity || now;
if (now - lastActivity > SESSION_TIMEOUT) {
// 세션 만료
return req.session.destroy((err) => {
res.status(401).json({ error: 'Session expired' });
});
}
// 활동 시간 갱신
req.session.lastActivity = now;
}
next();
});
6. 플래시 메시지
npm install connect-flash
const flash = require('connect-flash');
app.use(session({
secret: 'secret',
resave: false,
saveUninitialized: false
}));
app.use(flash());
app.post('/login', (req, res) => {
// 로그인 실패
if (!user) {
req.flash('error', 'Invalid credentials');
return res.redirect('/login');
}
req.flash('success', 'Welcome back!');
res.redirect('/dashboard');
});
app.get('/login', (req, res) => {
res.render('login', {
error: req.flash('error'),
success: req.flash('success')
});
});
7. 다중 서버 환경
7.1 Sticky Session (로드밸런서)
# Nginx 설정
upstream app {
ip_hash; # 같은 IP는 같은 서버로
server 127.0.0.1:3001;
server 127.0.0.1:3002;
server 127.0.0.1:3003;
}
server {
listen 80;
location / {
proxy_pass http://app;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host $host;
proxy_cache_bypass $http_upgrade;
}
}
7.2 공유 세션 스토어 (Redis)
// 모든 서버가 같은 Redis를 사용
const RedisStore = require('connect-redis').default;
const { createClient } = require('redis');
const redisClient = createClient({
url: 'redis://redis-server:6379'
});
redisClient.connect();
app.use(session({
store: new RedisStore({ client: redisClient }),
secret: 'shared-secret',
resave: false,
saveUninitialized: false
}));
8. 세션 관리 API
// 현재 세션 정보
app.get('/session/info', isAuthenticated, (req, res) => {
res.json({
id: req.sessionID,
cookie: req.session.cookie,
data: {
userId: req.session.userId,
createdAt: req.session.createdAt
}
});
});
// 세션 갱신
app.post('/session/refresh', isAuthenticated, (req, res) => {
req.session.touch(); // 만료 시간 갱신
res.json({ message: 'Session refreshed' });
});
// 모든 세션 종료 (Redis 사용 시)
app.post('/session/logout-all', isAuthenticated, async (req, res) => {
const userId = req.session.userId;
// 현재 세션 삭제
req.session.destroy();
// Redis에서 해당 사용자의 모든 세션 삭제
const keys = await redisClient.keys('sess:*');
for (const key of keys) {
const session = await redisClient.get(key);
const parsed = JSON.parse(session);
if (parsed.userId === userId) {
await redisClient.del(key);
}
}
res.json({ message: 'Logged out from all devices' });
});
9. Passport.js와 함께 사용
const passport = require('passport');
const LocalStrategy = require('passport-local').Strategy;
// Passport 초기화
app.use(passport.initialize());
app.use(passport.session());
// 사용자 직렬화
passport.serializeUser((user, done) => {
done(null, user.id);
});
passport.deserializeUser((id, done) => {
const user = findUserById(id);
done(null, user);
});
// 로컬 전략
passport.use(new LocalStrategy(
{ usernameField: 'email' },
async (email, password, done) => {
const user = users.get(email);
if (!user) {
return done(null, false, { message: 'User not found' });
}
const isValid = await bcrypt.compare(password, user.password);
if (!isValid) {
return done(null, false, { message: 'Invalid password' });
}
return done(null, user);
}
));
// 로그인
app.post('/login',
passport.authenticate('local'),
(req, res) => {
res.json({ message: 'Login successful', user: req.user });
}
);
// 보호된 라우트
app.get('/profile', (req, res) => {
if (!req.isAuthenticated()) {
return res.status(401).json({ error: 'Not authenticated' });
}
res.json({ user: req.user });
});
// 로그아웃
app.post('/logout', (req, res) => {
req.logout((err) => {
if (err) {
return res.status(500).json({ error: 'Logout failed' });
}
res.json({ message: 'Logged out' });
});
});
10. 세션 vs JWT 선택 기준
세션 사용:
- 전통적인 웹 애플리케이션
- 즉시 로그아웃/권한 취소 필요
- 서버 측 상태 관리 필요
- 소규모 사용자 기반
JWT 사용:
- SPA, 모바일 앱
- 마이크로서비스 아키텍처
- 상태 비저장 서버 필요
- 대규모 분산 시스템
결론
세션은 서버에서 사용자 상태를 관리하는 전통적인 방법입니다. express-session으로 기본 세션을 구현하고, Redis나 MongoDB 스토어를 사용하여 프로덕션 환경을 지원합니다. 보안을 위해 secure, httpOnly, sameSite 쿠키 옵션을 설정하고, 세션 재생성으로 세션 고정 공격을 방지합니다. 다중 서버 환경에서는 공유 세션 스토어를 사용합니다.
728x90
반응형
'Node.js' 카테고리의 다른 글
| Node.js의 OAuth2.0 구현 (1) | 2026.03.15 |
|---|---|
| Node.js의 JSON 웹 토큰(JWT) 사용법 (0) | 2026.03.14 |
| Node.js의 Socket.IO 사용법 (0) | 2026.03.14 |
| Node.js의 인증 및 인가(Authentication and Authorization) (0) | 2026.03.13 |
| Node.js의 웹소켓(WebSocket) 사용법 (0) | 2026.03.13 |