자바스크립트 공부 일지 11

Node.js 숙련 1주차 - JWT, 여러 미들웨어를 이용한 비즈니스 로직 구현해보기

미들웨어

미들웨어의 개념

  • 웹 서버에서 요청을 받을때 가끔 모든 요청에 대해 공통적인 처리를 하고싶은 경우가 생길 수 있습니다. 미들웨어를 통해 웹 서버의 요청/응답에 대해 공통적으로 관리가 가능합니다.
  • 모든 요청에 대해서 로그를 남겨 확인하고 싶은 경우처럼 말이죠. 이 외에도 승인된 사용자만 API를 접근할 수 있게 만들고 싶을때도 해당 될 수 있습니다.
  • 브라우저가 보낸 데이터를 우리가 쉽게 사용할 수 있게 바꿔주는 미들웨어도 존재 bodyParser, cookieParser
  • 보안 연결 방법인 HTTPS를 지원하기 위해서는 https 모듈을 추가하고, 모든 요청과 응답을 기록하고 싶다면 로깅을 해주는 모듈을 추가
js
// form-urlencoded 라는 규격의 body 데이터를 손쉽게 코드에서 사용할 수 있게 도와주는 미들웨어
app.use(express.urlencoded({ extended: false }));
// JSON 이라는 규격의 body 데이터를 손쉽게 코드에서 사용할 수 있게 도와주는 미들웨어에요!
app.use(express.json());

미들웨어 작성

js
app.use((req, res, next) => {
  // 필요한 코드
});
  • req: 요청(Request)에 대한 정보가 담겨있는 객체입니다.

HTTP Headers, Query Parameters, URL 등 브라우저가 서버로 보내는 정보들이 담겨있습니다.

  • res: 응답(Response)을 위한 기능이 제공됩니다.

어떤 HTTP Status Code로 응답 할지, 어떤 데이터 형식으로 응답 할지, 헤더는 어떤 값을 넣어 응답 할지 다양한 기능을 제공합니다.

  • next: 다음 스택으로 정의된 미들웨어를 호출합니다. -> 현재 app.use에서 빠져나와 다음 미들웨어가 존재하면 해당 미들웨어 실행.

[그림 미들웨어]

보통 미들웨어에서는 응답을 직접 처리하지 않고, 특정 로직이 작동하면 next()구문을 통해 실제 응답이 이뤄지는 지점까지를 범위로 삼습니다. Request 로그 남기는 미들웨어 작성

js
app.use((req, res, next) => {
    console.log('Request URL:', req.originalUrl, ' - ', new Date());
    next();
});

사용자 인증 및 로깅, 공통적인 코드를 사용할 시 구현하면 될것같군요.

여러개의 미들웨어가 겹치는 경우 동작하는 방식에 대해 코드로 알아봅시다. 위의 그림과 같습니다.

js
app.use((req, res, next) => {
    console.log('첫번째 미들웨어');
    next();
});

app.use((req, res, next) => {
    console.log('두번째 미들웨어');
    next();
});

app.use((req, res, next) => {
    console.log('세번째 미들웨어');
    next();
});

// print: 첫번째 미들웨어
// print: 두번째 미들웨어
// print: 세번째 미들웨어

아래와 같이 특정 라우터에 관한 미들웨어로도 사용할 수 있습니다.

js
app.use("/api",
  (req, res, next) => {
    console.log("이 라우터에서만 작동하는 미들웨어")
    next();
  },
  [goodsRouter, cartsRouter]
)

라우터와 차이

  • Router와 미들웨어는 서로 다른 방식처럼 보이지만 Router는 미들웨어 기반으로 구현된 객체이므로 미들웨어와 동일한 방식으로 작동 결론적으로 둘다 미들웨어로 보면 됩니다.

대표적인 구문

  • app.use(Middleware) : 모든 요청에서 미들웨어가 실행된다.
  • app.use(’/api’, Middleware) : /api로 시작하는 요청에서 미들웨어를 실행한다.
  • app.post(’/api’, Middleware) : /api로 시작하는 POST 요청에서 미들웨어를 실행한다.

미들웨어와 JWT로 로그인 구현하기

우선 다음과 같이 jsonwebtoken, cookie-parser를 설치합니다.

bash
> npm i jsonwebtoken cookie-parser

사용자로부터 nickname, email, password를 받는다고 보고 구현해봅시다.

회원가입 로직은 다음과 같습니다.

회원가입의 비즈니스 로직 정리하기!

  1. email, nickname, password, confirmPassword를 전달 받음
  2. passwordconfirmPassword가 동일한지 검증
  3. emailnickname값이 이미 DB에 존재하는지 검증
  4. email, nickname, password를 DB에 저장
  5. 회원 가입 성공

password의 경우 암호화를 진행하여 DB에 저장합니다만 우선 뒤로 두고 진행해봅시다.(Node.js의 경우 crypto 라이브러리로 암호화를 진행할 수 있죠. 브라우저에서는 https를 적용하구요.)

js
// users.js

const express = require("express")
const router = express.Router();
const User = require("../schemas/user")

// 회원가입 API
router.post('/users', async(req, res) => {

  const {email, nickname, password, confirmPassword} = req.body;

  // 패스워드, 패스워드 확인 성공
  if(password !== confirmPassword){
    res.status(400).json({
      errorMessage: "패스워드가 패스워드 확인란과 다릅니다.",
    });
    return;
  }

  // email, nickname이 실제로 DB에 존재하는지 확인
  const isExitsUser = await User.findOne({
    $or: [{email}, {nickname}], // 이메일 또는 이메일이 일치할 때 조회 둘다 unique 설정 해두었음.
  });

  if(isExitsUser){
    // 이미 존재하는 계정
    // 민감한 데이터를 가지므로 상세하게 전달하지 않는다.
    res.status(400).json({
      errorMessage: "이메일 또는 닉네임이 이미 사용중입니다."
    });
    return;
  }

  const user = new User({email, nickname, password});
  await user.save(); // DB에 저장한다.

  return res.status(201).json({});

});

module.exports = router;

로그인 API
이메일, 패스워드를 입력받아 데이터베이스에 있는 정보중 이메일, 패스워드가 일치하면 로그인이 성공
로그인에 성공하면 JWT토큰 구성 예시와 같은 방법으로 JWT를 생성하여 반환한다.

로그인 로직은 다음과 같습니다.

로그인의 비즈니스 로직 정리하기!

  1. email, password를 전달 받음
  2. email에 해당하는 사용자가 DB에 존재하는지 검증
  3. 사용자가 존재하지 않거나 사용자와 입력받은 password가 일치하는지 검증
  4. JWT 생성 후 CookieBody로 클라이언트에게 전달
  5. 로그인 성공!
js
// auth.js
const express = require("express");
const router = express.Router();
const User = require("../schemas/user")
const jwt = require("jsonwebtoken")

// 로그인 API
router.post('/auth', async(req, res) => {
  const {email, password} = req.body;

  // 이메일이 일치하는 유저를 찾음
  const user = await User.findOne({email});

  // 1. 이메일과 일치하는 유저가 없거나
  // 2. 유저가 있지만, 유저의 비밀번호와 입력한 비밀번호가 다를 때
  if(!user || user.password !== password ){
    return res.status(400).json({
      errorMessage: "로그인에 실패하였습니다.", 
    })
  }

  // 실제로 담을 데이터, 비밀키
  const token = jwt.sign({userId: user.userId}, "customized-secret-key");;

  // Bearer -> 어떤 타입으로 데이터를 쿠키 값을 전달했는지 타입 지정
  res.cookie("Authorization", `Bearer ${token}`);
  res.status(200).json({token});
});

module.exports = router;

쿠키를 전달 받은 사용자가 정말 쿠키를 가지고 있는지 인증하는 미들웨어를 구현해봅시다.

프론트엔드에서 Authorization: Bearer JWT토큰내용과 같은 양식으로 보낸다고 했을때, HTTP인증 유형 중 Bearer타입을 사용해 토큰을 전달합니다.

Authorization헤더로 전달받는 토큰이 유요한지 검사하고, 만약 유효한 경우 토큰 안에 있는 userId데이터로 해당 사용자가 데이터베이스에 존재하는지 체크합니다. OAuth 2.0 인증으로 발급한 엑세스 토큰이 아니기 때문에 공식적으로 이 방법은 비표준방식으로 볼 수 있습니다. 단, 토큰을 헤더로 교환할 목적으로 사용할 인증 유형이 Bearer가 제일 적절하다는 점에서 많이 사용되는 방식입니다.

이와 같이 사용자 인증 방식을 결정하는 방법에 대해 정리한 글이 있습니다.

백엔드가 이정도는 해줘야 함(Velog) - 5. 사용자 인증 방식 결정

검증 미들웨어로 사용자 인증이 성공했다면 이후 기능을 허가하도록 구현해보자

js
// /middlewares/auth-middleware.js
const jwt = require("jsonwebtoken")
const User = require("../schemas/user")

module.exports = async (req, res, next) => {
  const { Authorization } = req.cookies;
  console.log(req.cookies);
  // Bearer header.payload.signature
  
  // undefined; 로 존재한다면 아래 구문은 에러
  // authorization.split()

  // null 병합 연산 -> authorization 가 undefined라면 빈 문자열
  // Bearer, JWT로 분리
  const [authType, authToken] = (Authorization ?? "").split(" ") // Bearer header.payload.signature를 분리 

  console.log(authType);
  console.log(authToken);
  // 1. Bearer 검증
  // authType === Bearer값인지 확인
  // authToken 검증, authToken(JWT)(쿠키)가 있는지 확인
  if( authType !== "Bearer" || !authToken){
    res.status(400).json({
      errorMessage: "로그인 후에 이용할 수 있는 기능입니다."
    })
    return;
  }

  // 2. authToken 검증
  // authToken이 만료되었는가,
  // authToken이 서버가 발급한 토큰이 맞는지 검증
  // authToken에 있는 userId에 해당하는 사용자가 실제 DB에 존재하는지 확인

  // 서버가 멈추게 되지 않도록 try... catch 문 이용
  try{
    // 검증
    // authToken이 만료되었는가, verify -> 만료 확인
    // authToken이 서버가 발급한 토큰이 맞는지 검증 -> 생성 당시 비밀키가 맞는지 검증
    const {userId} = jwt.verify(authToken, "customized-secret-key");

    // authToken에 있는 userId에 해당하는 사용자가 실제 DB에 존재하는지 확인
    const user = await User.findOne({userId})

    // 다음 미들웨어에 넘기기 위해. user 정보 전달
    res.locals.user = user;

    // 다음 미들웨어로 보낸다.
    next();
  }catch(err){
    // 사용자 인증에 실패한 이유
    console.error(err);

    res.status(400).json({
      errorMessage: "로그인 후에 이용할 수 있는 기능입니다."
    })
    
    return;
  }
}

내 정보 조회구현하기

우선 이 API는 사용자가 토큰을 헤더에 담아 보내야 동작하는 API입니다. 로그인을 해야 "내 정보"를 조회할 수 있게 되죠.

js
// users.js


// 내 정보 조회 API -> email, nickname 반환
// authMiddleware -> next() -> async(req, res)
const authMiddleware = require("../middlewares/auth-middleware")

router.get("/users/me", aut hMiddleware, async(req, res) => {
  const {email, nickname} = res.locals.user;

  res.status(200).json({
    user: {
      email,
      nickname
    }
  });
})