🔥 학습목표
- Token을 사용하여 사용자의 로그인 상태를 저장한다.
.env 파일을 생성하여 액세스 토큰과 리프레시 토큰에 사용할 Salt를 정의한다.
const express = require('express');
const cors = require('cors');
const logger = require('morgan');
const cookieParser = require('cookie-parser');
const fs = require('fs');
const https = require('https');
const controllers = require('./controllers');
const app = express();
cookie-parser 를 사용한다.app.post('/login', controllers.login);
app.post('/logout', controllers.logout);
app.get('/userinfo', controllers.userInfo);

마찬가지로 이렇게 구성 되어있다.
필요한 상태 관리
const { userId, password } = req.body.loginInfo;
const { checkedKeepLogin } = req.body;
클라이언트에서 POST 요청 시, loginInfo 객체에 유저 아이디와 비밀번호가 request body로 담겨 온다.
로그인 유지 옵션을 체크했으면 true, 아니면 false 값이 넘어온다.
사용자 계정 데이터
const userInfo = {
...USER_DATA.filter((user) => user.userId === userId && user.password === password)[0],
};
저장 된 회원 데이터 중 아이디와 비밀번호가 일치한 회원 정보를 불러온다.
주요 기능
여기서부터 쿠키/세션 실습과 비교하면 좋다.
if (!userInfo.id) {
res.status(401).send('Not Authorized');
}
유저 정보가 존재하지 않는다면 에러 메세지를 출력한다.
const { accessToken, refreshToken } = generateToken(userInfo, checkedKeepLogin);
generateToken 함수에 대해선 후에 설명한다.
if (refreshToken) {
res.cookie('refresh_jwt', refreshToken, {
domain: 'localhost',
path: '/',
sameSite: 'strict',
httpOnly: true,
secure: true,
expires: new Date(Date.now() + 24 * 3600 * 1000 * 7), // 7일 후 소멸되는 Persistent Cookie
});
}
refresh_jwt 라는 이름의 쿠키를 전송한다. 쿠키에는 리프레시 토큰을 저장하며 해당 쿠키는 7일 후 소멸되는 expires 옵션을 갖고있다. (Presistent Cookie) res.cookie('access_jwt', accessToken, {
domain: 'localhost',
path: '/',
sameSite: 'strict',
httpOnly: true,
secure: true,
// Expires 옵션이 없는 Session Cookie
});
return res.redirect('/userinfo');
};
리프레시 토큰이 있든 없든 일단 액세스 토큰은 쿠키로 무조건 보낸다. 이때 액세스 쿠키는 expires 옵션이 없는 세션 쿠키에 해당한다.
쿠키를 전송한 후에는 로그인한 사용자 정보를 불러오기 위해 /userinfo 경로로 리다이렉트 한다.
const accessToken = req.cookies['access_jwt'];
const refreshToken = req.cookies['refresh_jwt'];
const accessPayload = verifyToken('access', accessToken);
유저 정보를 전달하려면 리소스를 요청한 사용자가 유효한 액세스 토큰을 갖고있는지 검증해야 한다.
req.cookies로 액세스 토큰과 리프레시 토큰을 가져온 뒤, 액세스 토큰에 대하여 verifyToken 함수로 유효성 검증을 실행한다.
해당 함수에 대해선 나중에 설명한다.
if (accessPayload) {
const userInfo = { ...USER_DATA.filter((user) => user.id === accessPayload.id)[0] };
if (!userInfo) {
return res.status(401).send('Not Authorized');
}
delete userInfo.password;
return res.json(userInfo);
} else if (refreshToken) {
const refreshPayload = verifyToken('refresh', refreshToken);
...
유효한 액세스 토큰을 가지고 있는 사용자라면, Payload에 저장 된 id 와 일치한 정보를 가져온다.
만약 일치하는 사용자 정보가 없다면 401 에러 메세지를 보낸다.
사용자 인증을 끝냈으니 이제 정보를 전달해줘야 한다. 가지고 있는 사용자 정보 중 민감한 정보인 password를 제거하고 json 형식으로 전달한다.
만약 리프레시 토큰을 가지고 있는 사용자라면 리프레시 토큰에 대해서도 검증(verifyToken) 해야한다.
if (!refreshPayload) {
return res.status(401).send('Not Authorized');
}
const userInfo = USER_DATA.filter((user) => user.id === refreshPayload.id)[0];
const { accessToken } = generateToken(userInfo);
res.cookie('access_jwt', accessToken, {
domain: 'localhost',
path: '/',
sameSite: 'strict',
httpOnly: true,
secure: true,
// Expires 옵션이 없는 Session Cookie
});
access_jwt라는 이름의 쿠키로 전송한다. 처음 생성했을 때와 마찬가지로 Expires 옵션이 없는 세션 쿠키로 설정한다. return res.json({ ...userInfo, password: undefined });
}
password를 undefined로 바꾼 뒤 사용자 정보를 json 형태로 전달한다.return res.status(401).send('Not Authorized');
};
쿠키 삭제
const refreshToken = req.cookies['refresh_jwt'];
if (refreshToken) {
res.clearCookie('refresh_jwt', {
domain: 'localhost',
path: '/',
sameSite: 'strict',
secure: true,
});
}
res.clearCookie('access_jwt', {
domain: 'localhost',
path: '/',
sameSite: 'strict',
secure: true,
});
return res.status(205).send('Logged Out Successfully');
req.cookies로 쿠키에 저장된 리프레시 토큰을 가져온 뒤, 리프레시 토큰이 존재하면 refresh_jwt 쿠키를 삭제하고, 존재하지 않으면 액세스 토큰 쿠키인 access_jwt만 삭제한다.require('dotenv').config();
const { sign, verify } = require('jsonwebtoken');
dotenv : 환경변수를 .env 파일에 저장하고 process.env 로 로드하는 의존성 모듈
jsonwebtoken : JWT 토큰을 자동으로 생성해주는 메서드가 담긴 모듈
(npm install jsonwebtoken 로 설치한다)
generateToken
(user, checkedKeepLogin) => {
const payload = {
id: user.id,
email: user.email,
};
let result = {
accessToken: sign(payload, process.env.ACCESS_SECRET, {
expiresIn: '1d', // 1일간 유효한 토큰을 발행
}),
};
if (checkedKeepLogin) {
result.refreshToken = sign(payload, process.env.REFRESH_SECRET, {
expiresIn: '7d', // 일주일간 유효한 토큰을 발행
});
}
return result;
}
토큰의 payload 부분에 저장할 사용자 정보를 정의한다.
sign : 토큰을 만들어 클라이언트에 발급해주는 메서드. 전송 데이터 payload와 .env 파일에 저장한 Salt 값을 인자로 전달한다.
액세스 토큰의 유효기간은 하루, 리프레시 토큰의 유효기간은 일주일이다. (리프레시 토큰은 사용자가 로그인 유지하기 체크박스를 체크했을 때만 생성된다.
verifyToken
(type, token) => {
let secretKey, decoded;
switch (type) {
case 'access':
secretKey = process.env.ACCESS_SECRET;
break;
case 'refresh':
secretKey = process.env.REFRESH_SECRET;
break;
default:
return null;
}
try {
decoded = verify(token, secretKey);
} catch (err) {
console.log(`JWT Error: ${err.message}`);
return null;
}
return decoded;
}
verify : 발급받은 토큰이 제대로 만들어진 토큰인지 확인해주는 메서드. 발급 받은 토큰과 비밀 키를 인자로 전달한다.