Cookie-Based Authentication#
쿠키 기반 인증은 웹 애플리케이션에서 가장 널리 사용되는 사용자 인증 방식 중 하나이다. 이 방식은 HTTP 프로토콜의 무상태(stateless) 특성을 극복하고 사용자의 로그인 상태를 유지하기 위한 메커니즘으로, 오랜 시간 동안 웹의 핵심 인증 기술로 자리잡아 왔다.
쿠키의 기본 개념#
쿠키(Cookie)는 웹 서버가 사용자의 브라우저에 저장하는 작은 데이터 조각이다. 이 데이터는 브라우저가 서버에 요청을 보낼 때마다 함께 전송되어, 서버가 클라이언트를 식별할 수 있게 해준다. 쿠키는 원래 Lou Montulli가 1994년 Netscape Communications에서 개발했으며, 상태를 유지하지 않는 HTTP 프로토콜에 상태 정보를 추가하기 위한 목적으로 설계되었다.
쿠키 기반 인증의 작동 원리#
쿠키 기반 인증의 기본 흐름은 다음과 같다:
- 사용자 로그인: 사용자가 자신의 자격 증명(일반적으로 사용자 이름과 비밀번호)을 서버에 전송한다.
- 자격 증명 검증: 서버는 제공된 자격 증명을 검증한다. 일반적으로 비밀번호는 데이터베이스에 해시된 형태로 저장되어 있으며, 서버는 입력된 비밀번호를 해시하여 저장된 해시와 비교한다.
- 세션 생성: 인증이 성공하면 서버는 고유한 세션 ID를 생성하고, 이 ID를 사용자 정보와 연결하여 서버의 메모리, 데이터베이스, 또는 캐시(Redis, Memcached 등)에 저장한다.
- 쿠키 설정: 서버는 HTTP 응답 헤더의 ‘Set-Cookie’ 지시자를 사용하여 세션 ID를 클라이언트에게 전송한다. 이 쿠키는 브라우저에 저장된다.
- 후속 요청: 사용자가 추가 요청을 할 때마다 브라우저는 자동으로 쿠키를 요청 헤더에 포함시켜 서버에 전송한다.
- 세션 검증: 서버는 요청에 포함된 세션 ID를 검증하고, 유효한 세션에 해당하는 사용자 정보를 검색하여 해당 요청을 처리한다.
- 로그아웃/세션 종료: 사용자가 로그아웃하거나 세션이 만료되면, 서버는 세션 저장소에서 세션 정보를 제거하고, 클라이언트의 쿠키를 무효화할 수 있다.
쿠키 속성과 옵션#
쿠키 기반 인증을 구현할 때, 다음과 같은 중요한 쿠키 속성들을 고려해야 한다:
필수 속성
- Name=Value: 쿠키의 이름과 값을 지정한다.
- 세션 인증에서는 일반적으로 이름이 ‘sessionid’나 ‘connect.sid’ 등으로, 값은 암호화된 세션 식별자로 설정된다.
보안 관련 속성
- Secure:
- 이 플래그가 설정되면 쿠키는 HTTPS 연결을 통해서만 전송된다.
- 이는 중간자 공격(MITM)으로부터 쿠키를 보호하는 데 중요하다.
- HttpOnly:
- 이 플래그가 설정되면 클라이언트측 JavaScript로는 쿠키에 접근할 수 없게 된다.
- 이는 크로스 사이트 스크립팅(XSS) 공격으로부터 보호하는 데 중요하다.
- SameSite:
- 이 속성은 쿠키가 크로스 사이트 요청과 함께 전송되는 방식을 제어한다.
- ‘Strict’, ‘Lax’, ‘None’ 값 중 하나를 가질 수 있으며, 크로스 사이트 요청 위조(CSRF) 공격을 방지하는 데 도움이 된다.
생명주기 관련 속성
- Expires/Max-Age:
- 쿠키의 수명을 정의한다.
- Expires는 절대적인 만료 날짜를 설정하고, Max-Age는 쿠키가 유효한 초 단위 시간을 지정한다.
- 설정하지 않으면 쿠키는 ‘세션 쿠키’가 되어 브라우저가 닫힐 때 삭제된다.
범위 관련 속성
- Domain:
- 쿠키가 전송될 도메인을 지정한다.
- 설정하지 않으면 쿠키를 설정한 도메인(서브도메인 제외)에서만 유효한다.
- Path:
- 쿠키가 유효한 URL 경로를 지정한다.
- 기본값은 ‘/‘로, 모든 경로에 대해 쿠키가 전송된다.
쿠키 기반 인증의 구현 예시#
서버 측 구현 (Node.js/Express)#
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
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
| const express = require('express');
const session = require('express-session');
const bcrypt = require('bcrypt');
const app = express();
// 세션 미들웨어 설정
app.use(session({
secret: '안전한_랜덤_문자열', // 세션 데이터 서명용 비밀 키
resave: false, // 세션이 변경되지 않아도 저장할지 여부
saveUninitialized: false, // 초기화되지 않은 세션도 저장할지 여부
cookie: {
httpOnly: true, // JavaScript에서 쿠키 접근 방지
secure: process.env.NODE_ENV === 'production', // HTTPS에서만 쿠키 전송
maxAge: 1000 * 60 * 60 * 24, // 쿠키 유효 기간 (1일)
sameSite: 'lax' // CSRF 방지를 위한 SameSite 설정
}
}));
// 사용자 로그인 처리
app.post('/login', async (req, res) => {
const { username, password } = req.body;
// 사용자 정보 조회 (실제로는 데이터베이스에서 조회)
const user = await findUserByUsername(username);
if (!user) {
return res.status(401).json({ error: '사용자를 찾을 수 없습니다' });
}
// 비밀번호 검증
const isPasswordValid = await bcrypt.compare(password, user.passwordHash);
if (!isPasswordValid) {
return res.status(401).json({ error: '비밀번호가 일치하지 않습니다' });
}
// 세션에 사용자 정보 저장 (민감한 정보 제외)
req.session.user = {
id: user.id,
username: user.username,
role: user.role
};
// 로그인 성공 응답
res.json({ success: true, user: req.session.user });
});
// 인증된 사용자만 접근할 수 있는 라우트 보호
function requireAuth(req, res, next) {
if (!req.session.user) {
return res.status(401).json({ error: '인증이 필요합니다' });
}
next();
}
// 보호된 라우트 예시
app.get('/profile', requireAuth, (req, res) => {
res.json({ user: req.session.user });
});
// 로그아웃 처리
app.post('/logout', (req, res) => {
req.session.destroy(err => {
if (err) {
return res.status(500).json({ error: '로그아웃 처리 중 오류가 발생했습니다' });
}
res.clearCookie('connect.sid'); // 세션 쿠키 삭제
res.json({ success: true });
});
});
app.listen(3000, () => {
console.log('서버가 3000번 포트에서 실행 중입니다');
});
|
클라이언트 측 구현 (JavaScript)#
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
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
| // 로그인 요청 함수
async function login(username, password) {
try {
const response = await fetch('/login', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ username, password }),
credentials: 'same-origin' // 쿠키가 요청과 함께 전송되도록 설정
});
const data = await response.json();
if (!response.ok) {
throw new Error(data.error || '로그인에 실패했습니다');
}
// 로그인 성공 처리
console.log('로그인 성공:', data.user);
return data.user;
} catch (error) {
console.error('로그인 오류:', error);
throw error;
}
}
// 보호된 라우트 데이터 요청 함수
async function fetchProfile() {
try {
const response = await fetch('/profile', {
credentials: 'same-origin' // 쿠키가 요청과 함께 전송되도록 설정
});
if (!response.ok) {
if (response.status === 401) {
// 인증되지 않은 상태 처리
window.location.href = '/login-page';
return;
}
throw new Error('프로필 데이터 요청 실패');
}
const data = await response.json();
return data;
} catch (error) {
console.error('프로필 요청 오류:', error);
throw error;
}
}
// 로그아웃 함수
async function logout() {
try {
const response = await fetch('/logout', {
method: 'POST',
credentials: 'same-origin'
});
const data = await response.json();
if (data.success) {
// 로그아웃 후 처리 (예: 로그인 페이지로 리디렉션)
window.location.href = '/login-page';
}
} catch (error) {
console.error('로그아웃 오류:', error);
}
}
|
쿠키 기반 인증의 장점#
- 간편한 구현: 대부분의 웹 프레임워크와 라이브러리는 쿠키 기반 인증을 쉽게 구현할 수 있는 도구를 제공한다.
- 브라우저 호환성: 모든 브라우저가 쿠키를 지원하므로 별도의 클라이언트 측 구현이 거의 필요하지 않다.
- 자동 전송: 쿠키는 브라우저에 의해 자동으로 요청에 포함되어 전송되므로, 개발자가 각 요청마다 인증 정보를 포함시킬 필요가 없다.
- 상태 유지: 무상태 HTTP 프로토콜에서 사용자 상태를 효과적으로 유지할 수 있다.
- 서버 제어: 서버는 언제든지 세션을 무효화하거나 만료시킬 수 있어 보안 관리가 용이하다.
쿠키 기반 인증의 단점과 보안 고려사항#
CSRF(Cross-Site Request Forgery) 취약점:
- 쿠키는 자동으로 요청에 포함되므로, 악의적인 웹사이트가 사용자의 브라우저를 통해 인증된 요청을 보낼 수 있다.
- 이를 방지하기 위해 CSRF 토큰, SameSite 쿠키 설정 등의 추가 보호 조치가 필요하다.
XSS(Cross-Site Scripting) 취약점:
- 클라이언트 측 스크립트가 쿠키에 접근할 수 있으면 세션 하이재킹의 위험이 있다.
- 이를 방지하기 위해 HttpOnly 플래그를 사용해야 한다.
쿠키 크기 제한:
- 쿠키는 일반적으로 4KB 크기 제한이 있어, 대량의 데이터를 저장하기에는 적합하지 않다.
확장성 문제:
- 서버 측에서 세션 상태를 유지해야 하므로, 대규모 분산 시스템에서는 세션 동기화 및 저장 문제가 발생할 수 있다.
모바일 앱 호환성:
- 네이티브 모바일 앱에서는 쿠키 처리가 웹 브라우저와 다를 수 있어 추가 구현이 필요할 수 있다.
보안 강화 방법#
CSRF 공격 방지#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
| // CSRF 토큰 구현 예시 (Express)
const csrf = require('csurf');
const csrfProtection = csrf({ cookie: true });
// CSRF 보호가 필요한 라우트에 미들웨어 적용
app.get('/form', csrfProtection, (req, res) => {
// CSRF 토큰을 폼에 포함
res.render('form', { csrfToken: req.csrfToken() });
});
app.post('/process', csrfProtection, (req, res) => {
// CSRF 토큰이 유효한 경우에만 처리
// 처리 로직…
});
|
안전한 쿠키 설정#
1
2
3
4
5
6
7
8
9
10
11
12
| // 안전한 쿠키 설정 예시
app.use(session({
// … 기타 옵션 …
cookie: {
httpOnly: true, // JavaScript에서 쿠키 접근 방지
secure: true, // HTTPS에서만 쿠키 전송
sameSite: 'strict', // 동일 사이트 요청에서만 쿠키 전송
domain: 'example.com', // 특정 도메인에서만 유효
path: '/', // 모든 경로에서 유효
maxAge: 3600000 // 1시간 후 만료
}
}));
|
SameSite#
- 이 속성은 쿠키가 크로스 사이트 요청과 함께 전송되는 방식을 제어한다.
속성 값 | 동작 방식 | 보안 수준 | 사용 사례 | 장점 및 단점 |
---|
Strict | 동일한 사이트에서 시작된 요청에만 쿠키 전송 | 매우 높음 | 금융 서비스, 민감 데이터 처리 애플리케이션 | ✅ 높은 보안 제공 ❌ 크로스 사이트 기능 제한으로 사용자 경험 저하 가능 |
Lax | 최상위 네비게이션(GET 요청)에는 쿠키 전송, 백그라운드 요청에는 전송되지 않음 | 중간 | 로그인 폼 리디렉션, 일반적인 웹 애플리케이션 | ✅ 보안과 유연성의 균형 ❌ 일부 크로스 사이트 시나리오에서는 제한적 |
None | 모든 요청(동일/크로스 사이트)에 쿠키 전송 | 낮음 (HTTPS 필수) | 광고 추적, SSO, 분석 도구 | ✅ 제3자 컨텍스트 지원 ❌ HTTPS 필요 및 보안 위험 증가 |
세션 하이재킹 방지#
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
31
| // 세션 하이재킹 방지 예시 (Express)
app.use(session({
// … 기타 옵션 …
rolling: true, // 각 요청마다 세션 쿠키 갱신
regenerate: true, // 주기적으로 세션 ID 재생성
saveUninitialized: false // 초기화되지 않은 세션 저장 방지
}));
// 사용자 IP 또는 사용자 에이전트 변경 감지
app.use((req, res, next) => {
const currentIp = req.ip;
const currentUserAgent = req.headers['user-agent'];
if (req.session.user) {
// 첫 로그인 시 IP와 사용자 에이전트 저장
if (!req.session.ip) {
req.session.ip = currentIp;
req.session.userAgent = currentUserAgent;
}
// 이전 세션 정보와 비교
if (req.session.ip !== currentIp || req.session.userAgent !== currentUserAgent) {
console.warn('잠재적 세션 하이재킹 시도 감지');
return req.session.destroy(() => {
res.redirect('/login');
});
}
}
next();
});
|
실제 적용 시나리오#
전통적인 웹 애플리케이션#
전통적인 서버 렌더링 웹 애플리케이션은 쿠키 기반 인증이 매우 적합하다. 사용자가 로그인하면 서버는 세션을 생성하고 세션 ID가 포함된 쿠키를 설정한다. 이후 모든 요청에서 서버는 이 쿠키를 통해 사용자를 식별하고 인증된 사용자에게 적절한 페이지를 렌더링한다.
하이브리드 애플리케이션#
SPA(Single Page Application)와 API 서버가 동일한 도메인에 있는 경우, 쿠키 기반 인증은 여전히 좋은 선택이다. SPA는 API 요청 시 credentials: 'include'
옵션을 설정하여 쿠키를 함께 전송할 수 있다.
분산 시스템#
여러 서버나 마이크로서비스 아키텍처에서는 Redis와 같은 중앙 집중식 세션 저장소를 사용하여 세션 정보를 공유할 수 있다:
1
2
3
4
5
6
7
8
9
10
11
12
| const session = require('express-session');
const RedisStore = require('connect-redis')(session);
const redis = require('redis');
const redisClient = redis.createClient();
app.use(session({
store: new RedisStore({ client: redisClient }),
secret: '안전한_비밀_키',
resave: false,
saveUninitialized: false,
cookie: { /* 보안 설정 */ }
}));
|
최신 트렌드와 모범 사례#
하이브리드 접근법#
많은 현대 웹 애플리케이션은 쿠키와 토큰의 장점을 결합한 하이브리드 접근법을 사용한다. 예를 들어, JWT를 쿠키에 저장하여 자동 전송의 이점을 누리면서도 토큰 기반 인증의 확장성을 확보할 수 있다:
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
31
32
| // JWT를 HTTP-only 쿠키에 저장하는 예시
app.post('/login', (req, res) => {
// 인증 로직…
const token = jwt.sign({ userId: user.id }, 'secret_key', { expiresIn: '1h' });
res.cookie('token', token, {
httpOnly: true,
secure: true,
sameSite: 'strict',
maxAge: 3600000 // 1시간
});
res.json({ success: true });
});
// 미들웨어에서 쿠키로부터 JWT 검증
app.use((req, res, next) => {
const token = req.cookies.token;
if (!token) {
return res.status(401).json({ error: '인증이 필요합니다' });
}
try {
const decoded = jwt.verify(token, 'secret_key');
req.user = decoded;
next();
} catch (err) {
return res.status(401).json({ error: '유효하지 않은 토큰입니다' });
}
});
|
보안 최적화#
최신 보안 모범 사례는 다음과 같은 접근법을 권장한다:
- SameSite=Lax 기본 설정: 최신 브라우저는 쿠키의 기본 SameSite 값을 ‘Lax’로 설정하여 CSRF 공격을 줄인다.
- 브라우저 지문(Fingerprinting): 세션과 함께 브라우저 특성을 저장하여 비정상적인 로그인 시도를 감지한다.
- 점진적인 세션 재검증: 민감한 작업(결제, 비밀번호 변경 등)에 대해 추가 인증을 요구한다.
- Content-Security-Policy(CSP): XSS 공격으로부터 보호하여 세션 쿠키 탈취 가능성을 줄인다.
용어 정리#
참고 및 출처#
Cookie-Based Auth vs. Basic Authentication 쿠키 기반 인증(Cookie-Based Authentication) 작동 원리 쿠키 기반 인증은 HTTP 쿠키를 사용하여 사용자의 인증 상태를 유지하는 방식이다.
일반적인 흐름은 다음과 같다:
사용자가 로그인 폼에 자격 증명(사용자 이름과 비밀번호)을 입력한다. 서버는 자격 증명을 검증하고, 인증에 성공하면 세션 ID를 생성한다. 서버는 이 세션 ID를 쿠키로 클라이언트에게 전송한다 (Set-Cookie 헤더 사용). 브라우저는 해당 도메인에 대한 후속 요청에 이 쿠키를 자동으로 포함시킨다. 서버는 쿠키에 포함된 세션 ID를 검증하여 사용자를 식별한다. 장점 사용자 경험: 사용자가 자격 증명을 한 번만 입력하면 되므로 편리하다. 상태 관리: 서버 측에서 세션 상태를 유지할 수 있어 세밀한 제어가 가능하다. 보안 옵션: HttpOnly, Secure, SameSite 등의 플래그를 통해 보안을 강화할 수 있다. 만료 및 갱신: 세션 타임아웃과 자동 갱신 메커니즘을 구현할 수 있다. 로그아웃: 서버에서 세션을 무효화하여 즉시 로그아웃이 가능하다. 단점 CSRF 취약점: 적절한 보호 조치 없이는 사이트 간 요청 위조(CSRF) 공격에 취약할 수 있다. 확장성 문제: 세션 데이터를 서버에 저장하면 분산 시스템에서 확장성 문제가 발생할 수 있다. 도메인 제한: 쿠키는 기본적으로 단일 도메인에 제한되어 있어 크로스 도메인 요청에 제약이 있다. 모바일 앱 호환성: 일부 모바일 앱 환경에서는 쿠키 관리가 복잡할 수 있다. 기본 인증(Basic Authentication) 작동 원리 기본 인증은 HTTP 프로토콜에 내장된 간단한 인증 메커니즘이다:
...