Express 에서 SQL 연동 & JWT 사용하기

김태훈·2023년 4월 10일
0

프로젝트1

목록 보기
2/4
post-thumbnail
post-custom-banner

현재 프로젝트에서는 Node.js 를 활용중이며, ES6문법을 사용하고, 이를 Babel을 통해 트랜스컴파일 한다.

1. SQL 연동 및 DB 관련 sql.js

1) mysql2 를 사용한 이유

두기자의 선택지가 있었다.

  • mysql
  • mysql2

이 두가지의 선택지 중에서 mysql2를 사용했고, 이는 Promise 객체를 사용하기 위해서이다.
Promise를 반환하는 async와 await을 적절히 사용하여 callback을 사용하는 mysql 방식을 벗어났다.

2) createPool

// pool 을 사용한 이유 -> Connection 계속 유지하므로 부하 적어짐. (병렬 처리 가능)
const pool = mysql.createPool(
  process.env.JAWSDB_URL ?? {
    host: "3.34.97.140",
    user: "mixbowl",
    database: "Mixbowl",
    password: "swe302841",
    waitForConnections: true,
    connectionLimit: 10,
    queueLimit: 0,
  }
);

createPool을 사용하면, connectionLimit 만큼 미리 connection을 Pool에 만들어 놓는다. 따라서 createConnection이라는 단일 커넥션보다 훨씬 부담이 적을 수 밖에 없다.

  • 매 연결마다 connection을 만들고 종료시키는 비용을 줄인다.
  • 미리 생성된 connection을 사용하므로 DB에 접속해 연결하는 시간을 줄일 수 있다.
  • connection 수를 제어하여, 과부화를 방지한다.

다음은 공식문서이다.
https://www.npmjs.com/package/mysql
Once pool.end is called, pool.getConnection and other operations can no longer be performed. Wait until all connections in the pool are released before calling pool.end. If you use the shortcut method pool.query, in place of pool.getConnection → connection.query → connection.release, wait until it completes.
pool.end calls connection.end on every active connection in the pool. This queues a QUIT packet on the connection and sets a flag to prevent pool.getConnection from creating new connections. All commands / queries already in progress will complete, but new commands won't execute.
즉, pool.query를 사용하면, pool.getConnection을 사용하여 connection.query를 사용한후 connection.release를 사용하는 것보다 훨씬 간편하게 사용할 수 있다. 알아서 connection이 release되기 때문이다.

const promisePool = pool.promise();
이렇게 사용하면, promise 객체를 반환해서 async, await를 쉽게 사용할 수 있다.

3) 내가 정의한 jwt_modules 파일에서 JWT 관련 로직을 DB에 저장하기

해당 작업이 필요한 이유는, Refresh Token을 DB에 저장하기 위해서이다. DB에 refresh token을 담는 것이 heavy 할 수도 있지만, 일단은 그렇게 하기로 했다.

Refresh Token이 필요한 이유

Access Token만으로도 구현할 수 있지만, 이는 보안상으로 문제가 있을 수 있다. 탈취되면 끝이기 때문이다. 따라서 Access Token의 유효기간을 적게 하고, Refresh Token을 하나 더 발급하여서, Access Token의 유효기간이 끝나면 DB에 개인별로 저장된 Refresh Token을 이용하여 새로운 Access Token을 발급한다.

따라서 로그인을 할 때, 두가진 토큰을 발급하는데, Access Token과 Refresh Token 을 발급하여 클라이언트에게 전송한다. 이 때, 서버에서는 DB에 Refresh Token을 저장하는 과정을 거쳐야한다.

loginUser: async (req) => {
    const { nickname, password } = req.body;
    try {
      const [username] = await promisePool.query(`
      SELECT NICKNAME FROM Mixbowl.USER WHERE '${nickname}' = NICKNAME AND '${password}' = PASSWORD ;
      `);
      console.log("in sql", username);
      const accessToken = await jwt_module.sign(username[0]["NICKNAME"]);
      const refreshToken = await jwt_module.refresh();
      //refresh token sql 업데이트
      await promisePool.query(`
        UPDATE USER SET TOKEN = '${refreshToken}' WHERE NICKNAME = '${nickname}';
      `);
      return {
        code: 200,
        message: "토큰이 발급되었습니다.",
        token: {
          accessToken,
          refreshToken,
        },
      };
    } catch (error) {
      console.log(error.message);
    }
  }

2. JWT 설정

0) 디렉토리 정보

  • jwt 디렉토리
    jwt에 관한 함수들이 모여있다. 후술할 내용들이 jwt에 관한 함수들이다.
  • middleware
    사용자의 인증이 필요할 때 사용하는 미들웨어이다. AccessToken과 RefreshToken의 유효성을 파악할 때 사용하는 모듈이 정의되어 있다.

1) sign

Access Token을 발급하는 코드이다.

export function sign(username) {
  // Access 토큰 생성 코드
  const payload = {
    type: "JWT",
    nickname: username,
  };

  return jwt.sign(payload, process.env.SECRET_KEY, {
    expiresIn: "1h",
    issuer: "MixBowl",
  });
}

2) accessVerify

Access Token이 유효한지 확인하는 코드이다.

export function accessVerify(token) {
  //Access 토큰 확인 코드
  let decoded = null;
  try {
    decoded = jwt.verify(token, process.env.SECRET_KEY);
    return {
      ok: true,
      nickname: decoded.nickname[0]["NICKNAME"],
    };
  } catch (error) {
    return {
      ok: false,
      message: error.message,
    };
  }
}

.env에 저장한 SECRET_KEY 와 함께, 클라이언트에서 HTTP HEADER에 보낸 token 정보를 파라미터로 받아와서 verify 코드를 사용하여 해당 JWT를 해석하여

{
  ok:true,
  nickname:~~~
}

를 클라이언트에게 전송한다.

해당 모듈 함수는, 사용자 인증이 필요한 express router에 적용할 미들웨어를 작성하는 함수에 사용된다.
아래는 그 함수이다.

// middleware/checkAccessToken.js
export default (req, res, next) => {
  // 인증 완료
  try {
    //req.headers.authorization = access Token일 경우
    req.decoded = jwt.verify(req.headers.authorization, process.env.SECRET_KEY);
    return next();
  } catch (error) {
    //유효시간 만료
    if (error.name === "TokenExpiredError") {
      return res.status(419).json({
        ok: false,
        message: "토큰이 만료되었습니다.",
      });
    }
    //비밀키 일치 오류
    if (error.name === "JsonWebTokenError") {
      return res.status(401).json({
        ok: false,
        message: "유효하지 않은 토큰입니다",
      });
    }
  }
};

여기에서 verify함수를 호출하면서 req.headers.authorization을 인자로 주어서, Access Token 정보를 받아온다. 그 다음 라우터에게 콜백으로 할 일을 넘긴다.

3) refresh

Refresh Token을 발급하는 함수이다.

export function refresh() {
  // Refresh 토큰 생성 코드
  return jwt.sign({}, process.env.SECRET_KEY, {
    expiresIn: "14d",
    issuer: "MixBowl",
  });
}

이 때, payload에는 아무것도 존재하지 않아도 된다. 단지 기간만 좀 길게 설정했다.

4) refreshVerify

이는 Refresh Token의 유효성을 확인하는 코드이다.
sql 모듈의 getToken함수를 이용하여 username 인자로 들어온 유저의 Token 정보를 찾아 refToken에 저장하여, 이를 클라이언트에서 전송한 Token정보와 비교한다.

//토큰 header에 주고, db 내 refresh 토큰으로 확인
export async function refreshVerify(token, username) {
  //Refresh 토큰 확인 코드
  //redis 도입하면 좋을듯
  try {
    const refToken = await sql.getToken(username);
    if (token === refToken) {
      try {
        jwt.verify(token, process.env.SECRET_KEY);
        return true;
      } catch (error) {
        return false;
      }
    } else {
      return false;
    }
  } catch (error) {
    return false;
  }
}

5) refresh_new

새로운 AccessToken을 발급해주기 위한 함수이다.
클라이언트에서 RefreshToken 과 AccessToken의 정보를 HTTP HEADER에 담아 전송해야한다.

HTTP HEADER에
“authorization” : Access Token 정보
“refresh”: Refresh Token 정보
를 담아 보낸다.

// header에 "authorization", "refresh"에 각각 토큰정보 넣어줄 것
export const refresh_new = async (req, res) => {
  if (req.headers.authorization && req.headers.refresh) {
    const access = req.headers.authorization;
    const refresh = req.headers.refresh;

    const accessResult = accessVerify(access);
    const decodeAccess = jwt.decode(access);

    if (decodeAccess === null) {
      res.status(401).send({
        ok: false,
        message: "No Authorization for Access Token",
      });
    }

    const refreshResult = refreshVerify(refresh, decodeAccess.nickname);

    if (accessResult.ok === false && accessResult.message === "jwt expired") {
      if (refreshResult.ok === false) {
        res.status(401).send({
          ok: false,
          message: "No Authorization, MAKE A NEW LOGIN",
        });
      } else {
        //refresh token이 유효하므로, 새로운 access token 발급
        const newAccessToken = sign(req.body.nickname);

        res.stauts(200).send({
          ok: true,
          nickname: req.body.nickname,
        });
      }
    } else {
      res.status(400).send({
        ok: false,
        message: "Access Token is not expired",
      });
    }
  } else {
    res.status(400).send({
      ok: false,
      message: "Access token and Refresh Token are needed for refresh",
    });
  }
};
profile
기록하고, 공유합시다
post-custom-banner

0개의 댓글