
yarn add express jsonwebtoken cookie-parser
// 비밀 키는 실제로 .env에서 관리
const ACCESS_TOKEN_SECRET_KEY = `~~`; // Access Token의 비밀 키를 정의
const REFRESH_TOKEN_SECRET_KEY = `~~`; // Refresh Token의 비밀 키를 정의
app.use(express.json());
app.use(cookieParser());
const tokenStorages = {} //리프레시 토큰을 관리할 객체
/** 엑세스, 리프레시 토큰 발급 API */
app.post('/tokens', async(req,res)=>{
// id 전달
const {id} = req.body;
// 엑세스 토큰과 리프레시 토큰을 발급
const accessToken = createAccessToken(id);
const refreshToken = createRefreshToken(id);
tokenStorages[refreshToken] = {
id: id,
ip: req.ip,
userAgent: req.headers['user-agent'],
}
//클라이언트에게 쿠키(토큰)을 할당
res.cookie('accessToken', accessToken);
res.cookie('refreshToken', refreshToken);
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;
}
⭐ jwt의 sign 함수를 이용하여 id, key, 만료 시간을 지정한다는 것 기억하기!
/** Acess token 검증 API */
app.get('/tokens/validate', (req,res)=>{
const {accessToken} = req.cookies;
//엑세스 토큰이 존재하는지 확인한다.
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.status(200).json({message:`${id}의 Payload를 가진 Token이 정상적으로 인증되었습니다.`})
});
// Token을 검증하고, Payload를 조회하기 위한 함수
function validateToken(token,secretKey){
try{
return jwt.verify(token, secretKey);
} catch (err){
return null;
}
}
나는 여기에서 계속해서 400 error가 떴는데 알고보니 AccessToken을 AccesssToken으로 오타를 내버린 것... 오타가 있는지 중간중간 꼭 점검해볼것... 에러가 뜨면 오타부터 있는지 확인해 볼것...
app.post('/tokens/refresh', async(req,res)=>{
// Refresh Token 가져와서 검증하기
const {refreshToken} = req.cookies;
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이 정상적이지 않습니다.'});
}
// Refresh Token으로 userInfo 받아오기
const userInfo = tokenStorages[refreshToken];
if(!userInfo){
return res.status(419).json({errorMessage:'Refresh Token의 정보가 서버에 존재하지 않습니다.'});
}
// 받은 userInfo에 새 Access Token 생성하기
const newAcessToken = createAccessToken(userInfo.id);
res.cookie('accessToken', newAcessToken);
return res.status(200).json({message:'Access Token을 정상적으로 새롭게 발급했습니다.'});
});
yarn add winston
import winston from 'winston';
const logger = winston.createLogger({
level: 'info', // 로그 레벨을 'info'로 설정
format: winston.format.json(), // 로그 포맷을 JSON 형식으로 설정
transports: [
new winston.transports.Console(), // 로그를 콘솔에 출력
],
});
export default function (req, res, next) {
// 1. 클라이언트의 요청이 시작된 (현재)시간을 기록
const start = new Date().getTime();
// 3. 응답이 완료되면 로그를 기록
res.on('finish', () => {
const duration = new Date().getTime() - start; //기간 설정
logger.info(
`Method: ${req.method}, URL: ${req.url}, Status: ${res.statusCode}, Duration: ${duration}ms`,
);
});
//2. 다음 app.use 실행
next();
}
export 부분에 처리 순서를 숫자로 표시했다.
아래 코드는 로그 미들웨어를 등록한 app.js
⭐ 로그 미들웨어는 클라이언트의 요청이 발생했을 때 가장 먼저 실행되어야하므로 전역 미들웨어 중에서 가장 최상단에 있다.
import express from 'express';
import cookieParser from 'cookie-parser';
import LogMiddleware from './middlewares/log.middleware.js';
import UsersRouter from './routes/users.router.js';
...
app.use(LogMiddleware);
app.use(express.json());
app.use(cookieParser());
// error-handling.middleware.js
export default function (err, req, res, next) {
// 에러를 출력합니다.
console.error(err);
// 클라이언트에게 에러 메시지를 전달합니다.
res.status(500).json({ errorMessage: '서버 내부 에러가 발생했습니다.' });
}
그리고 회원가입 API에 에러처리 리팩토링을 할 때 try, catch를 사용한다.
router.post('/sign-up', async (req, res, next) => {
try {
const { email, password, name, age, gender, profileImage } = req.body;
const isExistUser = await prisma.users.findFirst({
where: {
email,
},
});
.
.
.
} catch (err) {
next(err);
}
그리고 app.js에 에러처리 미들웨어를 등록한다.
import ErrorHandlingMiddleware from './middlewares/error-handling.middleware.js';
.
.
.
app.use(cookieParser());
app.use('/api', [UsersRouter]);
app.use(ErrorHandlingMiddleware);
⭐ 에러처리 미들웨어는 클라이언트의 요청이 실패했을 때 가장 마지막에 실행되어야하므로 로그와 반대로 전역 미들웨어 중 가장 최하단에 있어야한다. 또한 서버 내부에서 발생한 에러를 상세하게 제공하면 악의적인 공격을 받을 수 있으므로 에러 처리 미들웨어에서는 추상적인 내용을 전달해야한다.
/* 게시글 작성 API */
router.post('/posts', authMiddleware, async (req, res, next) => {
// authMiddleware 검증 후 실행
const { title, content } = req.body;
const { userId } = req.user;
const post = await prisma.posts.create({
data: {
userId: +userId,
title: title,
content: content
}
});
return res.status(201).json({ data: post });
});
/* 게시글 조회 API */
router.get('/posts', async (req, res, next) => {
const posts = await prisma.posts.findMany({
select: {
postId: true,
userId: true,
title: true,
createdAt: true,
updatedAt: true,
},
// 정렬방식 설정
orderBy: {
createdAt: 'desc'
}
});
return res.status(200).json({data:posts});
});
/* 게시글 상세 조회 API */
// id로 조회
router.get('/posts/:postId',async(req,res,next)=>{
const {postId}= req.params;
const post = await prisma.posts.findFirst({
where:{postId: +postId},
select:{
postId: true,
userId: true,
title: true,
content: true,
createdAt: true,
updatedAt: true
}
});
return res.status(200).json({data: post});
})
/** 댓글 생성 API */
router.post('/posts/:postId/comments', authMiddleware, async(req, res,next)=>{
const {postId}= req.params;
const {content} = req.body;
const {userId}= req.user; //authMiddleware가 request에 id 할당해줌
// post가 있는지 조회
const post = await prisma.posts.findFirst({where: {postId: +postId}});
if(!post) return res.status(404).json({message:'게시글이 존재하지 않습니다.'});
const comment = await prisma.comments.create({
data:{
postId: +postId,
userId: +userId,
content: content
}
});
return res.status(201).json({data:comment});
})
/** 댓글 조회 API */
router.get('/posts/:postId/comments',async(req,res,next)=>{
const {postId}=req.params;
//id에 해당하는 댓글들을 desc로 정렬(최신순)
const comments = await prisma.comments.findMany({
where: {postId:+postId},
orderBy: {createdAt:'desc'},
});
return res.status(200).json({data:comments});
})
그리고 위의 router들을 app.js에 호출한다.
app.use('/api',[UsersRouter, PostsRouter, CommentsRouter]);