[부트캠프] nodeJS - JWT & API서버

RedPanda·2022년 9월 11일
0

NodeJS

목록 보기
10/11

JWT

JWT란?

Json Web Token의 약자로, 토큰으로 웹서버로의 요청을 인증받는 하나의 인증 방식이다.

왜 JWT?

이전에 우리가 다루었던 cookie & session 로그인 방식은 인증 요청을 할 때마다 세션DB에 있는 사용자 정보를 조회해야 했다.
그러나 jwt는 모든 요청에 같이 보내지며, 보내질 때 서버에서는 지정한 암호화 알고리즘으로 요청에 대한 인증을 진행한다.
이 때문에 우리는 DB를 조회하는 번거로운 일을 거치지 않고도 인증할 수 있다.

JWT의 구조

(출처 : https://inpa.tistory.com/entry/WEB-%F0%9F%93%9A-JWTjson-web-token-%EB%9E%80-%F0%9F%92%AF-%EC%A0%95%EB%A6%AC)

jwt는 헤더와 페이로드, 서명으로 구분된다.

Header에는 해시 알고리즘과 토큰의 타입이 들어있고, Payload에는 우리가 원하는 사용자의 정보가 들어있다. Signature에는 Header와 Payload를 Header에 명시된 해시함수를 적용하여 개인키(Private Key)로 서명한 전자서명이 담겨있다.

사용방법

nodeJS에서의 사용법을 설명하고자 한다.

  • jwt 모듈을 npm에서 install 해온다.
npm i jsonwebtoken
  • jwt를 만들어주고 session에 만든 토큰을 저장해준다.
const token = jwt.sign(
  { // Payload
    id: user.id, // User DB에서 확인할 사용자 정보의 id
    nick: user.nick, // 사용자의 닉네임
  },
  process.env.JWT_SECRET,
  { // Signature
    expiresIn: "30m", // 토큰의 유효 시간
    issuer: "nodebirdAPI", // 토큰 제공자의 이름
  }
);
res.session.jwt = token;
return res.json({
  code: 200,
  message: "토큰이 발급되었습니다",
  token,
});
  • Payload에 넣은 값들은 req.decoded 형식으로 모든 요청에 같이 보내진다. 그러므로 요청이 가지고있는 토큰이 유효한지 응답 전에 확인하기 위한 미들웨어를 만들어 사용한다.
const jwt = require("jsonwebtoken");

exports.verifyToken = (req, res, next) => {
  try {
    req.decoded = jwt.verify(req.headers.authorization, process.env.JWT_SECRET);
    return next(); // 토큰이 유효한지 확인하여 다음으로 넘김
  } catch (error) {
    if (error.name === "TokenExpiredError") {
      // 유효기간 초과
      return res.status(419).json({
        code: 419,
        message: "토큰이 만료되었습니다",
      });
    }
    return res.status(401).json({
      code: 401,
      message: "유효하지 않은 토큰입니다",
    });
  }
};
.
.
.
// 다음과 같은 형식으로 요청과 응답 사이에 토큰을 인가해주는 미들웨어 사용
router.post("/follow", verifyToken, async (req, res, next) => {...}

API서버

API 서버의 정의

API(Application Programming Interface)는 다른 어플리케이션에서 현재 프로그램의 기능을 사용할 수 있게 허용하는 접점을 의미하며, 웹 API는 다른 웹 서비스의 기능을 사용하거나 자원을 가져올 수 있는 곳이다.

API 서버는 위와 같은 기능들을 서버에 추가하여 URL을 통해 외부에서 접근할 수 있게 만든 것을 웹 API 서버라고 한다. 이 과정에서 클라이언트의 도메인을 확인하여 인증하고 사용량과 기능 등에 제한을 둘 수 있다.

API는 백-백엔드 서버?

수업 때 '백-백서버, 백-프론트서버, 프론트-어플리케이션' 이라는 말을 사용하셨다. 나름대로 쉽게 이해시키려 사용하신 단어들인 것 같은데 그래도 단박에 이해하기 어려웠다.

예를 들자면, 사용자가 '프론트-어플리케이션'인 UI에서 버튼을 누르면 '백-프론트 서버'인 중간다리 역할을 하는 서버에서 요청을 확인하고 필요한 정보를 '백-백서버'인 API에서 가져온다.

지금껏 예제에서 백엔드 서버와 프론트로 구분했던 우리들에게는 다소 이해하기 어려웠다. 그러나 API가 무엇인지 알고 난 후에는 각자 어떤 역할을 수행하는지 알 수 있는 간단한 설명이었다.

프론트의 요청에 대한 API 서버 만들기

사실 이전에 만들었던 백엔드 사이드와 별반 다를 것이 없다. 단지 프론트 사이드가 없는 웹서버를 구축하는 것이다.

이는 MVC 모델에 근거하여 작성을 할 예정이다.

Models

설계에 따라 DB를 모델링한다. 나는 sequelize를 사용하여 모델을 만들었다.

// User DB(DB 예시)
const Sequelize = require('sequelize');

module.exports = class User extends Sequelize.Model {
  static init(sequelize) {
    return super.init({
      email: {
        type: Sequelize.STRING(40),
        allowNull: true,
        unique: true,
      },
      nick: {
        type: Sequelize.STRING(15),
        allowNull: false,
      },
      password: {
        type: Sequelize.STRING(100),
        allowNull: true,
      },
    }, {
      sequelize,
      timestamps: true,
      underscored: false,
      modelName: 'User',
      tableName: 'users',
      paranoid: true, // 사용자 정보 삭제에 대한 유예 기간을 두기 위한 설정
      charset: 'utf8',
      collate: 'utf8_general_ci',
    });
  }
  // 각각의 연관 관계를 가짐
  static associate(db) {
    db.User.hasMany(db.Post);
    db.User.hasMany(db.Like);
    db.User.hasMany(db.Follow, {foreignKey: 'followerId', targetKey: 'id'});
  }
};

+) 이번에 안 사실인데, sequelize는 연관관계를 맺으면 include를 사용하지 않고 간단한 메소드로 join을 할 수 있다고 한다. 다음 예시는 조인에 대한 설명이다. (출처 - https://sequelize.org/docs/v6/core-concepts/assocs/)

Ship.belongsTo(Captain, { as: 'leader' }); // This creates the `leaderId` foreign key in Ship.

// Eager Loading no longer works by passing the model to `include`:
console.log((await Ship.findAll({ include: Captain })).toJSON()); // Throws an error
// Instead, you have to pass the alias:
console.log((await Ship.findAll({ include: 'leader' })).toJSON());
// Or you can pass an object specifying the model and alias:
console.log((await Ship.findAll({
  include: {
    model: Captain,
    as: 'leader'
  }
})).toJSON());

// Also, instances obtain a `getLeader()` method for Lazy Loading:
const ship = Ship.findOne();
console.log((await ship.getLeader()).toJSON());

Controller

view 부분은 만들지 않기로 했으니 Controller 부분을 만들어보고자 한다.
Controller는 기존에 만들었던 Router와 다르지 않다.

  • 각각의 요청에 대해 라우터를 만들어 준다.
// app.js
const likeRouter = require("./routes/like");
const authRouter = require("./routes/auth");
const postRouter = require("./routes/post");
const userRouter = require("./routes/user");
.
.
.
app.use("/auth", authRouter); // 회원가입, 로그인 등의 기능
app.use("/post", postRouter); // 게시판 CRUD와 팔로우에 대한 기능
app.use("/user", userRouter); // 유저 정보 삭제, 수정 등의 기능
app.use("/like", likeRouter); // 게시판의 좋아요 기능
// /user는 로그인 이후의 작업을 수행하기 때문에 인증 관련 미들웨어를 사용한다. 따라서 auth와 구분지어 설계했다.
  • 라우터 내부에 세부적인 요청에 대한 api들을 만들어준다.
const express = require("express");
const { verifyToken, apiLimiter } = require("./middleware");
const User = require("../models/user");
const { Follow, Post } = require("../models");
const router = express.Router();

// 회원 수정 기능 (닉네임만)
router.post("/update", verifyToken, apiLimiter, async (req, res) => {
  try {
    // req = {inputNick}
    const newname = req.body.inputNick;
    const user = User.findOne({
      where: { id: req.decoded.id }, // jwt에서 가져온 user Id
    });
    if (user) {
      await User.update( // userDB에서 닉네임을 수정하게 한다.
        { nick: newname },
        { where: { id: req.decoded.id } }
      );
      res.json({
        code: 200,
        message: `${newname}으로 수정되었습니다.`,
      });
    }
  } catch (err) {
    res.json({
      code: 400,
      message: `수정할 수 없습니다.`,
    });
  }
});
.
.
.
module.exports = router;

+) API는 외부의 요청에 대한 응답을 해야하기 때문에 json 형식으로 응답하는 것이 적절하다.

  • 위의 API 서버에 대해 외부에서는 다음과 같은 형식으로 요청을 보낸다.
const data = await axios.post("localhost:8001/auth/join", ...);

API 확인하기

위에서 설계한 API는 프론트가 없기 때문에 요청에 대한 응답이 제대로 이루어지고 있는지 확인할 방법이 없다. 그리하여 우리는 Postman이라는 프로그램으로 요청이 잘 이루어지고 있는지 확인할 것이다.

포스트맨 사용하기

상단에 요청 방법을 설정하고 URL을 입력한다.
중단에 원하는 데이터를 json 형식으로 입력하고 Send 버튼을 누르게 되면 하단의 body 부분에 결과가 뜨게 된다.
이때 중단에서 raw와 json 설정을 해주어야 편리하게 이용할 수 있다.

요청에 토큰이 필요하면?

jwt를 이용하여 각 API들을 이용하기 때문에 각 요청에 토큰을 넣어줄 필요가 있었다.
jwt같은 경우에는 토큰을 헤더에 추가하여 요청을 보낸다고 한다. 따라서 Header에 토큰을 추가했다.
방법은 중단에서 Header를 선택하고 Authorization을 추가하여 발급된 토큰을 입력하면 된다고 한다. 아래의 사진을 참고하자.

여담

사실 API에서나 JWT에서나 아직 이해가 부족하기 때문에 내용이 많이 부실한 것 같다.
도메인, CORS, 사용량 제한과 같은 내용도 다루고 싶지만 조금 더 이해를 한 후에 다루는 것이 좋을 것 같다고 생각했다.

어찌됐든 이번 API 서버를 만들어보면서 백엔드에 더 흥미가 생기게 되었다. 더불어 네트워크나 보안 쪽으로 전혀 관심이 없었는데 이번 수업으로 공부를 더 해보면 좋겠다고 생각했다.

profile
끄적끄적 코딩일기

0개의 댓글