🔥 학습목표
- 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
: 발급받은 토큰이 제대로 만들어진 토큰인지 확인해주는 메서드. 발급 받은 토큰과 비밀 키를 인자로 전달한다.