게시글 생성 API 비즈니스 로직

  1. 게시글을 작성하려는 클라이언트가 로그인된 사용자인지 검증한다.
  2. 게시글 생성을 위한 title, content를 body로 전달받는다.
  3. Posts 테이블에 게시글을 생성한다.

 

게시글은 사용자 ( Users )는 여러개의 게시글 ( Posts )을 등록할 수 있다는 조건에 따라 사용자와 1:N의 관계를 가지고,

현재 로그인 한 사용자의 정보가 존재했을 때만 게시글을 생성할 수 있도록 구현해야 한다.

 

사용자를 생성했을 때와 동일하게, routers/posts.router.js 파일을 생성하고,  app.js 파일에 라우터를 추가해보자.

 

게시글 생성 API

// src/routes/posts.router.js

import express from 'express';
import { prisma } from '../utils/prisma/index.js';
import authMiddleware from '../middlewares/auth.middleware.js';

const router = express.Router();

/** 게시글 생성 API **/
router.post('/posts', authMiddleware, async (req, res, next) => {
  const { userId } = req.user;
  const { title, content } = req.body;

  const post = await prisma.posts.create({
    data: {
      userId: +userId,
      title,
      content,
    },
  });

  return res.status(201).json({ data: post });
});

export default router;

 

사용자 인증 미들웨어를 통해 게시글을 작성하려는 클라이언트가 로그인된 사용자 인지 검증하였고,

전달된 userId 값을 이용해, 사용자와 1:N 관계를 맺고 있는 게시글을 생성하도록 구현했다.

 

PostsRouter를 등록한 app.js

// src/app.js

import express from 'express';
import cookieParser from 'cookie-parser';
import LogMiddleware from './middlewares/log.middleware.js';
import ErrorHandlingMiddleware from './middlewares/error-handling.middleware.js';
import UsersRouter from './routes/users.router.js';
import PostsRouter from './routes/posts.router.js';

const app = express();
const PORT = 3018;

app.use(LogMiddleware);
app.use(express.json());
app.use(cookieParser());
app.use('/api', [UsersRouter, PostsRouter]);
app.use(ErrorHandlingMiddleware);

app.listen(PORT, () => {
  console.log(PORT, '포트로 서버가 열렸어요!');
});

 

게시글 조회 API

게시글 목록 조회 API와 게시글 상세 조회 API는 이전에 prisma-curd에서 구현한 Posts 테이블과 동일한 코드로 구성되어 있다.

 

게시글 목록 조회 API

// src/routes/posts.router.js

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

// src/routes/posts.router.js

/** 게시글 상세 조회 API **/
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 });
});

 

로그 미들웨어 ( Log Middleware )는 클라이언트의 모든 요청 사항을 기록해 서버의 상태를 모니터링하기 위한 미들웨어다.

 

로그 미들웨어는 클라이언트의 요청을 기록해 어플리케이션을 모니터링하고 문제가 발생할 때 빠르게 진단할 수 있다.

또한, 로그 데이터는 사용자의 행동을 분석하는 등 데이터 분석 작업에도 활용할 수 있다.

규모가 큰 프로젝트를 진행하게 되면, 화면에 표시되는 모든 로그를 일일이 확인하는 것은 불가능에 가깝다.

이런 경우를 대비해 로그 기능을 지원하는 morgan, winston과 같은 라이브러리를 사용하거나 AWS CloudWatch, Datadog와 같은 외부 모니터링 솔루션 서비스를 이용해 로그를 수집하거나 관리할 수 있다.

 

Datadog와 같은 서비스를 이용해 로그 수집, 로그 분석과 같은 서비스를 빠르게 구현할 수 있다.

 

아래 예시를 통해 클라이언트의 요청을 터미널에 기록하는 간단한 로그 미들웨어를 구현해보자.

우선 winstion 라이브러리를 설치하는것으로 시작!

 

# yarn을 이용해 winston을 설치합니다.
yarn add winston

 

log.middleware

// src/middlewares/log.middleware.js

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) {
  // 클라이언트의 요청이 시작된 시간을 기록합니다.
  const start = new Date().getTime();

  // 응답이 완료되면 로그를 기록합니다.
  res.on('finish', () => {
    const duration = new Date().getTime() - start;
    logger.info(
      `Method: ${req.method}, URL: ${req.url}, Status: ${res.statusCode}, Duration: ${duration}ms`,
    );
  });

  next();
}

 

app.js

// src/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';

const app = express();
const PORT = 3018;

app.use(LogMiddleware);
app.use(express.json());
app.use(cookieParser());
app.use('/api', [UsersRouter]);

app.listen(PORT, () => {
  console.log(PORT, '포트로 서버가 열렸어요!');
});

 

로그 레벨 ( level )은 로그의 중요도를 나타낸다. 

단순히 클라이언트의 요청 사항을 기록하기 위해 "Info" 레벨을 사용하지만, "error", "warn", "debug" 등 다양한 로그레벨이 있으며, 특정 상황에 따라 출력하는 레벨을 다르게 구현할 수 있다.

 

로그 미들웨어는 클라이언트의 요청이 발생했을 때, 가장 먼저 실행되어야 하는 미들웨어다.

따라서 app.use를 이용한 전역 미들웨어 중에서 가장 최상단에 위치한다.

 

에러 처리 미들웨어

에러 처리 미들웨어 ( Error Handling Middleware )는 Express.js에서 발생한 에러를 통합적으로 처리하기 위한 미들웨어다

 

error-handling.middleware

// src/middlewares/error-handling.middleware.js

export default function (err, req, res, next) {
  // 에러를 출력합니다.
  console.error(err);

  // 클라이언트에게 에러 메시지를 전달합니다.
  res.status(500).json({ errorMessage: '서버 내부 에러가 발생했습니다.' });
}

 

user.router.js

// src/routes/users.router.js

/** 사용자 회원가입 API 에러 처리 미들웨어 **/
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,
      },
    });

    if (isExistUser) {
      return res.status(409).json({ message: '이미 존재하는 이메일입니다.' });
    }

    // 사용자 비밀번호를 암호화합니다.
    const hashedPassword = await bcrypt.hash(password, 10);

    // Users 테이블에 사용자를 추가합니다.
    const user = await prisma.users.create({
      data: {
        email,
        password: hashedPassword, // 암호화된 비밀번호를 저장합니다.
      },
    });

    // UserInfos 테이블에 사용자 정보를 추가합니다.
    const userInfo = await prisma.userInfos.create({
      data: {
        userId: user.userId, // 생성한 유저의 userId를 바탕으로 사용자 정보를 생성합니다.
        name,
        age,
        gender: gender.toUpperCase(), // 성별을 대문자로 변환합니다.
        profileImage,
      },
    });

    return res.status(201).json({ message: '회원가입이 완료되었습니다.' });
  } catch (err) {
    next(err);
  }
});

 

try catch 구문으로 묶고 error가 나면 next로 error-handling.middleware로 에러 전달

 

 

app.js

// src/app.js

import express from 'express';
import cookieParser from 'cookie-parser';
import LogMiddleware from './middlewares/log.middleware.js';
import ErrorHandlingMiddleware from './middlewares/error-handling.middleware.js';
import UsersRouter from './routes/users.router.js';

const app = express();
const PORT = 3018;

app.use(LogMiddleware);
app.use(express.json());
app.use(cookieParser());
app.use('/api', [UsersRouter]);
app.use(ErrorHandlingMiddleware);

app.listen(PORT, () => {
  console.log(PORT, '포트로 서버가 열렸어요!');
});

 

에러 처리 미들웨어는 클라이언트의 요청이 실패했을 때, 가장 마지막에 실행되어야 하는 미들웨어다.

따라서 app.use를 이용한 전역 미들웨어 중 가장 최하단에 위치한다.

 

에러 처리 미들웨어는 모든 에러를 관리하는 미들웨어, 서버 내부에서 발생한 에러를 상세하게 클라이언트에게 제공한다면 악의적인 사용자에게 공격의 표적이 될 수 있다. 

그러므로, 에러 처리 미들웨어에서는 "서버에서 에러가 발생했습니다."와 같은 추상적인 내용을 클라이언트에게 전달하도록 구현해야한다.

 

에러처리 공식 문서 

https://expressjs.com/ko/guide/error-handling.html

 

Express - Node.js web application framework

 

expressjs.com

 

 

 

delete 뒤에 객체의 속성 값을 입력하면 해당 속성을 제거한다.

 

let test = {a:0, b:1, c:2};
delete test.a;

console.log(test); // { b: 1, c: 2 }

 

https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Operators/delete

 

delete 연산자 - JavaScript | MDN

**delete**는 객체의 속성을 제거합니다. 속성의 값이 객체이고 더 이상 그 객체에 대한 참조가 없다면, 해당 객체는 결국 자동으로 메모리에서 해제됩니다.

developer.mozilla.org

 

정규표현식은 어떤 특정한 규칙을 가진 문자열의 집합을 표현하기 위해 사용하는 형식 언어다.

 

아래 표를 통해 기본적인 정규표현식의 문법을 확인할 수 있다.

메타문자 기능 설명
. 문자 1개의 문자와 일치한다. 단일행 모드에서는 새줄 문자를 제외한다.
[ ] 문자 클래스 "["과 "]" 사이의 문자 중 하나를 선택한다. "|"를 여러 개 쓴 것과 같은 의미다.
예를 들어 [abc]d는 ad, bc, cd를 뜻한다. 또한, "-" 기호와 함께 쓰면 범위를 지정할 수 있다. "[a-z]"는 a부터 z까지 중 하나, "[1-9]"는 1부터 9까지 중의 하나를 의미한다.
[^ ] 부정 문자 클래스 안의 문자를 제외한 나머지를 선택한다. 예를 들면 [^abc]d는 ad, bd, cd는 포함하지 않고 ed, fd 등을 포함한다. [^a-z]는 알파벳 소문자로 시작하지 않는 모든 문자를 의미한다.
^ 처음 문자열이나 행의 처음을 의미한다.
$ 문자열이나 행의 끝을 의미한다.
( ) 하위식 여러 식을 하나로 묶을 수 있다. "abc|adc"와 "a(b|d)c"는 같은 의미를 가진다.
\n 일치하는 n번째 패턴 일치하는 패턴들 중 n번째를 선택하며, 여기에서 n은 1에서 9 중 하나가 올 수 있다.
* 0회 이상 0개 이상의 문자를 포함한다. "a*b"는 "b", "ab", "aab", "aaab"를 포함한다.
{m,n} m회 이상 n회 이하 "a{1,3}b"는 "ab", "aab", "aaab"를 포함하지만, "b"나 "aaab"는 포함하지 않는다.

 

다양한 정규표현식을 살펴보자.

 

숫자만 

/^[0-9]+$/

 

소문자 영어와 숫자로만 이루어진 2자 이상 10자 이하 문자열

 

/^(?=[a-za-z])(?=.*[0-9]).{2,10}$/

 

이메일

/^([a-zA-Z0-9._%-]+@[a-zA-Z0-9.-]+.[a-zA-Z]{2,6})*$/

 

 jpg, gif, png 확장자를 가진 그림 파일명

/([^\s]+(?=.(jpg|gif|png)).\2)/

 

'IT' 카테고리의 다른 글

[IT] 객체 지향 프로그래밍 ( Object-Oriented Programming, OOP )  (0) 2024.09.26
[IT] Visual Studio Code - 디버그  (0) 2024.09.19
[IT] Access Token, Refresh Token  (1) 2024.09.09
[IT] 인증, 인가  (0) 2024.09.09
[IT] JWT  (0) 2024.09.08

오늘의 목표

더보기

✔️ 개인 과제 구현 


⏱️ 오늘의 일정

10:00 ~ 11:00 - 스탠다드반 CS 강의

11:00 ~ 19:00 - 개인과제 구현

19:00 ~ 20:30 - 챌린지반 수업


📜 스탠다드 반 CS 강의

10:00 ~ 11:00 - 스탠다드 반 CS 강의

 

매주 화목에 진행하는 스탠다드반 수업 강의가 있어서 참여했다.

오늘 내용은 이더넷, 허브, 스위치, Mac 주소에 관해 배웠다.

 

📜 개인과제 구현

11:00 ~ 19:00 개인과제 구현

 

git Commit 목록

 

오늘은 속도가 좀 붙어서 로그인 기능과 캐릭터 생성, 캐릭터 삭제, 캐릭터 조회, 아이템 생성 까지 구현했다.

 

로그인 기능에서 고민이 있던 부분을 포스팅하고자 한다.

처음에는 로그인 할때 아이디와 비밀번호가 맞으면 AccessToken을 생성해 반환해주는 걸로 우선 구현을 했다.

 

이렇게 구현하다가는 만약 재로그인한 대상이 해당 유저가 아닐 경우에는? 어떻게 해야할까? 라고 생각이 들어서 재로그인할때는 AccessToken을 살펴보기로 하고 구현했다.

 

내가 생각한 로그인 방식은 다음과 같다.

 1. 로그인 요청한 대상이 AccessToken을 가지고 오지 않았으면

  1.1 처음 발급 하는것이므로 AccessToken과 RefreshToken을 발급하고 로그인 성공 처리한다.

 2. 로그인 요청한 대상이 AccessToken을 가지고 오면

  2.1 해당 AccessToken이 유효한지 판단한다.

   2.1.1 AccessToken이 유효하면 로그인 성공 처리한다.

   2.1.2 AccessToken이 유효하지 않으면, RefreshToken을 가지고 있는지 확인한다.

    2.1.2.1 RefreshToken을 가지고 있으면, RefreshToken이 유효한지 판단한다.

     2.1.2.1.1 RefreshToken이 유효하면, AccessToken을 재발급하고 로그인 성공 처리한다.

     2.1.2.1.2 RefreshToken이 유효하지 않으면, RefreshToken과 AccessToken을 재발급하고 로그인 성공 처리한다.

    2.1.2.2 RefreshToken을 가지고 있지않으면, RefreshToken을 발급하고 AccessToken을 재발급하고 로그인 성공처리

 

위처럼 생각하고 구현해보니 어떻게해도 무조건 로그인은 성공처리가 되는것이 아닌가?

 

다시한번 생각하니 로그인 처리에서는 AccessToken과 RefreshToken을 발급 하는것이 목적인것이고 ( 아이디와 비밀번호만 맞으면 ), 클라이언트에서는 AccessToken을 가지고 로그인을 제외한 다른 작업을 할때, '검증' 하는 것을 목적으로 삼으면 되는것이니까.

 

로그인 처리에서는 앞서 언급했듯이 아이디와 비밀번호만 맞으면 로그인 성공처리로 해주면 되겠구나라는 생각이 들어서 그대로 진행했다.

 

로그인 처리를 제외하고는 다른 캐릭터 생성, 캐릭터 삭제 등 인증이 필요한 곳에는 AccessToken으로 검증을 해 해당 값이 유효하지 않으면 바로 에러를 반환하도록 구현했다.

 

위처럼 생각한 것이 맞는지는 확신할 수 없다.

 

https://velog.io/@chuu1019/Access-Token%EA%B3%BC-Refresh-Token%EC%9D%B4%EB%9E%80-%EB%AC%B4%EC%97%87%EC%9D%B4%EA%B3%A0-%EC%99%9C-%ED%95%84%EC%9A%94%ED%95%A0%EA%B9%8C

 

🧐 Access Token과 Refresh Token이란 무엇이고 왜 필요할까?

JWT 토큰은 유저의 신원이나 권한을 결정하는 정보를 담고 있는 데이터 조각이다. JWT 토큰은 비밀키로 암호화되어 있기에 비교적 안전하다. 그런데 탈취 당했을 때가 문제다!! 어떻게 위험을 최

velog.io

위 글을 보면 맞는거 같기도 하고.. 애매..

 

내일 과제를 완료하면 따로 튜터님한테 가서 물어보긴 해야겠다.

 

로그인

// 로그인
router.post('/sign-in', async (req, res, next) => {
    const { id, password } = req.body;
    const user = await prisma.users.findFirst({
        where: { id }
    });

    // 아이디와 비밀번호 검사
    if (!user) {
        return res.status(401).json({ message: `${id}은 존재하지 않는 아이디 입니다.` });
    }
    else if (!(await bcrypt.compare(password, user.password))) {
        return res.status(401).json({ message: `비밀번호가 일치하지 않습니다.` });
    }

    // 재로그인할때 필요한 accessToken을 가져옴
    const c2sAccessToken = req.cookies.accessToken;    

    let s2cAccessToken = 0;
    let s2cRefreshToken = 0;

    // accessToken 발급 여부 판단
    let newAccessToken = false;

    if (!c2sAccessToken) // 액세스 토큰이 없음
    {
        // 액세스 토큰 새 발행
        newAccessToken = true;
    }
    else // 액세스 토큰이 있음
    {        
        // 액세스 토큰 유효한지 확인
        // 유효하면 로그인 성공
        const [tokenType, token] = c2sAccessToken.split(' ');

        // tokenType이 맞는지 확인
        if (tokenType !== process.env.TOKEN_TYPE_CHECK) {
            return res.status(404).send('not found');
        }

        // 다른 유저의 AccessToken을 가지고 왔을 경우
        // 현재 로그인한 유저를 대상으로 AccessToken 재발행
        const myToken = CreateAccessToken(id);
        if (myToken !== c2sAccessToken)
        {
            newAccessToken = true;
        }
        else
        {
            // 나의 AccessToken을 가지고 오면 유효한지 확인
            const payload = ValidateToken(token, process.env.ACCESS_TOKEN_SECRET_KEY);
            if (!payload) // 액세스 토큰이 유효하지 않음
            {
                // 액세스 토큰 새 발행
                newAccessToken = true;
            }
        }        
    }

    // 액세스 토큰 새로 발급
    if (newAccessToken) {
        // DB에서 리프레시 토큰을 읽어옴
        const dbRefreshToken = await prisma.refreshTokens.findFirst({
            where: { userId: id },
            select: {
                token: true
            }
        });        

        // DB에 리프레시 토큰이 없음
        if (dbRefreshToken == null)
        {
            // 액세스 토큰과 리프레시 토큰을 새로 발급
            s2cAccessToken = CreateAccessToken(id);
            s2cRefreshToken = CreateRefreshToken(id);

            // 리프레시 토큰을 DB에 저장
            const newDBRefreshToken = await prisma.refreshTokens.create({
                data: {
                    userId: id,
                    token: s2cRefreshToken
                }
            });   

            // 쿠키 전달
            res.cookie('accessToken', s2cAccessToken);
            res.cookie('refreshToken', s2cRefreshToken);
        }
        else 
        {
            // DB에 리프레시 토큰이 있음

            const [tokenType, token] = dbRefreshToken.token.split(' ');

            // tokenType이 맞는지 확인
            if (tokenType !== process.env.TOKEN_TYPE_CHECK) {
                return res.status(404).send('not found');
            }

            // 리프레시 토큰이 유효한지 확인
            const dbRefreshTokenCheck = ValidateToken(token, process.env.REFRESH_TOKEN_SECRET_KEY);
            if (dbRefreshTokenCheck) // 리프레티 토큰이 유효함
            {
                // 액세스 토큰 발급
                s2cAccessToken = CreateAccessToken(id);                

                // 액세스 토큰 전달
                res.cookie('accessToken', s2cAccessToken);
            }
            else // 리프레티 토큰이 유효하지 않음
            {
                // 액세스 토큰과 리프레시 토큰을 새로 발급
                s2cAccessToken = CreateAccessToken(id);
                s2cRefreshToken = CreateRefreshToken(id);

                // 리프레시 토큰을 DB에 업데이트
                const newDBRefreshToken = await prisma.refreshTokens.update({
                    where: { userId: id },
                    data: {                        
                        token: s2cRefreshToken
                    }
                }); 

                // 쿠키 전달
                res.cookie('accessToken', s2cAccessToken);
                res.cookie('refreshToken', s2cRefreshToken);
            }
        }
    }

    return res
        .status(200)
        .json({ message: `${id}로 로그인 성공` });
});

 

 

캐릭터 생성, 캐릭터 삭제, 캐릭터 조회에서는 authMiddleware를 통해 검증하도록 구현했다.

export default async function (req, res, next) {
    const c2sAccessToken = req.cookies.accessToken;
    if (c2sAccessToken === undefined) {
        return res.status(404).json({ message: 'not found' });
    }

    const [tokenType, accessToken] = c2sAccessToken.split(' ');
    if (tokenType !== process.env.TOKEN_TYPE_CHECK) {
        return res.status(401).json({ message: '토큰 타입이 일치하지 않습니다.' });
    }

    // 토큰 검증
    const decodedToken = ValidateToken(accessToken, process.env.ACCESS_TOKEN_SECRET_KEY);
    if (!decodedToken) {
        return res.status(401).json({ message: '토큰이 만료되었습니다. 로그인을 다시 해주세요' })
    }

    const id = decodedToken.id;

    const user = await prisma.users.findFirst({
        where: {
            id : id
        }
    });

    if (!user) {
        return res.status(401).json({message:`${id} 사용자를 찾을수 없습니다.`})
    }

    req.user = user;

    next();    
}

 

로그인한 유저가 AccessToken을 들고오지 않았을 경우, TokenType이 일치하지 않을 경우,

그리고 Token이 만료할 경우 에러를 반환하도록했다.

오늘의 목표

더보기

✔️ 프로그래머스 코테 문제 풀기

✔️ Node.js 강의 듣기


⏱️ 오늘의 일정

9:00 ~ 10:00 - 프로그래머스 코테 문제 풀기

10:00 ~ 17:00 - Node.js 강의

17:00 ~ 21:00 - 개인과제

 

 

📜 프로그래머스 코테 문제 풀기

9:00 ~ 10:00 - 프로그래머스 코테 문제 풀기

 

오늘도 알고리즘 코드카타를 진행해 문제를 풀었다.

 

옹알이(2)

https://github.com/YamSaeng/AlgorithmCodingTest/tree/main/%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%A8%B8%EC%8A%A4/1/133499.%E2%80%85%EC%98%B9%EC%95%8C%EC%9D%B4%E2%80%85%EF%BC%882%EF%BC%89

 

AlgorithmCodingTest/프로그래머스/1/133499. 옹알이 (2) at main · YamSaeng/AlgorithmCodingTest

This is an auto push repository for Baekjoon Online Judge created with [BaekjoonHub](https://github.com/BaekjoonHub/BaekjoonHub). - YamSaeng/AlgorithmCodingTest

github.com

 

처음에는 아기가 말할 수 있는 모든 단어를 우선 조합하고 진행하려 했는데, 옳은 방법 같지가 않아서

좀더 고민을 하다가 아래처럼 풀게 되었다.

 

아기가 말할 수 있는 단어가 포함되어 있는지 찾는 방법이다.

뒤에 babyStrDuplicationCheck는 같은 발음이 연속되는 것을 방지한다.

function solution(babbling) {
    let answer = 0;    

    for (let i = 0; i < babbling.length; i++) {

        let isCheck = true;
        let babyStrDuplicationCheck = 0;

        for (let j = 0; j < babbling[i].length; j++) {           

            if (babbling[i].substr(j, 3) == "aya" && babyStrDuplicationCheck != 1) {
                babyStrDuplicationCheck = 1;
                j += 2;
            }
            else if (babbling[i].substr(j, 2) == "ye" && babyStrDuplicationCheck != 2) {
                babyStrDuplicationCheck = 2;
                j += 1;
            }
            else if (babbling[i].substr(j, 3) == "woo" && babyStrDuplicationCheck != 3) {
                babyStrDuplicationCheck = 3;
                j += 2;
            }
            else if (babbling[i].substr(j, 2) == "ma" && babyStrDuplicationCheck != 4) {
                babyStrDuplicationCheck = 4;
                j += 1;
            }
            else {
                isCheck = false;
                break;
            }            
        }

        if (isCheck == true) {
            answer++;
        }
    }

    return answer;
}

 

 

📜 Node.js 강의

10:00 ~ 17:00 - Node.js 강의

 

2024.09.09 - [IT] - [IT] 인증, 인가

 

[IT] 인증, 인가

인증 ( Authentication ) 인증 ( Authentication )은 서비스를 이용하려는 사용자가 인증된 신분을 가진 사람이 맞는지 검증하는 작업을 뜻한다.인증 ( Authentication )은 일반적인 사이트의 로그인 기능에 해

program-yam.tistory.com

 

 

2024.09.09 - [Javascript/실습] - [Javascript][실습][게시판 프로젝트] 로그인, 회원가입 API

 

[Javascript][실습][게시판 프로젝트] 로그인, 회원가입 API

Express.js를 이용한 게시판 프로젝트를 만들기 전에, 우선 app.js에 기본적인 토대를 만들고 간단한 API를 구현해보자. 모든 Router에서 Prisma를 사용할 수 있도록 utils/prisma/index.js 파일을 생성한다. ap

program-yam.tistory.com

 

2024.09.09 - [IT] - [IT] Access Token, Refresh Token

 

[IT] Access Token, Refresh Token

Access Token Access Token은 사용자의 인증( 예) 로그인 )이 완료된 후 해당 사용자를 인증하는 용도로 발급하는 토큰을 말한다.앞서 쿠키 ( Cookie )에 jwt를 설정하고, 지정된 만료 시간이 지나면 인증이

program-yam.tistory.com

 

2024.09.09 - [Javascript/실습] - [Javascript][실습] Access Token, Refresh Token 발급

 

[Javascript][실습] Access Token, Refresh Token 발급

Access Token과 Refresh Token을 발급해보자. # yarn을 이용해 프로젝트를 초기화합니다.yarn init -y# express, jsonwebtoken, cookie-parser 패키지를 설치합니다.yarn add express jsonwebtoken cookie-parserpackage.json에 "type":"mod

program-yam.tistory.com

 

2024.09.09 - [Javascript] - [Javascript] .env ( 환경 변수 읽어 오기 )

 

[Javascript] .env ( 환경 변수 읽어 오기 )

.env 파일에 설정한 환경변수 값을 읽어 오고 싶을 경우 dotenv 패키지를 설치해 읽어오면 편하다. yarn add dotenv 위 명령어를 이용해 dotenv 패키지를 설치한다. .envACCESS_TOKEN_SECRET_KEY="YamSaeng52"REFRESH_T

program-yam.tistory.com

 

인증, 인가에 대한 개념을 배우고 이를 토대로 실습으로 로그인과 회원가입 API를 작성했다.

Access Token과 Refresh Token을 배우고 이를 토대로 실습으로 직접 Access Token, Refresh Token을 발급했다.

 

📜 개인과제

17:00 ~ 21:00 - 개인과제

 

개인과제를 진행했다. 오늘은 회원가입 까지만 우선 완료하고 내일 캐릭터 생성 까지 끝낼 예정이다.

 

회원 가입 router

router.post('/sign-up', async (req, res, next) => {
    const { id, password, confirmPassword } = req.body;
    const isExistUser = await prisma.users.findFirst({
        where: {
            id,
        },
    });

    const engNumIdRule = /^(?=[a-za-z])(?=.*[0-9]).{2,10}$/;
    if (!engNumIdRule.test(id)) {
        return res.status(409).json({ message: '소문자 영어와 숫자를 조합해 입력하세요 ( 최소 2글자, 최대 10글자 )' });
    }

    if (confirmPassword === undefined) {
        return res.status(409).json({ message: '비밀번호 확인을 입력하세요' });
    }

    if (password.length < 6) {
        return res.status(409).json({ message: '비밀번호는 최소 6자 이상이 되어야 합니다.' });
    }

    if (password != confirmPassword) {
        return res.status(409).json({ message: '비밀번호와 비밀번호 확인이 일치하지 않습니다.' });
    }

    if (isExistUser) {
        return res.status(409).json({ message: '이미 존재하는 아이디입니다.' });
    }

    const hashedPassword = await bcrypt.hash(password, 10);

    const user = await prisma.users.create({
        data: {
            id,
            password: hashedPassword,
        },
    });

    return res.status(201).json({ message: `${id}로 회원가입이 완료되었습니다.` });
});

 

예전에 배운 정규표현식을 이용해 유저한테 입력받은 아이디를 판별햇다.

영어 소문자 + 숫자 조합으로 2자 이상 10자 이하 만 아이디로 등록할 수 있도록 했다.

 

2024.09.10 - [IT] - [IT] 정규표현식

 

[IT] 정규표현식

정규표현식은 어떤 특정한 규칙을 가진 문자열의 집합을 표현하기 위해 사용하는 형식 언어다. 아래 표를 통해 기본적인 정규표현식의 문법을 확인할 수 있다.메타문자기능설명.문자1개의 문

program-yam.tistory.com

 

이번에 찾은 김에 글로 정리했다.

.env 파일에 설정한 환경변수 값을 읽어 오고 싶을 경우 dotenv 패키지를 설치해 읽어오면 편하다.

 

yarn add dotenv

 

위 명령어를 이용해 dotenv 패키지를 설치한다.

 

.env

ACCESS_TOKEN_SECRET_KEY="YamSaeng52"
REFRESH_TOKEN_SECRET_KEY="JungSungWon52"

 

 

.env파일에 위와 같이 선언한 변수 값을 읽어오고 싶을때 아래와 같이 해주면 된다.

import dotenv from "dotenv";

dotenv.config();

const accessTokenSecretKey = process.env.ACCESS_TOKEN_SECRET_KEY;
const refreshTokenSecretKey = process.env.REFRESH_TOKEN_SECRET_KEY;

console.log(accessTokenSecretKey);
console.log(refreshTokenSecretKey);

 

출력 모습

Access Token과 Refresh Token을 발급해보자.

 

# yarn을 이용해 프로젝트를 초기화합니다.
yarn init -y

# express, jsonwebtoken, cookie-parser 패키지를 설치합니다.
yarn add express jsonwebtoken cookie-parser
  • package.json에 "type":"module" 추가

 

app.js ( 기본적인 틀 )

// app.js

import express from 'express';
import jwt from 'jsonwebtoken';
import cookieParser from 'cookie-parser';

const app = express();
const PORT = 3019;

// 비밀 키는 외부에 노출되면 안되겠죠? 그렇기 때문에, .env 파일을 이용해 비밀 키를 관리해야함
const ACCESS_TOKEN_SECRET_KEY = `HangHae99`; // Access Token의 비밀 키를 정의한다.
const REFRESH_TOKEN_SECRET_KEY = `Sparta`; // Refresh Token의 비밀 키를 정의한다.

app.use(express.json());
app.use(cookieParser());

app.get('/', (req, res) => {
  return res.status(200).send('Hello Token!');
});

app.listen(PORT, () => {
  console.log(PORT, '포트로 서버가 열렸어요!');
});

 

 

Refresh Token과 Access Token을 발급하는 API 만들어보기

POST /tokens 의 주소를 갖는 API 생성

// app.js

import express from 'express';
import jwt from 'jsonwebtoken';
import cookieParser from 'cookie-parser';

const app = express();
const PORT = 3019;

// 비밀 키는 외부에 노출되면 안되겠죠? 그렇기 때문에, .env 파일을 이용해 비밀 키를 관리해야함
const ACCESS_TOKEN_SECRET_KEY = `HangHae99`; // Access Token의 비밀 키를 정의한다.
const REFRESH_TOKEN_SECRET_KEY = `Sparta`; // Refresh Token의 비밀 키를 정의한다.

app.use(express.json());
app.use(cookieParser());

app.get('/', (req, res) => {
    return res.status(200).send('Hello Token!');
});

let tokenStorage = {}; // Refresh Token을 저장할 객체

/** Access Token, Refresh Token 발급 API **/
app.post('/tokens', (req, res) => {
    // id 전달
    const { id } = req.body;

    // accessToken 발급
    const accessToken = createAccessToken(id);
    // refreshToken 발급
    const refreshToken = createRefreshToken(id);

    // Refresh Token을 가지고 해당 유저의 정보를 서버에 저장합니다.
    tokenStorage[refreshToken] = {
        id: id, // 사용자에게 전달받은 ID를 저장합니다.
        ip: req.ip, // 사용자의 IP 정보를 저장합니다.

        // 사용자의 User Agent 정보를 저장합니다.
        // 특정 클라이언트가 어떤 방식으로 서버에 요청했는지를 알수 있는 정보
        // 예를 들어 firefox를 이용했는지, chrome을 이용했는지, 모바일로 chrome을 실행했는지 등        
        userAgent: req.headers['user-agent'], 
    };

    //console.log(tokenStorage);

    res.cookie('accessToken', accessToken); // Access Token을 Cookie에 전달한다.
    res.cookie('refreshToken', refreshToken); // Refresh Token을 Cookie에 전달한다.

    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;
}

app.listen(PORT, () => {
    console.log(PORT, '포트로 서버가 열렸어요!');
});

 


 

Refresh Token의 정보를 어디서 관리해야할까?

위 예시에서는 tokenStorage 라는 변수에서 관리했지만, 이는 인 메모리 방식을 사용하기 때문에 서버가 재시작 또는 종료될 경우 모든 정보가 사라지게 된다.

따라서 별도의 테이블에서 Refresh Token을 저장하고 관리해야한다. 

이렇게 하면, Refresh Token 검증 작업을 MySQL과 같은 데이터베이스를 조회함과 동시에 함께 처리할 수 있다.

 

Refresh Tokens Table

Name 타입 ( Type ) NULL default 비고
tokenId ( PK ) INTEGER NOT NULL AUTO_INCREMENT 토큰의 기본 키
userId ( FK ) INTEGER NOT NULL   사용자의 기본 키
token STRING NOT NULL   리프레시 토큰
expiresAt DATETIME NOT NULL   만료 날짜
createdAt DATETIME NOT NULL 현재 시간 생성 날짜
  • 이 외에도 ip 또는 user-Agent와 같은 정보를 추가할 수 있다.

 

Access Token 검증 API

서버에서 발급받은 Access Token을 검증하는 GET /tokens/validate API를 만들어보자.

 

// app. js

...

/** 엑세스 토큰 검증 API **/
app.get('/tokens/validate', (req, res) => {
  const accessToken = req.cookies.accessToken;

  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.json({
    message: `${id}의 Payload를 가진 Token이 성공적으로 인증되었습니다.`,
  });
});

// Token을 검증하고 Payload를 반환합니다.
function validateToken(token, secretKey) {
  try {
    const payload = jwt.verify(token, secretKey);
    return payload;
  } catch (error) {
    return null;
  }
}

 

ValidateToken 함수 추가 설명

ValidaeToken 함수는 제공된 토큰이 유효한지 여부를 검증하는 역할을 담당한다.

  • secretkey를 전달받아, 서버에서 검증할 비밀 키를 설정한다.
  • Access Token 이나 Refresh Token이 우리가 발급한 것인지 검증한다.
  • Access Token 이나 Refresh Token의 만료 여부를 검증한다.

사용자가 Cookie를 전달할 때, Access Token이 없다면 에러가 발생한다.

  • { "message": "Access Token이 존재하지 않습니다." }

사용자가 전달한 Access Token이 유효하지 않을 경우 에러가 발생한다.

  • { "message": "Access Token이 유효하지 않습니다." }

 

사용자가 Access Token을 이용해 API를 호출하면, 해당 토큰의 상태에 따라 적절한 응답 ( Response )이 반환되는 것을 확인 할 수 있다.

Access Token이 정상적으로 인증되면, 응답에서 페이로드 값을 확인할 수 있다.

 


 

Refresh Token으로 Access Token을 재발급하는 API 

서버에서 발급받은 Access Token을 검증하는 POST /tokens/refresh API를 만들어보자.

// app. js

...

/** 리프레시 토큰 검증 API **/
app.post('/tokens/refresh', (req, res) => {
  const refreshToken = req.cookies.refreshToken;

  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이 유효하지 않습니다.' });
  }

  const userInfo = tokenStorage[refreshToken];
  if (!userInfo)
    return res.status(419).json({
      errorMessage: 'Refresh Token의 정보가 서버에 존재하지 않습니다.',
    });

  const newAccessToken = createAccessToken(userInfo.id);

  res.cookie('accessToken', newAccessToken);
  return res.json({ message: 'Access Token을 새롭게 발급하였습니다.' });
});

// Token을 검증하고 Payload를 반환합니다.
function validateToken(token, secretKey) {
  try {
    const payload = jwt.verify(token, secretKey);
    return payload;
  } catch (error) {
    return null;
  }
}

 

사용자가 Cookie를 전달할 때, Refresh Token이 없다면 에러가 발생한다.

  • { "message": "Refresh Token이 존재하지 않습니다." }

사용자가 전달한 Refresh Token이 유효하지 않을 경우 에러가 발생한다.

  • { "message": "Refresh Token이 유효하지 않습니다." }

Refresh Token이 유효하지만, 서버에 해당 토큰 정보가 없을 경우 에러가 발생한다.

  • { "message": "Refresh Token의 정보가 서버에 존재하지 않습니다." }

사용자가 Refresh Token을 이용해 API를 호출하면, 해당 토큰의 상태에 따라 적절한 응답 ( Response )이 반환되는 것을 확인할 수 있다.

만약, Refresh Token이 정상적으로 인증되면, 사용자에게 Access Token을 담은 Cookie를 전달하는 것을 확인할 수 있다.

 


 

 

Access Token

Access Token

 

Access Token은 사용자의 인증( 예) 로그인 )이 완료된 후 해당 사용자를 인증하는 용도로 발급하는 토큰을 말한다.

앞서 쿠키 ( Cookie )에 jwt를 설정하고, 지정된 만료 시간이 지나면 인증이 만료되는 구조 또한 Access Token이라고 부를 수 있다.

인증 요청시 Access Token을 사용하면, 토큰을 생성할 때 사용한 비밀키 ( Secret Key )로 인증을 처리하게 된다.

이 방식은 복잡한 설계나 여러 분기 처리 없이 코드를 구현할 수 있다는 장점을 가지고 있다.

 

Access Token은 Statelss ( 무상태 ) 즉, Node.js 서버가 재시작되더라도 동일하게 작동한다. 이는 jwt를 이용해 사용자의 인증 여부는 확인할 수 있지만, 처음 토큰을 발급한 사용자가 정말 그 사용자인지는 확인할 수 없다.

Access Token은 그 자체로도 사용자 인증에 필요한 모든 정보를 가지고 있다. 그렇기 때문에 토큰을 가지고 있는 시간이 늘어날 수록, 탈취되었을 때 피해 규모는 매우 크다.

토큰이 탈취되었다 하더라도, 서버에서는 해당 토큰이 탈취된 토큰인지 알 수 없고, 강제로 토큰을 만료시킬 수도 없다. 

따라서 서버는 언제나 토큰이 탈취될 수 있다는 가정 하에, 피해를 최소화할 수 있는 방향으로 개발을 진행해야 한다.

 


 

Refresh Token

Refresh Token

 

Refresh Token은 사용자의 모든 인증 정보를 담고 있는 Access Token과는 달리, 특정 사용자가 Access Token을 발급받기 위한 목적으로만 사용된다.

 

Refresh Token은 사용자의 인증 정보를 검증하는데 사용되고, 이를 서버에서 관리한다.

서버는 Refresh Token디코딩해 사용자의 정보를 확인하게 된다. 이 방식은 필요한 경우 서버에서 강제로 토큰을 만료시킬 수 있으며, 사용자의 인증 상태를 언제든지 서버에서 제어할 수 있다는 장점을 가지고 있다.

왜 직접 Access Token을 발급하지 않고, Refresh Token을 통해 Access Token을 발급하는 걸까?

이는 토큰이 탈취당한 경우에 대비해 피해를 최소화하기 위함이다.

일상생활에서 흔히 사용하는 OTP처럼, 사용자의 인증 정보는 짧은 시간동안만 사용되어야 한다. 주기적으로 토큰을 재발급함으로써, 토큰이 유출되더라도 그 피해가 오랜 시간 동안 지속되는 것이 아니라, 짧은 기간 동안만 사용 가능하도록 제한피해를 최소화 할 수 있게 된다.

서버에서는 언제나 토큰이 탈취될 수 있다는 사실을 항상 인지하고, 탈취를 막는 것이 어려운 상황이라면, 탈취된 토큰을 사용할 수 있는 기간을 줄임으로써 피해를 방지해야 한다.

'IT' 카테고리의 다른 글

[IT] Visual Studio Code - 디버그  (0) 2024.09.19
[IT] 정규표현식  (1) 2024.09.10
[IT] 인증, 인가  (0) 2024.09.09
[IT] JWT  (0) 2024.09.08
[IT] 쿠키와 세션  (0) 2024.09.08

일반적으로, 사용자의 비밀번호를 데이터베이스에 저장할 때, 보안을 위해 비밀번호를 평문으로 저장하지 않고 암호화해 저장한다.

 

bcrypt 모듈은 입력받은 데이터를 특정 암호화 알고리즘을 이용해 암호화 및 검증을 도와주는 모듈이다.

aaaa4321 이라는 비밀번호를 bcrypt를 이용해 암호화하면 특정한 문자열로 변환된다.

이 변환된 문자열은 단방향 암호화되어 원래의 비밀번호 ( aaaa4321 )로 복구할 수 없게 된다.

하지만, 입력된 비밀번호가 암호화된 문자열과 일치하는지를 비교할 수 있다.

이를 통해 사용자의 비밀번호가 올바른지, 아닌지 검증할 수 있게 되는것이다.

 

bcrypt 설치

# yarn을 이용해 bcrypt를 설치합니다.
yarn add bcrypt

 

bcrypt 암호화

import bcrypt from 'bcrypt';

const password = 'Sparta'; // 사용자의 비밀번호
const saltRounds = 10; // salt를 얼마나 복잡하게 만들지 결정합니다.

// 'hashedPassword'는 암호화된 비밀번호 입니다.
const hashedPassword = await bcrypt.hash(password, saltRounds);

console.log(hashedPassword); //$2b$10$OOziCKNP/dH1jd.Wvc3JluZVm7H8WXR8oUmxUQ/cfdizQOLjCXoXa

 

 

bcrypt 복호화

import bcrypt from 'bcrypt';

const password = 'Sparta'; // 사용자가 입력한 비밀번호
const hashed = '$2b$10$OOziCKNP/dH1jd.Wvc3JluZVm7H8WXR8oUmxUQ/cfdizQOLjCXoXa'; // DB에서 가져온 암호화된 비밀번호

// 'result'는 비밀번호가 일치하면 'true' 아니면 'false'
const result = await bcrypt.compare(password, hashed);

console.log(result); // true

// 비밀번호가 일치하지 않다면, 'false'
const failedResult = await bcrypt.compare('FailedPassword', hashed);

console.log(failedResult); // false

 

+ Recent posts