2024. 1. 29. 21:47ㆍ카테고리 없음
목차
1. 학습 내용
1. Access Token과 Refresh Token의 개념을 학습한다.
2. Access Token과 Refresh Token을 실제로 구현한다.
2. 내용 정리
1) Access Toekn
사용자의 인증(ex:로그인)이 끝난 후, 해당 사용자를 인증하는 용도로 발급하는 토큰
=> 쿠키에 JWT를 설정하고, 지정된 만료시간이 지나면 인증이 만료되는 구조 또한 Access Token이라 부를 수 있다.
인증요청시 Access Token을 사용하면, 토큰 생성시 사용한 Secret Key로 인증을 처리하게 되는데, 이는 따로 설계나 처리할 필요 없이 코드를 구현할 수 있다는 장점을 가진다.
Access Token은 상태의존성이 없다. 즉, 서버가 다시 시작되더라도 동일하게 작동하게 된다. 사용자 인증 여부 확인은 가능하지만, 토큰을 발급받은 사용자가 지금의 사용자인지 확인할 수는 없다.
이는 토큰을 가지고 있던 시간이 길수록 탈취되었을 때 피해의 규모가 커짐을 의미하며, 서버는 이 토큰이 탈취된 토큰인지 알 수 없으므로 강제로 만료시킬 수도 없다.
따라서... 서버는 언제나 토큰이 탈취될 수 있다는 가정 하에, 피해를 최소화할 수 있는 방향으로 개발되어야 한다.
2) Refresh Token
사용자의 모든 인증 정보를 담고 있는 Access Token과 달리, Access Token을 발급 받기 위한 목적으로만 사용됨
=> 인증 정보를 검증하는데 사용되며, 서버에서 관리된다!
서버는 Refresh Token을 디코딩하여 사용자 정보를 확인하게 되며, 필요한 경우 서버에서 강제로 토큰을 만료시킬 수 있으며 사용자 인증 상태를 언제든지 서버에서 제어할 수 있다는 장점을 가지고 있다.
// 직접 Access Token을 발급하지 않고, Refresh Token을 통해 발급하는 이유
토큰이 탈취당한 경우 피해를 최소화 하기 위함.
Access Token은 짧은시간 사용됨.(피해 최소화)
Access Token을 발급하기 위한 Refresh Token의 만료 시간은 길다.
3. 예제
토큰 발급 API
/** Refresh Token과 Access Token을 발급하는 API **/
let tokenStorage = {}; // Refresh Token을 저장할 객체
/** Access Token, Refresh Token 발급 API **/
app.post('/tokens', (req, res) => {
const { id } = req.body;
const accessToken = createAccessToken(id);
const refreshToken = createRefreshToken(id);
// Refresh Token을 가지고 해당 유저의 정보를 서버에 저장합니다.
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에 전달한다.
return res
.status(200)
.json({ message: 'Token이 정상적으로 발급되었습니다.' });
});
// 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일 뒤에 만료되도록 설정합니다.
);
return refreshToken;
}
createAccessToken 함수는 Access Token(10초 뒤 만료)을,
createRefreshToken 함수는 Refresh Token(7일 뒤 만료)을 생성한다.
위의 상황에선 tokenSotrage라는 변수에 Refresh Token을 저장했지만, 실제 서비스에선 서버가 재시작/종료 될때에도 토큰이 보존되도록 별도의 테이블에서 저장하고 관리한다.
이 덕분에 DB조회와 검증작업이 동시에 함께 처리될 수 있게 된다.
/** 엑세스 토큰 검증 API **/
app.get('/tokens/validate', (req, res) => {
const accessToken = req.cookies.accessToken;
if (!accessToken) {
return res
.status(400)
.json({ errorMessage: 'Access Token이 존재하지 않습니다.' });
}
const payload = validateToken(accessToken, ACCESS_TOKEN_SECRET_KEY);
if (!payload) {
return res
.status(401)
.json({ errorMessage: 'Access Token이 유효하지 않습니다.' });
}
const { id } = payload;
return res.json({
message: `${id}의 Payload를 가진 Token이 성공적으로 인증되었습니다.`,
});
});
// Token을 검증하고 Payload를 반환합니다.
function validateToken(token, secretKey) {
try {
const payload = jwt.verify(token, secretKey);
return payload;
} catch (error) {
return null;
}
}
validationToken함수는 제공된 토큰이 유효한지 검증한다.
secretKey를 전달받아, 서버에서 검증할 비밀 키를 설정한다.
토큰의 만료 여부를 확인하고(~~token이 유효하지 않습니다!)
토큰이 제대로 발급된건지 검증한다.
1. Cookie를 전달할 때, Access Token이 없는 경우 - Access Token이 존재하지 않습는다.
2. Access Token이 유효하지 않을 경우 - Access Token이 유효하지 않습니다.
/** 리프레시 토큰 검증 API **/
app.post('/tokens/refresh', (req, res) => {
const refreshToken = req.cookies.refreshToken;
if (!refreshToken)
return res
.status(400)
.json({ errorMessage: 'Refresh Token이 존재하지 않습니다.' });
const payload = validateToken(refreshToken, REFRESH_TOKEN_SECRET_KEY);
if (!payload) {
return res
.status(401)
.json({ errorMessage: 'Refresh Token이 유효하지 않습니다.' });
}
const userInfo = tokenStorage[refreshToken];
if (!userInfo)
return res.status(419).json({
errorMessage: 'Refresh Token의 정보가 서버에 존재하지 않습니다.',
});
const newAccessToken = createAccessToken(userInfo.id);
res.cookie('accessToken', newAccessToken);
return res.json({ message: 'Access Token을 새롭게 발급하였습니다.' });
});
// Token을 검증하고 Payload를 반환합니다.
function validateToken(token, secretKey) {
try {
const payload = jwt.verify(token, secretKey);
return payload;
} catch (error) {
return null;
}
}
refreshToken도 같은 방식으로 검증한다.
대신, 서버와 사용자의 Refresh Token검증이 추가된다
1. 사용자가 Cookie를 전달할 대, Refresh Token이 없는 경우 -> Refresh Token이 존재하지 않습니다.
2. 사용자가 전달한 Refresh Token이 유효하지 않을 경우 -> Refresh Token이 유효하지 않습니다.
3. Refresh Token이 유효, 서버에 토큰 정보가 없을 경우 -> Refresh Token의 정보가 서버에 존재하지 않습니다.
토큰 상태에 따라 적절한 응답이 반환.
정상 인증시, 응답에서 확인 가능(Access Token의 Payload/Refresh Token이 인증된 Access Token의 Cookie)
4. 생각 정리
Access Token과 Refresh Token을 사용하면 보안적으로는 안전하지만, 인증 절차가 길어지고 복잡해진다. 구현 또한 어려워진다. 또, Access Token이 만료될 때 마다 새로 발급하는 과정에서 요청 횟수도 늘어난다.
Access Token만을 사용한다면 프로젝트를 신속하게 구현할 수 있으며 인증 절차가 간소화 된다.
개발자는 두 방식을 개발 목적을 고려하여 선택할 수 있어야 한다.
Access Token과 Refresh Token에 대한 이야기가 끝났다.
인증과 인가 관련된 내용은 이로서 끝인듯 하고
다음은 트랜잭션에 대한 TIL로 찾아뵙도록 하겠다.(아마)