Session ID
를 클라이언트에 발급한다.Session ID
를 쿠키에 저장한다.Session ID
를 서버로 전송한다.Authorization
헤더에 토큰을 포함하여 서버로 전송한다.항목 | 토큰 기반 인증 | 세션 기반 인증 |
---|---|---|
서버 상태 유지 여부 | Stateless (서버 상태 저장 안 함) | Stateful (서버 상태 저장) |
인증 상태 저장 위치 | 클라이언트(로컬/세션 스토리지, 쿠키) | 서버(세션 저장소: 메모리, 데이터베이스 등) |
확장성 | 높음: 서버 확장 용이. HTTP Stateless 활용 가능. | 낮음: 서버 확장이 복잡(Stiky Session, Clustering 등 필요). |
보안 위험 | 클라이언트가 정보 보관. 탈취 시 위험. (XSS, CSRF) | 인증 정보 서버 관리. 탈취 시 무효화 가능. |
로그아웃 처리 | 클라이언트가 토큰 삭제, 즉시 무효화 어려움 | 서버에서 세션 삭제로 즉시 로그아웃 가능 |
성능 | 클라이언트가 인증 데이터 관리. 서버 부담 없음. | 서버가 세션 데이터를 저장·관리. 부담 증가. |
데이터 전송량 | 큰 크기의 토큰 포함 (JWT 등) | 작은 크기의 세션 ID 전송 |
주요 사용 사례 | REST API, SPA, 모바일 앱 등 | 전통적 웹 애플리케이션, 서버 렌더링 기반 사이트 |
JWT는 세 부분으로 구성되며, 각 부분은 점(.
)으로 구분된다. Header.Payload.Signature
typ
)과 서명에 사용된 알고리즘(alg
) 정보를 포함한다.{
"alg": "HS256",
"typ": "JWT"
}
iss
, exp
, sub
등).{
"sub": "1234567890",
"name": "John Doe",
"iat": 1516239022
}
Signature = HMACSHA256(
base64UrlEncode(header) + "." + base64UrlEncode(payload),
secret
)
Authorization: Bearer <JWT>
const BASE_URL = 'https://api.example.com';
async function login(username, password) {
try {
const response = await fetch(`${BASE_URL}/login`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ username, password }),
});
if (!response.ok) {
const errorData = await response.json();
throw new Error(errorData.message || 'Login failed');
}
const data = await response.json();
const token = data.token; // 서버에서 반환한 JWT 토큰
localStorage.setItem('jwtToken', token); // 토큰 저장
console.log('Login successful. Token stored.');
} catch (error) {
console.error('Login error:', error.message);
}
}
async function fetchData(endpoint) {
const token = localStorage.getItem('jwtToken'); // 로컬 스토리지에서 토큰 가져오기
if (!token) {
throw new Error('No token found. Please log in.');
}
try {
const response = await fetch(`${BASE_URL}/${endpoint}`, {
method: 'GET',
headers: {
'Authorization': `Bearer ${token}`, // Authorization 헤더 추가
},
});
if (!response.ok) {
const errorData = await response.json();
throw new Error(errorData.message || 'API call failed');
}
return await response.json();
} catch (error) {
console.error('API error:', error.message);
throw error;
}
}
Bearer는 토큰 기반 인증에서 타입(Scheme)을 나타내는 표준화된 명칭이다. 이를 활용하여 클라이언트와 서버 간의 인증 요청을 일관성 있게 처리할 수 있다. Bearer를 사용하는 이유는 호환성과 표준화를 통해 인증 방식의 통일성을 확보하고, 다양한 환경에서 쉽게 적용할 수 있도록 하기 위함이다.
Bearer는 토큰 기반 인증에서 사용하는 타입(Scheme) 중 하나로, 서버가 요청을 인증할 때 사용하는 방식이다. Bearer Token은 HTTP 요청 헤더의 Authorization
필드에 포함되어 전송된다.
Authorization: <type> <credentials>
<type>
은 Bearer가 되고, <credentials>
는 실제 토큰 값이다.
Bearer Token은 JWT와 OAuth 같은 토큰을 HTTP 요청의 Authorization 헤더에 포함하여 전달하는 표준 인증 방식이다.
서버와 클라이언트 간 통신
Bearer Token은 "소유자(Bearer)가 해당 리소스에 접근할 권한이 있다"고 간주한다.
초기에는 서비스마다 인증 방식이 달라, 타사 서비스나 공개 표준 프로토콜(OAuth 등)과의 호환이 어렵고, 사용자가 각 서비스에서 별도로 로그인해야 하는 불편함이 있었다. 이를 해결하기 위해 IETF는 OAuth 2.0과 함께 Bearer Token을 정의한 RFC 6750을 발표하였다. 이로써 Bearer Token은 JWT 또는 OAuth 토큰과 함께 사용하기에 적합한 인증 방식으로 자리잡았으며, 서비스 간의 일관된 인증 방식을 제공하기 위한 표준으로 널리 사용되고 있다.
IETF(Internet Engineering Task Force): 인터넷 표준을 개발하고 유지 관리하는 국제 조직
RFC(Request for Comments): 인터넷과 관련된 기술, 프로토콜, 시스템 등을 문서화한 공개 표준 또는 제안서
JWT를 단일 인증 수단으로 사용할 경우 발생할 수 있는 몇 가지 한계점이 있다.
이러한 문제를 해결하기 위해 OAuth 2.0에서 Access Token과 Refresh Token 개념이 도입되었으며, JWT는 이 토큰을 구현하는 데 널리 사용된다. 즉, JWT가 Access/Refresh Token을 구현하는 포맷 중 하나로 자주 활용되는 것이다.
Access Token과 Refresh Token은 사용자 인증 및 리소스 접근 관리에서 중요한 역할을 한다. 하지만 이 두 토큰이 유출될 경우 각각 고유의 보안 문제를 발생시킬 수 있다. 다음은 각 토큰의 유출 상황과 대응 방안이다.
토큰 저장 방식은 보안의 핵심이다. 특히 브라우저 환경에서는 XSS(Cross-Site Scripting)나 CSRF(Cross-Site Request Forgery) 공격에 대비하여 설계해야 한다.
Strict
또는 Lax
로 설정하여 의도치 않은 요청을 차단한다.async function login(username, password) {
const response = await fetch('https://api.example.com/auth/login', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ username, password }),
});
if (!response.ok) {
throw new Error('Login failed');
}
const data = await response.json();
const { accessToken, refreshToken } = data;
// Access Token은 메모리 또는 로컬 스토리지에 저장
localStorage.setItem('accessToken', accessToken);
// Refresh Token은 HttpOnly 쿠키로 전송되거나 로컬 스토리지에 저장
localStorage.setItem('refreshToken', refreshToken);
console.log('Login successful. Tokens stored.');
}
Authorization
헤더에 포함하여 서버에 요청을 전송한다.async function fetchData(endpoint) {
const accessToken = localStorage.getItem('accessToken');
if (!accessToken) {
throw new Error('No Access Token found. Please log in.');
}
const response = await fetch(`https://api.example.com/${endpoint}`, {
method: 'GET',
headers: {
'Authorization': `Bearer ${accessToken}`,
},
});
if (response.status === 401) {
// Access Token 만료 시 처리
const refreshed = await refreshAccessToken();
if (refreshed) {
return fetchData(endpoint); // 토큰 갱신 후 재요청
} else {
throw new Error('Session expired. Please log in again.');
}
}
if (!response.ok) {
throw new Error('API call failed');
}
return await response.json();
}
async function refreshAccessToken() {
const refreshToken = localStorage.getItem('refreshToken');
if (!refreshToken) {
return false; // Refresh Token 없음
}
const response = await fetch('https://api.example.com/auth/refresh', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ refreshToken }),
});
if (!response.ok) {
return false; // 토큰 갱신 실패
}
const data = await response.json();
const { accessToken } = data;
// 새로운 Access Token 저장
localStorage.setItem('accessToken', accessToken);
return true; // 토큰 갱신 성공
}
정리글 잘 읽었습니다!