지난 주 node.js기반 API 서버 구현 개인 프로젝트를 진행하면서 배웠던 점들을 정리한다.
Access Token과 Refresh Token의 개념와 Flow
// 엑세스 토큰 생성기
const getAccessToken = ((username, _id) => {
const accessToken = (username, _id) => jwt.sign({ username, _id }, process.env.ACCESS_TOKEN_KEY, {
expiresIn: '30m',
});
return (username, _id) => accessToken(username, _id);
})();
JWT(JSONWEBTOKEN) 패키지를 통해 토큰을 생성 가능하며 이 때 사용자의 정보를 함께 담아서 생성할 수 있다.
이번 프로젝트의 경우 유저네임과 고유 ID를 서버에서 환경변수에 저장해서 숨겨둔 절대절대 SECRET KEY와 조합하여 엑세스 토큰을 만드는 방식을 취했다.
추가적으로 expiresIn 옵션을 통해 토큰의 유효기간을 정해줄 수 있으며 결과적으로 아래와 같은 토큰이 생성된다.
Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6ImFwcGxlMiIsIl9pZCI6IjY0OGFhZjkyZmVjMDA5MzUxMzg4Mzk4MyIsImlhdCI6MTY4NzEzODA3NiwiZXhwIjoxNjg3MTM5ODc2fQ.lJ1S_d-hDIbfuF2P87_I-Cu1qlS3diHPIlGNuTEeZNI
JWT는 점(.)을 기준으로 총 세 단계의 세그먼트로 분류가 된다.
HEADER: 해당 토큰에 적용된 해싱(Hashing) 알고리즘과 데이터 타입과 같은 메타데이터에 해당
PAYLOAD: 클라이언트로부터 전달받은 유저정보와 더불어, 만료기간, 주제와 같은 각종 정보를 담은 부분에 해당
VERIFY SIGNATURE: 클라이언트에서 전달받은 토큰이 유효한지를 검증하는 부분, 헤더와 페이로드를 인코딩한 값과 서버에 저장된 Secret 키를 조합하여 해당 세그먼트값을 만든다.
위 세가지 세션을 토대로 토큰이 구성되며 마지막 세션이 검증하는데 활용되는 실질적인 토큰으로 볼 수 있다.
또한 PAYLOAD에 유저 정보도 담을 수 있으므로 토큰을 가져옴과 동시에 해당 토큰이 누구에게 발행된 것인지도 확인할 수 있다.
MongoDB와 mongoose
MVC 패턴에서 M(model)의 역할을 할 수 있도록 돕는 패키지가 mongoose이며 이를 통해 컬렉션을 js파일로서 정의하고, 파일 내 필드를 정의할 수 있다.
기본적으로 각 필드에서 필요로 하는 유일성 유지, 생성 날짜, 수정 날짜 자동 기입 등 기본적으로 필요로 하는 기능을 쉽게 구현할 수 있도록 제공해준다. 이번 프로젝트에서는 개인적으로 createdAt과 updatedAt을 직접 구현했는데 나중에서야 이 기능도 간단하게 모델 정의 부분에서 구현할 수 있다는 것을 알게 되었다...
const schema = new mongoose.Schema(
{
// 스키마 필드들
name: String,
age: Number
},
{
timestamps: true // createdAt 및 updatedAt 필드를 자동으로 설정
}
);
토큰 저장 및 재발급과 관련된 고민
해당 부분이 정해진 답이 없다고 느껴졌다. 인터넷 검색, 멘토님들에게 질문, 전문가의 강의를 참고했지만 엑세스 토큰과 리프레시 토큰 둘 중 어떤게 더 보안이 잘 지켜져야 하는지, 그리고 각 토큰의 유효기간을 어떻게 설정해야 하는지 등의 의견이 다양한다.
나는 맨 처음 참고했던 인증 구현 관련 강의 자료에서 엑세스 토큰은 headers의 Authorization으로 전달하고, 리프레시 토큰은 쿠키와 데이터베이스에 저장하는 방식으로 운영하는 것으로 들었다. 리프레시 토큰을 기본적으로 엑세스 토큰을 홀로 쓰는 것에 대한 보완의 취약성 때문에 사용한다는 의견으로 배웠고, 그렇기 때문에 데이터베이스에 저장한다고 들었다.
이 부분에서 의문이 들었던 점은 엑세스 토큰을 어디에 저장하는지 알려주지 않았다는 점과 보완의 취약성을 개선하고자 만든 리프레시 토큰을 유저에게 쿠키로 제공하는 것이 맞냐? 라는 생각이 들었다.
이 부분에 대해서 멘토님의 의견을 구했으며, 멘토님께서는 엑세스 토큰을 쿠키로 전달하고, 리프레시 토큰의 경우 Redis나 토큰을 위한 데이터베이스에 저장하면서 주기적으로 리프레시 토큰의 유효 기간에 따라 데이터를 정리하는 방식으로 관리한다고 답을 받았다. 전자의 경우 리프레시 토큰을 유저에게 쿠키로 제공하기 때문에 리프레시 토큰에 대한 검증 시 유저와 서버 간 통신이 발생하지만 후자의 경우 유저와의 통신이 없다는 점이었다.
생각해보면 멘토님의 말씀이 더 합리적으로 받아들여지기는 했다. 리프레시 토큰은 엑세스 토큰 만료 시 재발급을 위한 용도로 사용되므로 미들웨어 절차 상 엑세스 토큰의 인증이 먼저 발생하는 것이 자연스러운 흐름이라 생각했고, 그렇다면 엑세스 토큰의 일치여부 검증 뿐 아니라 토큰에 내부적으로 담긴 유저에 대한 정보를 활용한다면 2차적으로 발생할 리프레시 토큰에 대한 검증은 굳이 필요하지 않다는 생각이 들었다. 엑세스 토큰 검증 과정에서 획득한 유저의 identification으로 해당 유저가 로그인 시 서버에 저장되었던 리프레시 토큰 정보를 내부적으로 매칭할 수 있을 것이므로 이를 기반으로 엑세스 토큰을 재발급해주면 된다고 생각했기 때문이다.
결론적으로 나는 엑세스 토큰의 시간을 1분 수준으로 짧게 설정한 뒤 지속적으로 리프레시 토큰으로 재발급해주는 프로세서로 구현되어야 좋지 않을까 라는 결정을 내렸다. 엑세스 토큰은 탈취 시 직접적으로 로그인을 가능케 하는 토큰이므로 탈취되었다 하더라도 1분 뒤 못쓰게 되므로 추가적인 보완 이슈가 발생하기에 짧은 시간이 아닐까? 라는 생각이 들었기 때문이다. 물론 1분 수준이 객관적으로 짧은 시간이 아니라면 더 줄일 필요도 있겠다. 하지만 1분 마다 모든 유저가 리프레시 토큰 활용을 위해 데이터 베이스와의 잦은 통신을 해야한다는 점에서 이 부분이 혹시 서버 과부하와 관련해서 문제가 되지 않을까 라는 생각이 든다. 이 부분은 아직 지식이 부족하므로 향후 확인이 필요할 것 같다.
'백엔드 개발자(node.js)가 되는 과정' 카테고리의 다른 글
Sequelize로 모델 생성 및 테이블 연동하기 (0) | 2023.06.21 |
---|---|
Sequalize로 MYSQL 데이터베이스 마이그레이션하기 (0) | 2023.06.20 |
[nodejs 개인 프로젝트 3일 차] Access Token, Refresh Token (0) | 2023.06.16 |
mongoDB의 find 사용 시 주의할 점 (0) | 2023.06.14 |
[nodejs 개인 프로젝트 2일 차] 로그인, 인증 기능 구현하기 (0) | 2023.06.13 |