10장

마조리카·2021년 5월 10일
0

외부 서버서버에서 API를 이용해서 데이터 가져오기.

API란

Applaication Programming Interface 약자

  • 다른 애플리케이션에서 현재 프로그램의 기능을 사용 할 수 있게 한다.
  • 웹 API : 다른 웹 서비스의 기능을 사용하거나 자원을 가져올 수 있게 해준다.
  • 다른 사람들에게 정보를 제공하고 싶은 부분만 API를 열고, 제공하고 싶지 않은 부분을 만들지 않으면 된다.
  • API에 제한을 걸어 일정 횟수 내에만 가져가게 할 수 있다.
  • NodeBird 에서는 인증된 사용자에게만 정보 제공이 가능하게 구현함.
  • 누군가 api 요청을 너무 많이 요청하면 서버가 터져버릴 수 있으므로 메인서버랑 api 서버는 분리해 두는것이 좋다.

프로젝트 만들기

nodebird-api 폴더를 만들고 npm init 설정

npm i bcrypt cookie-parser dotenv express express-session morgan mysql2 nunjucks passport passport-local sequelize uuid passport-kakao
npm i -D nodemon

이전 프로젝트에서 config, models, passport 모두 복사하여 nodebird-api에 복사.
routes 폴더에서 auth.js, middlewares.js 복사
.env도 복사

api.js 작성

const express = require('express');
const path = require('path');
const cookieParser = require('cookie-parser');
const passport = require('passport');
const morgan = require('morgan');
const session = require('express-session');
const nunjucks = require('nunjucks');
const dotenv = require('dotenv');

dotenv.config();
const authRouter = require('./routes/auth');
const indexRouter = require('./routes');
const { sequelize } = require('./models');
const passportConfig = require('./passport');

const app = express();
passportConfig();
app.set('port', process.env.PORT || 8002); //nodebird - 8001, nodebird-api - 8002, nodecat - 8003
app.set('view engine', 'html');
nunjucks.configure('views', {
  express: app,
  watch: true,
});
sequelize.sync({ force: false })
  .then(() => {
    console.log('데이터베이스 연결 성공');
  })
  .catch((err) => {
    console.error(err);
  });

app.use(morgan('dev'));
app.use(express.static(path.join(__dirname, 'public')));
app.use(express.json());
app.use(express.urlencoded({ extended: false }));
app.use(cookieParser(process.env.COOKIE_SECRET));
app.use(session({
  resave: false,
  saveUninitialized: false,
  secret: process.env.COOKIE_SECRET,
  cookie: {
    httpOnly: true,
    secure: false,
  },
}));
app.use(passport.initialize());
app.use(passport.session());

app.use('/auth', authRouter);
app.use('/', indexRouter);

app.use((req, res, next) => {
  const error =  new Error(`${req.method} ${req.url} 라우터가 없습니다.`);
  error.status = 404;
  next(error);
});

app.use((err, req, res, next) => {
  res.locals.message = err.message;
  res.locals.error = process.env.NODE_ENV !== 'production' ? err : {};
  res.status(err.status || 500);
  res.render('error');
});

app.listen(app.get('port'), () => {
  console.log(app.get('port'), '번 포트에서 대기중');
});

도메인 모델 생성

models/domain.js

  • API를 사용할 도메인을 저장하는 모델
//models/domain.js
const Sequelize = require('sequelize');
//mysql에 도메인이라는 테이블 생성
module.exports = class Domain extends Sequelize.Model {
  static init(sequelize) {
    return super.init({
      host: { //웹 주소 입력 받기 위함
        type: Sequelize.STRING(80),
        allowNull: false,
      },
      type: { //요금제 구분하기 위함
        type: Sequelize.ENUM('free', 'premium'),
        allowNull: false,
      },
      clientSecret: { //REST API KEY 같은거 입력 받기 위함
        type: Sequelize.STRING(36), //이렇게해도 되고 옳바른 uuid인지까지 검사하고 싶다면 Sequelize.UUID를 하면 된다.
        allowNull: false,
      },
    }, {
      sequelize,
      timestamps: true,
      paranoid: true,
      modelName: 'Domain',
      tableName: 'domains',
    });
  }

  static associate(db) {
    db.Domain.belongsTo(db.User);
  }
};
  • API 서버는 다름 서드 파티 서브에서 노드버드 데이터를 가져가는데 제한을 둘 수 있다.
    제한을 두려면 가져가는 사람이 누군지 알아야 한다.
  • 그래서 우리는 서비스에 로그인을 하도록 만들고 부가적으로 도메인이라는걸 만들라 시킬것이다.
  • clientSecret이 Key를 발급해준다.
  • Enum은 STRING(10) 같은걸 해도 되는데 이건 타입이 열글자 이내 모든 문자열이 가능하지만 좀 더 상세하게 하고 싶을 때 enum을 쓴다.
  • Enum('free', 'premium')을 쓰면 두 개 중 하나만 넣을 수 있다. Rare같은 문자열을 사용하면 에러가 난다.
  • Type은 요금제 같은 것을 부과할 때 free인지 primium인지 구분하기 위해 사용한다.

도메인 등록 라우터 생성하기

routes/index.js에서 도메인 등록 라우터 생성

  • uuid 패키지로 사용자가 등록한 도메인에 고유한 비밀번호 부여
  • uuid는 충돌 위험은 있지만 매우 희박
  • 비밀번호가 일치하는 요청만 API 응답
/routes/index.js
const express = require('express');
const { v4: uuidv4 } = require('uuid'); //v1,v2 ~ 있는데 v1 아니면 v4를 사용함. 버전마다 특성들이 있는데 그건 검색해보기
const { User, Domain } = require('../models');
const { isLoggedIn } = require('./middlewares');

const router = express.Router();

router.get('/', async (req, res, next) => { //localhost:8002로 접속을 하면 login.html이 실행되면서 이 부분(try부분)이 실행된다.
  //로그인해야만 domain 정보 불러오게끔
  try {
    const user = await User.findOne({
      where: { id: req.user && req.user.id || null },
      include: { model: Domain },
    });
    res.render('login', {
      user,
      domains: user && user.Domains,
    });
  } catch (err) {
    console.error(err);
    next(err);
  }
});

router.post('/domain', isLoggedIn, async (req, res, next) => {
  try {
    await Domain.create({
      UserId: req.user.id,
      host: req.body.host,
      type: req.body.type,
      clientSecret: uuidv4(), //key 같은거 만들어줌. 발급된 키가 중복될 가능성이 거의 없다.
    });
    res.redirect('/');
  } catch (err) {
    console.error(err);
    next(err);
  }
});

module.exports = router;

도메인 등록하고 비밀키 발급 받기

  • localhost:8002 접속하여 로그인하여 도메인 발급

JWT 토큰으로 인증하기

NodeBird 가 아닌 다른 클라이언트가 데이터를 가져가게 하려면 인증 과정이 필요한데 그 과정을 JWT 토큰을 활용하여 한다.

헤더, 페이로드, 시그니처로 구성되어 있다.
  • 헤더 : 토큰의 종류와 해시 알고리즘 정보가 들어있다.
  • 페이로드 : 토큰의 내용물이 인코딩된 부분
  • 시그니처 : 일련의 문자열로, 시그니처를 통해 토큰이 변조되었는지 여부 확인 가능하다.

주의점

  • JWT에 민감한 내용을 넣으면 안된다. (페이로드 내용 볼 수 있다.)
  • 그럼에도 사용하는 이유는 토큰 변조가 불가능하고, 내용물이 들어있으므로 데이터 베이스 조회를 하지 않을 수 있다.
  • 대신 노출되도 좋은 정보만 넣어야한다.
  • 용량이 커서 네트워크 요청시 데이터양이 증가한다는 단점이 있다.

JWT 토큰 사용하기

npm i jsonwebtoken
.env 파일에 JWT_SECRET=jwtSecret 추가

JWT토큰을 검사하는 verfyToken 미들웨어 작성

//routes/middleware.js

const jwt = require('jsonwebtoken');

exports.isLoggedIn = (req, res, next) => {
  if (req.isAuthenticated()) {
    next();
  } else {
    res.status(403).send('로그인 필요');
  }
};

exports.isNotLoggedIn = (req, res, next) => {
  if (!req.isAuthenticated()) {
    next();
  } else {
    res.redirect('/');
  }
};

exports.verifyToken = (req, res, next) => {
  try {//만약 JWT 토큰을 해커가 위조한다면 verify가 되지 않으므로 보안에 이득이 된다.
    req.decoded = jwt.verify(req.headers.authorization, process.env.JWT_SECRET); //용도에 따라 session을 사용해도 된다.
    //JWT 토큰은 req.headers.authorization에 들어 있음
    //req.decoded에 페이로드를 넣어 다음 미들웨어에서 쓸 수 있게 함


    return next();
  } catch (error) {
    if (error.name === 'TokenExpiredError') { // 유효기간 초과
      return res.status(419).json({
        code: 419,
        message: '토큰이 만료되었습니다',
      });
    }
    return res.status(401).json({ //아예 누군가 위조한 토큰인지 검사해줌.
      code: 401,
      message: '유효하지 않은 토큰입니다',
    });
  }
};

JWT 토큰 발급 라우터 만들기

//routes/v1.js
//누군가 API를 사용하기 위해 요청을 보냈을 때 처리를 하기 위한 라우터들임. (version 1의 v1)
const express = require('express');
const jwt = require('jsonwebtoken');

const { verifyToken } = require('./middlewares');
const { Domain, User } = require('../models');

const router = express.Router();

router.post('/token', async (req, res) => { //토큰을 발급해주는 라우터
  const { clientSecret } = req.body;
  try {
    const domain = await Domain.findOne({ //도메인 등록했나 검사
      where: { clientSecret },
      include: {
        model: User,
        attribute: ['nick', 'id'],
      },
    });
    if (!domain) {
      return res.status(401).json({
        code: 401,
        message: '등록되지 않은 도메인입니다. 먼저 도메인을 등록하세요',
      });
    }
    const token = jwt.sign({ //토큰 발급해줌
      id: domain.User.id,
      nick: domain.User.nick,
    }, process.env.JWT_SECRET, {
      expiresIn: '1m', // 1분
      issuer: 'nodebird',
    });
    return res.json({
      code: 200,
      message: '토큰이 발급되었습니다',
      token,
    });
  } catch (error) {
    console.error(error);
    return res.status(500).json({
      code: 500,
      message: '서버 에러',
    });
  }
});

router.get('/test', verifyToken, (req, res) => { //토큰을 제대로 발급했는지 테스트해주는 라우터
  res.json(req.decoded);
});

module.exports = router;
  • 버전을 배포 후애는 라우터를 함부로 수정하면 안된다.
    수정사항이 있을시 버전을 올려야 함
  • POST/token에서 JWT를 발급해줌.
  • 먼저 도메인 검사 후 등록된 도메인이면 jwt.sign 메서드로 JWT 토큰을 발급해줌.
  • 첫 번쨰 인수로 페이토드를 넣고 두번째 인수로 JWT 비밀키, 세번쨰 인수로 토큰 옵션을 넣어준다.
    (expiresln 은 만료시간, issuer은 발급자)
  • expiresln은 1m = 60 * 1000 같은 밀리초 단위도 가능
  • GET/test 라우터에서 토큰 인증 테스트 가능하다.
  • 라우터는 응담을 일정한 형식으로 해야 사용자들이 헷갈리지 않는다.

app.js에 라우터 연결하기

...
const dotenv = require('dotenv');

dotenv.config();
const v1 = require('./routes/v1');
const authRouter = require('./routes/auth');
...
app.use(passport.session());

app.use('/v1', v1);
app.use('/auth', authRouter);
...

JWT 토큰으로 로그인 하기

세션 쿠키 대신 JWT 토큰을 쿠키로 발급하면 된다.

  • Authentcate 메서드의 두 번째 인수로 옵션을 주면 세션을 사용하지 않는다.
...
router.post('/login', isNotLoggedIn, (req, res, next) => {
  passport.authenticate('local', {session: false }, (authError, user, info) => { 
    if (authError) {
 ...

클라이언트에서 JWT 토큰을 사용하고 싶다면

  • process.env.JWT_SECRET은 클라이언트에서 노출되면 안됨

  • RSA같은 양방향 비대칭 암호화 알고리즘을 사용해야함

  • JWT는 PEM 키를 사용해서 양방향 암호화를 하는것을 지원함

    API 호출하는 서버 만들기

    프로젝트 구조 갖추기

  • nodecat 폴도를 만들고 package.json 설정

  • 위에서 도메인 등록 후 발급 받은 비밀키를 .env 파일에 저장한다.

  • 필요한 패키지 설치

  • app.js 작성

const express = require('express');
const morgan = require('morgan');
const cookieParser = require('cookie-parser');
const session = require('express-session');
const nunjucks = require('nunjucks');
const dotenv = require('dotenv');

dotenv.config();
const indexRouter = require('./routes');

const app = express();
app.set('port', process.env.PORT || 4000);
app.set('view engine', 'html');
nunjucks.configure('views', {
  express: app,
  watch: true,
});

app.use(morgan('dev'));
app.use(cookieParser(process.env.COOKIE_SECRET));
app.use(session({
  resave: false,
  saveUninitialized: false,
  secret: process.env.COOKIE_SECRET,
  cookie: {
    httpOnly: true,
    secure: false,
  },
}));

app.use('/', indexRouter);

app.use((req, res, next) => {
  const error =  new Error(`${req.method} ${req.url} 라우터가 없습니다.`);
  error.status = 404;
  next(error);
});

app.use((err, req, res, next) => {
  res.locals.message = err.message;
  res.locals.error = process.env.NODE_ENV !== 'production' ? err : {};
  res.status(err.status || 500);
  res.render('error');
});

app.listen(app.get('port'), () => {
  console.log(app.get('port'), '번 포트에서 대기중');
});

토큰 테스트용 라우터 만들기

routes/index.js 생성하기

  • GET/test 에 접근 시 세션 검사
    세션에 토큰이 저장되어 있지 않으면 POST http://localhost:8002/v1/token 라우터로 부터 토큰 발급
  • 이 떄 HTTP 요청 본문에 클라이언트 비밀키 동봉
  • 발급에 성공했다면 발급받은 토큰으로 다시 GET https://localhost:8002/v1/test 라우터 접근해서 토큰 테스트
//routes/index.js
const express = require('express');
const axios = require('axios');

const router = express.Router();

router.get('/test', async (req, res, next) => { // 토큰 테스트 라우터
  try {
    if (!req.session.jwt) { // 세션에 토큰이 없으면 토큰 발급 시도 (처음에는 세션에 토큰이 안들어 있을테니 !false라서 발급해줌)
      const tokenResult = await axios.post('http://localhost:8002/v1/token', { //토큰을 발급 받을 땐 v1/token이란 라우터에 요청을 보내면서
        clientSecret: process.env.CLIENT_SECRET, //CLIENT_SECRET을 같이 넣어 보내준다.
      });
      if (tokenResult.data && tokenResult.data.code === 200) { // 토큰 발급 성공
        req.session.jwt = tokenResult.data.token; // 토큰을 발급 받으면 tokenResult.data.token에 저장됨 그걸 세션에 토큰 저장(유효 기간 동안만)
      } else { // 토큰 발급 실패
        return res.json(tokenResult.data); // 발급 실패 사유 응답
      }
    }
    // 발급받은 토큰 테스트
    const result = await axios.get('http://localhost:8002/v1/test', {
      headers: { authorization: req.session.jwt }, //방금 발급 받은 후 세션에 넣었던 토큰을 header authorization에 넣어서 api 서버에 테스트 해보는거 -> v1.js으로 이동
    });
    return res.json(result.data); //
  } catch (error) {
    console.error(error);
    if (error.response.status === 419) { // 토큰 만료 시
      return res.json(error.response.data);
    }
    return next(error);
  }
});

module.exports = router;

요청 보내기

npm start 로 서버를 시작한 뒤 http://localhost:4000/test에 접속한다.


sns api 서버 만들기

nodebird 데이터 제공하기

nodebird-api 라우터 작성

  • 위에서 만들었던 v1.js 부가 설명
//routes/v1.js
const express = require('express');
const jwt = require('jsonwebtoken');

const { verifyToken } = require('./middlewares');
const { Domain, User } = require('../models');

const router = express.Router();

//nodecat의 const tokenResult = await axios.post('http://localhost:8002/v1/token' 이게 실행되면 아래의 라우터가 실행됨.
router.post('/token', async (req, res) => {
  const { clientSecret } = req.body;
  try {
    const domain = await Domain.findOne({ //도메인에 clientSecret이 등록되어 있는지 검사
      where: { clientSecret },
      include: {
        model: User,
        attribute: ['nick', 'id'],
      },
    });
    if (!domain) { //등록되어 있지 않을 때 실행
      return res.status(401).json({
        code: 401,
        message: '등록되지 않은 도메인입니다. 먼저 도메인을 등록하세요',
      });
    }
    const token = jwt.sign({ //등록 되어 있으면) 토큰 발행해줌
      id: domain.User.id,
      nick: domain.User.nick,
    }, process.env.JWT_SECRET, { //털리면 안됨, 시그니처를 만들어주고 위조를 검사해주는거!
      expiresIn: '1m', // 유효기간 1분 - 토큰은 JWT_SECRET가 틀려도 error, 유효기간이 끝나도 error
      issuer: 'nodebird', //누가 발급해 준건지 나타냄 - 위조 방지
    });
    return res.json({ //위에선 이런저런 정보 넣어주고 여기서 발급!
      code: 200,
      message: '토큰이 발급되었습니다',
      token,
    });
  } catch (error) {
    console.error(error);
    return res.status(500).json({
      code: 500,
      message: '서버 에러',
    });
  }
});

router.get('/test', verifyToken, (req, res) => { //nodecat에서 테스트 요청이 들어오면 verifyToken이 실행될 때 middlewares.js을 가서 verifytoken을 한다.
  res.json(req.decoded); //req.decoded는 middlewares.js의 verifyToken에서 나온거임
  //req.decoded를 axios 요청 보내면 index.js의 result.data가 된다.
});

module.exports = router;

nodebird 데이터 가져오기

nodecat의 라우터 작성

  • 토큰을 발급받고 요청을 보내는 부분을 request 함수로 만들어 둠
  • 요청은 axios로 보내고 세션 토큰 검사, 재발급까지 같이 수행

위에서만든 routes/index.js 수정

//routes/index.js
const express = require('express');
const axios = require('axios');

const router = express.Router();
const URL = 'http://localhost:8002/v1'; //nodebird-api의 v1 라우터로 요청을 보낸다.

axios.defaults.headers.origin = 'http://localhost:4000'; // origin 헤더 추가 -> origin에 nodecat 서버 주소를 넣어 놓는다. -> nodebird-api쪽에서 어디서 요청이 왔는지를 headers.origin보고 판단을 하는데 이게 안들어 있으면 어디서 왔는지 몰라서 요청을 거절할 수 있다.
//브라우저에서 서버로 요청을 보낼 때는 origin을 넣어서 보내주기도 하는데 서버에서 서버로 보내주면 안넣어주는 경우도 많아서 일부로 넣어줌.

const request = async (req, api) => {
  try {
    if (!req.session.jwt) { // 세션에 토큰이 없으면
      const tokenResult = await axios.post(`${URL}/token`, {
        clientSecret: process.env.CLIENT_SECRET,
      });
      req.session.jwt = tokenResult.data.token; // 세션에 토큰 저장
    }
    return await axios.get(`${URL}${api}`, { //토큰 발급 성공했으면 토큰 저장 후 원래 보내고 싶었던 api 주소로 요청을 보낸다.
      headers: { authorization: req.session.jwt },
    }); // API 요청
  } catch (error) {
    if (error.response.status === 419) { // 토큰 만료시 토큰 재발급 받기
      delete req.session.jwt;
      return request(req, api); //맨 위의 request 함수를 호출한거임 / 바로 위에서 토큰을 삭제해주었기 때문에 새로 발급됨
    } // 419(토큰 만료) 외의 다른 에러면
    return error.response;
  }
};

router.get('/mypost', async (req, res, next) => {
  try {
    const result = await request(req, '/posts/my'); //여기에 넣은 주소는 api서버의 주소임(v1에 있는 주소)
    res.json(result.data);
  } catch (error) {
    console.error(error);
    next(error);
  }
});

router.get('/search/:hashtag', async (req, res, next) => {
  try {
    const result = await request(
      req, `/posts/hashtag/${encodeURIComponent(req.params.hashtag)}`, //주소 부분은 한글일 수 있으니까 encodeURIComponent한거
    ); //req.params.~로 주소 부분의 프로퍼티를 가져올 수 있다.(위의 /:hashtag 이거)
    res.json(result.data);
  } catch (error) {
    if (error.code) {
      console.error(error);
      next(error);
    }
  }
});

module.exports = router;

실제 요청 보내기

localhost:4000/mypost에 접속하면 게이글 받아옴(nodebird 서비스에 게시글이 있어야함

localhost:4000/search/노드 라우터에 접속하면 노드 해시태크 검색

사용량 제한하기

DOS 공격 등을 대비해야 함

nodebird-api 프로젝트에서 npm i express-rate-limit을 해준다.

apiLimiter 미들웨어 추가

//nodebird-api/routes/middlwares.js
const jwt = require('jsonwebtoken');
const RateLimit = require('express-rate-limit');

exports.isLoggedIn = (req, res, next) => {
  if (req.isAuthenticated()) {
    next();
  } else {
    res.status(403).send('로그인 필요');
  }
};

exports.isNotLoggedIn = (req, res, next) => {
  if (!req.isAuthenticated()) {
    next();
  } else {
    res.redirect('/');
  }
};

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: '유효하지 않은 토큰입니다',
    });
  }
};

exports.apiLimiter = new RateLimit({ //몇분간 몇번을 사용했는지 체크해주는 미들웨어임
  windowMs: 60 * 1000, // 1분(60*1000밀리초)
  max: 10, //1분간 최대 10번
  delayMs: 0, // 만약 1000이라면 요청간의 간격이 1초라는거
  handler(req, res) {
    res.status(this.statusCode).json({
      code: this.statusCode, // 기본값 429 -> 할당량을 넘은 경우
      message: '1분에 열 번까지 요청할 수 있습니다.',
    });
  },
});

exports.deprecated = (req, res) => { //더 이상 이 api 사용하지마라.라는 것을 알려주는 미들웨어
  res.status(410).json({
    code: 410,
    message: '새로운 버전이 나왔습니다. 새로운 버전을 사용하세요.',
  });
};
  • deprecates 미들웨어는 사용하면 안 되는 라우터에 붙여서 사용 시 경고문을 띄어준다.

응답코드 정리

새 라우터 버전 업데이트

사용량 제한 기능이 추가되어 기존 API와 호환되지 않는다.

  • apiLimiter 추가된 v2.js를 만들고 v1.js에는 deprecated 처리해준다.
//routes/v1.js
const express = require('express');
const jwt = require('jsonwebtoken');

const { verifyToken, deprecated } = require('./middlewares'); //진짜 수정해야 되는 상황이 생기면 deprecated로 새로운 버전이 있음을 알려줌
const { Domain, User, Post, Hashtag } = require('../models');

const router = express.Router();

//모든 라우터들에게 공통적으로 적용되면 이렇게 쓰는거 알쥐
router.use(deprecated); //이런식으로 쓰면
//v1.js의 라우터들 실행될 때
//middlewaresjs의 exports.deprecated 이 부분 실행됨

router.post('/token', async (req, res) => { 
  const { clientSecret } = req.body;
  try {
    const domain = await Domain.findOne({
      where: { clientSecret },
      include: {
        model: User,
        attribute: ['nick', 'id'],
      },
    });
    if (!domain) {
      return res.status(401).json({
        code: 401,
        message: '등록되지 않은 도메인입니다. 먼저 도메인을 등록하세요',
      });
    }
    const token = jwt.sign({
      id: domain.User.id,
      nick: domain.User.nick,
    }, process.env.JWT_SECRET, {
      expiresIn: '1m', // 1분
      issuer: 'nodebird',
    });
    return res.json({
      code: 200,
      message: '토큰이 발급되었습니다',
      token,
    });
  } catch (error) {
    console.error(error);
    return res.status(500).json({
      code: 500,
      message: '서버 에러',
    });
  }
});

router.get('/test', verifyToken, (req, res) => {
  res.json(req.decoded);
});

router.get('/posts/my', verifyToken, (req, res) => {
  Post.findAll({ where: { userId: req.decoded.id } })
    .then((posts) => {
      console.log(posts);
      res.json({
        code: 200,
        payload: posts,
      });
    })
    .catch((error) => {
      console.error(error);
      return res.status(500).json({
        code: 500,
        message: '서버 에러',
      });
    });
});

router.get('/posts/hashtag/:title', verifyToken, async (req, res) => {
  try {
    const hashtag = await Hashtag.findOne({ where: { title: req.params.title } });
    if (!hashtag) {
      return res.status(404).json({
        code: 404,
        message: '검색 결과가 없습니다',
      });
    }
    const posts = await hashtag.getPosts();
    return res.json({
      code: 200,
      payload: posts,
    });
  } catch (error) {
    console.error(error);
    return res.status(500).json({
      code: 500,
      message: '서버 에러',
    });
  }
});

module.exports = router;
//routes/v2.js
const express = require('express');
const jwt = require('jsonwebtoken');

const { verifyToken, apiLimiter } = require('./middlewares');
const { Domain, User, Post, Hashtag } = require('../models');

const router = express.Router();

router.post('/token', apiLimiter, async (req, res) => { 
  const { clientSecret } = req.body;
  try {
    const domain = await Domain.findOne({
      where: { clientSecret },
      include: {
        model: User,
        attribute: ['nick', 'id'],
      },
    });
    if (!domain) {
      return res.status(401).json({
        code: 401,
        message: '등록되지 않은 도메인입니다. 먼저 도메인을 등록하세요',
      });
    }
    const token = jwt.sign({
      id: domain.User.id,
      nick: domain.User.nick,
    }, process.env.JWT_SECRET, {
      expiresIn: '30m', // 30분
      issuer: 'nodebird',
    });
    return res.json({
      code: 200,
      message: '토큰이 발급되었습니다',
      token,
    });
  } catch (error) {
    console.error(error);
    return res.status(500).json({
      code: 500,
      message: '서버 에러',
    });
  }
});

router.get('/test', verifyToken, apiLimiter, (req, res) => {
  res.json(req.decoded);
});

router.get('/posts/my', apiLimiter, verifyToken, (req, res) => {
  Post.findAll({ where: { userId: req.decoded.id } })
    .then((posts) => {
      console.log(posts);
      res.json({
        code: 200,
        payload: posts,
      });
    })
    .catch((error) => {
      console.error(error);
      return res.status(500).json({
        code: 500,
        message: '서버 에러',
      });
    });
});

router.get('/posts/hashtag/:title', verifyToken, apiLimiter, async (req, res) => { //토큰을 검사하고 apiLimit을 할건지 apiLimit을 먼저 하고 토큰을 검사할건지 순서는 서비스에 따라 결정하면 됨. (순서가 유의미하기 때문에 app.use로 안함)
  try {
    const hashtag = await Hashtag.findOne({ where: { title: req.params.title } });
    if (!hashtag) {
      return res.status(404).json({
        code: 404,
        message: '검색 결과가 없습니다',
      });
    }
    const posts = await hashtag.getPosts();
    return res.json({
      code: 200,
      payload: posts,
    });
  } catch (error) {
    console.error(error);
    return res.status(500).json({
      code: 500,
      message: '서버 에러',
    });
  }
});

module.exports = router;

새 라우터 실행해보기

nodecat의 버전 v2로 바꾸기

//nodecat/routes/index.js
const express = require('express');
const axios = require('axios');

const router = express.Router();
const URL = 'http://localhost:8002/v2'; //v1이 deprecated 되었기 때문에 v2로 바꿔준거

...

v1의 API를 사용하거나 사용량을 초과하면 에러 발생


CORS 이해하기

[CORS 에러 예시]

  • 브라우저에서 서버로 요청을 보낼 때 도메인이 다를 경우에만 발생하는 에러이다.
    (localhost:4000에서 localhst:4000으로 보낼때는 발생하지 않음)
  • CORS 에러는 브라우저가 발생시킨다. 보안상 그렇게 설계

CORS 에러 해결법

1. CORS 모듈 설정하기

Response header에 Acess-Control-Allow-Origin을 넣어주면된다.

npm i cors
//routes/v2.js
const express = require('express');
const jwt = require('jsonwebtoken');
const cors = require('cors');
const url = require('url');

const { verifyToken, apiLimiter } = require('./middlewares');
const { Domain, User, Post, Hashtag } = require('../models');

const router = express.Router();

//CORS 해결하기
router.use((req, res, next) => { //미들웨어 확장 패턴 -> 장점 : 회원가입 되어 있는 도메인인지 검사를 할 수 있다.
  const domain = await Domain.findOne({
    where: { host: url.parse(req.get('origin'))?.host }
     //req.get('origin')은 req의 헤더 안에 있는 origin 가져오는거임. 
     //origin은 요청 보낼 때(nodecat의 /routes/index.js에서) 설정해줬었음. 
     //그래서 origin 설정을 안해주면 origin이 없다고 에러를 뿜을 수 있기 때문에 nodecat에서 origin 설정해준거임.
  });//?은 옵셔널 체이닝이라는 문법임. 앞에 것이 undefind면 그대로 undefind이고 객체이면 그 객체안에서 host를 꺼내줌.
  
  if(domain) { //중요! 회원가입한 사람들의 도메인에서 요청이 온 경우에만 허용
    cors({
      origin: true, //모든 주소를 허용하면 보안상 좋지 않음. 
      //-> 그래서 미들웨어 확장 패턴을 사용하여
      //조건문을 통해 회원가입 되어 있는 도메인인지 검사를 거치게 함.
      
      credentials: true, //true를 해야 프런트와 백엔드 간에 쿠키가 공유됨
    })(req, res, next);
  } else {
    next();
  }
})

router.use(async (req, res, next) => {
  const domain = await Domain.findOne({
    where: { host: url.parse(req.get('origin')).host },
  });
  if (domain) {
    cors({
      origin: req.get('origin'),
      credentials: true,
    })(req, res, next);
  } else {
    next();
  }
});

router.post('/token', apiLimiter, async (req, res) => {
  const { clientSecret } = req.body;
  try {
    const domain = await Domain.findOne({
      where: { clientSecret },
      include: {
        model: User,
        attribute: ['nick', 'id'],
      },
    });
    if (!domain) {
      return res.status(401).json({
        code: 401,
        message: '등록되지 않은 도메인입니다. 먼저 도메인을 등록하세요',
      });
    }
    const token = jwt.sign({
      id: domain.User.id,
      nick: domain.User.nick,
    }, process.env.JWT_SECRET, {
      expiresIn: '30m', // 30분
      issuer: 'nodebird',
    });
    return res.json({
      code: 200,
      message: '토큰이 발급되었습니다',
      token,
    });
  } catch (error) {
    console.error(error);
    return res.status(500).json({
      code: 500,
      message: '서버 에러',
    });
  }
});

router.get('/test', verifyToken, apiLimiter, (req, res) => {
  res.json(req.decoded);
});

router.get('/posts/my', apiLimiter, verifyToken, (req, res) => {
  Post.findAll({ where: { userId: req.decoded.id } })
    .then((posts) => {
      console.log(posts);
      res.json({
        code: 200,
        payload: posts,
      });
    })
    .catch((error) => {
      console.error(error);
      return res.status(500).json({
        code: 500,
        message: '서버 에러',
      });
    });
});

router.get('/posts/hashtag/:title', verifyToken, apiLimiter, async (req, res) => {
  try {
    const hashtag = await Hashtag.findOne({ where: { title: req.params.title } });
    if (!hashtag) {
      return res.status(404).json({
        code: 404,
        message: '검색 결과가 없습니다',
      });
    }
    const posts = await hashtag.getPosts();
    return res.json({
      code: 200,
      payload: posts,
    });
  } catch (error) {
    console.error(error);
    return res.status(500).json({
      code: 500,
      message: '서버 에러',
    });
  }
});

module.exports = router;

2. 프록시 서버 두기

CORS 적용 확인하기

localhost:4000에 접속하면 정상적으로 토큰이 발행됨을 확인할 수 있다.

응답 헤더를 보면 Access-Control-Allow-Origin 헤더가 들어 있다.

미들웨어 확장 패턴 복습

위의 미들웨어를 아래처럼 수정하면 미들웨어 위 아래로 임의의 코드를 추가할 수 있다.

router.use(cors()); 
router.use((req, res, next) => {
		cors()(req, res, next);
    }); 

0개의 댓글

관련 채용 정보