Access Token과 Refresh Token
Access Token 이란?
사용자의 인증이 완료되면, 서버는 해당 사용자를 식별하고 인증하기 위해 Access Token을 발급합니다.
Access Token을 가진 사용자만 서버에 접근할 수 있으며, 이 토큰 안에는 사용자가 서버에 접근할 수 있는 주요 정보가 담겨 있습니다.
따라서 이 토큰이 해커에게 탈취되면 큰 피해로 이어질 수 있습니다.
이러한 보안상의 이유로, Access Token의 유효 시간을 짧게 설정하고
대신 Refresh Token을 함께 발급하여 필요할 때 새로운 Access Token을 발급받는 방식을 주로 사용합니다.
Refresh Token 이란?
사용자의 모든 정보가 담긴 Access Token과는 달리, Refresh 토큰은 특정 사용자가 새로운 Access Token을 발급받기 위해 사용되는 토큰입니다.
Access 토큰은 유효 기간을 짧게 가져가서 자주 갱신되도록 함으로써, 만약 Access 토큰이 탈취되더라도 피해를 최소화할 수 있습니다.
하지만 이렇게 유효 기간을 짧게 가져가면, 사용자가 서비스를 계속 이용하는 동안 새로운 Access 토큰을 발급받아야 하는 상황이 자주 발생합니다.
이때 매번 로그인을 시킬 수는 없기 때문에, 사용자는 Refresh 토큰을 이용하여 별도의 인증 없이 새 Access 토큰을 발급받습니다.
💡 그렇다면 Refresh 토큰이 탈취되면 똑같이 위험하지 않을까?
Refresh 토큰이 탈취되면 공격자는 만료된 Access 토큰을 계속 갱신하여 장기간 서비스에 접근할 수 있게 되므로, 더 큰 위험을 초래할 수 있습니다.
Access 토큰은 금방 만료되지만, Refresh 토큰은 상대적으로 긴 유효 기간을 가지기 때문에 특히 주의가 필요합니다.
🔒 Refresh 토큰 탈취 위험을 줄이기 위한 방법
1. 안전한 저장 방식 사용(httpOnly, Secure, SameSiite 쿠키)
- Refresh 토큰을 클라이언트 측에서 localStorage나 sessionStorage에 저장하면, XSS(교차 사이트 스크립팅) 공격을 통해 자바스크립트로 쉽게 탈취 뒬 수 있습니다.
- 반면 httpOnly 옵션이 적용된 쿠키는 브라우저의 JavaScript로 접근할 수 없으므로 XSS로부터 안전합니다.
- 여기에 Secure(HTTPS에서만 전송)와 SameSite(크로스사이트 요청 제한) 옵션을 추가하면, 네트워크 스니핑이나 CSRF 공격을 통한 탈취 가능성까지 줄일 수 있습니다.
👉 결과적으로, 브라우저에서 토큰을 훔치기 훨씬 어렵게 만들어 위험을 줄입니다.
❓XSS(교차 사이트 스크립팅이란)?
웹 사이트에서 발생하는 보안 취약점 중 하나로, 공격자가 악성 스크립트를 웹 페이지에 삽입하여 다른 사용자의 브라우저에서 실행되도록 하는 공격 방식입니다.
이를 통해 공격자는 사용자의 쿠키를 탈취하거나, 사용자를 속여 악성 코드를 실행하게 만들거나, 웹 페이지를 변조하는 등의 공격을 수행할 수 있습니다.
2. 토큰 회전 적용
Refresh 토큰을 사용할 때마다 새 토큰을 발급하고 이전 토큰은 폐기하면, 설령 어떤 공격자가 특정 시점에 Refresh 토큰을 훔쳤더라도,
- 이미 사용된 토큰이라면 더 이상 쓸 수 없고,
- 새로 발급된 토큰을 또 탈취하지 않는 이상 Access 토큰을 갱신할 수 없습니다.
👉 즉, 탈취된 토큰이 재사용될 가능성을 최소화하고, 공격자가 오래 지속적으로 접근하는 것을 막습니다.
3. Refresh 토큰 만료 및 철저한 검증
- Refresh 토큰이 무기한 유효하다면, 한 번 탈취된 토큰으로 공격자가 오랜 기간 접근할 수 있습니다.
- 하지만 만료 시간을 설정하고 주기적으로 갱신하도록 하면, 토큰을 훔쳐도 일정 시간이 지나면 자동으로 쓸 수 없게 됩니다.
- 또 서버에서 토큰 상태를 DB나 Redis 등에 저장하고 매번 검증하면, 이미 무효화된 토큰이나 회전으로 폐기된 토큰을 사용할 수 없습니다.
👉 결과적으로, 탈취된 토큰의 유효 시간을 단축하고, 무단 사용을 차단합니다.
4. 사용 패턴 모니터링 및 이상 탐지
- Refresh 토큰이 정상적인 사용자가 아닌 공격자에게 넘어갔다면, 사용 위치(IP), 디바이스, 브라우저 정보가 평소와 달라지는 경우가 많습니다.
- 이런 이상 징후를 모니터링하고, 의심스러운 접근 시 추가 인증(MFA)을 요구하거나 즉시 토큰을 폐기하면, 공격자가 Refresh 토큰을 이용해도 추가 인증을 통과하지 못하고 접근이 차단됩니다.
👉 결과적으로, 실제 유효한 토큰이더라도 비정상적인 사용을 탐지하여 피해를 줄입니다.
AccessToken과 Refresh Token API 구현
아래와 같이 app.js 파일에 비밀키를 정의하니다. 하지만 이렇게 하면 비밀키가 노출되기 때문에 .env 파일을 이용해 비밀키를 관리해야 합니다. 그러면 코드 상에 비밀키를 노출 시키지 않습니다.
const ACCESS_TOKEN_SECRET_KEY = `HangHae99`; // Access Token의 비밀 키를 정의합니다.
const REFRESH_TOKEN_SECRET_KEY = `Sparta`; // Refresh Token의 비밀 키를 정의합니다.
그리고 Refresh Token을 저장할 객체를 만들어 줍니다.
let tokenStorage = {}; // Refresh Token을 저장할 객체
❓ Refresh Token의 정보는 어디서 관리해야하나요?
여기서 제시한 예시는 실제 프로덕션 환경에서는 사용해선 안됩니다. 이는 인 메모리 방식을 사용하기 때문에 서버가 재시작 또는 종료될 경우 모든 정보가 사라지게 됩니다.
이러한 문제점으 해결하기 위해, 실제 서비스에서는 별도의 테이블에서 Refresh Token을 저장하고 관리합니다. 이렇게 할 경우, Refresh Token 검증 작업을 MySQL과 같은 데이터베이스를 조회함과 동시에 함께 처리할 수 있게 됩니다.
다음으로 Access 토큰과 Refresh 토큰을 생성해주는 함수를 만들어줍니다. jwt 모듈을 토큰을 이용해 만들 수 있습니다. jwt 모듈에 sign 함수 안에는 만료 기간을 정할 수 있는 expiresIn 객체를 내장하고 있습니다.
// Access Token을 생성하는 함수
function createAccessToken(id) {
const accessToken = jwt.sign(
{ id: id }, // JWT 데이터
ACCESS_TOKEN_SECRET_KEY, // Access Token의 비밀 키
{ expiresIn: '10s' }, // Access Token이 10초 뒤에 만료되도록 설정합니다.
);
return accessToken;
}
// Refresh Token을 생성하는 함수
function createRefreshToken(id) {
const refreshToken = jwt.sign(
{ id: id }, // JWT 데이터
REFRESH_TOKEN_SECRET_KEY, // Refresh Token의 비밀 키
{ expiresIn: '7d' }, // Refresh Token이 7일 뒤에 만료되도록 설정합니다.
);
생성된 Access 토큰과 Refresh 토큰을 할당해줍니다.
const accessToken = createAccessToken(id);
const refreshToken = createRefreshToken(id);
이제 할당은 Refresh 토큰을 키로하여 가지고 유저의 정보를 서버에 저장해줍니다.
tokenStorage[refreshToken] = {
id: id, // 사용자에게 전달받은 ID를 저장합니다.
ip: req.ip, // 사용자의 IP 정보를 저장합니다.
userAgent: req.headers['user-agent'], // 사용자의 User Agent 정보를 저장합니다.
};
그리고 클라이언트에 토큰을 쿠키로 전달합니다.
res.cookie('accessToken', accessToken); // Access Token을 Cookie에 전달한다.
res.cookie('refreshToken', refreshToken); // Refresh Token을 Cookie에 전달한다.
토큰 검증
Access 토큰 검증
아래 코드는 클라이언트가 요청할 때 함께 보낸 쿠키 중에서 accessToken이라는 이름의 값을 꺼내오는 부분입니다.
서버에서 이전에 accessToken을 쿠키로 설정해 두었기 때문에, 이후 요청에서 req.cookies.accessToken을 통해 그 값을 읽어올 수 있습니다.
const accessToken = req.cookies.accessToken;
이후에는 그냥 accessToken이 있는지 if문으로 판별하고, jwt.verify로 토큰을 검증하고 PayLoad(저장한 정보)를 반환하는 코드를 작성해주면 됩니다.
Refresh 토큰 검증
마찬가지로 요청할 때 함께 보낸 쿠키 중에서 refreshToken이라는 이름의 값을 꺼내옵니다.
const refreshToken = req.cookies.refreshToken;
이번에는 refreshToken에 이상이 없다면 위에서 저장한 객체인 tokenStorage에서 저장된 정보를 refreshToken을 키로 하여 정보를 꺼냅니다.
const userInfo = tokenStorage[refreshToken];
그리고 이상이 없으면 새로운 accessToken을 만듭니다.
const newAccessToken = createAccessToken(userInfo.id);
그리고 이 새롭게 만들어진 accessToken을 쿠키로 저장해서 클라이언트에게 전달해줍니다.
res.cookie('accessToken', newAccessToken);
그래서 새롭게 발급 받은 access Token으로 서버에 접속할 수 있습니다.
정리해보면, 앞에서 Access Token을 10초 뒤에 만료되도록 설정해 두었습니다.
따라서 10초가 지나면 더 이상 해당 Access Token으로는 서버에 접근할 수 없습니다.
이때 Refresh Token을 사용해 새로운 Access Token을 발급받고, 발급받은 새로운 Access Token으로 다시 서버에 요청을 보내어 정상적으로 접근할 수 있습니다.
'이노베이션캠프 > TIL' 카테고리의 다른 글
트랜잭션 (2) | 2025.07.26 |
---|---|
winston 라이브러리를 이용해 로그 미들웨어 구현 (3) | 2025.07.25 |
Node.js 숙련 2주차 강의 정리 - 인증과 인가 구현하기 (0) | 2025.07.23 |
Node.js 숙련 1주차 강의 정리 (5) | 2025.07.22 |
Prisma Client 생성 안되는 오류 (0) | 2025.07.21 |