TIL 26일차 (20240130)

박세연·2024년 1월 30일

TIL

목록 보기
22/70

Access Token와 Refresh Token

✏️ Access Token

  • 사용자의 인증이 완료된 후 해당 사용자를 인증하는 용도로 발급하는 토큰
  • 서버가 재시작되더라도(stateless-무상태) 동일하게 작동됨
  • 사용자 인증에 필요한 모든 정보가 들어있으므로 서버는 탈취 위험을 인식하고 피해를 최소화할 수 있는 방향으로 개발을 진행

✏️ Refresh Token

  • Acess Token을 발급받기 위한 목적으로 발급하는 토큰
  • 필요한 경우 서버에서 강제로 토큰을 만료시킬 수 있음 -> 사용자의 인증 상태를 언제든지 서버에서 제어함

Access Token, Refresh Token 만들기

🚀 필요한 패키지

yarn add express jsonwebtoken cookie-parser

🚀 Access token, Refresh token 발급하기

// 비밀 키는 실제로 .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으로 오타를 내버린 것... 오타가 있는지 중간중간 꼭 점검해볼것... 에러가 뜨면 오타부터 있는지 확인해 볼것...

🚀 Refresh token으로 Access token을 재발급하는 API

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을 정상적으로 새롭게 발급했습니다.'});
});

로그 (Log) 미들웨어 - winston

log middleware

  • 클라이언트의 모든 요청 사항을 기록하여 서버의 상태를 모니터링하고, 문제가 발생할 때 빠르게 진단, 그리고 사용자의 행동을 분석하는 미들웨어

🚀 winston 설치

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

🚀 게시글 생성 API

  • posts.router.js에 작성
/* 게시글 작성 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

/* 게시글 조회 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

  • comments.router.js에 작성
/** 댓글 생성 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

/** 댓글 조회 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]);

profile
배워나가는 중

0개의 댓글