第10章

175566 ワード

外部サーバサーバからAPIを使用してデータをインポートします.

APIバー


アプリケーションプログラミングインタフェースの略語

  • は、他のアプリケーションが現在のプログラムの機能を使用することを可能にする.
  • Web API:他のWebサービスの機能を使用したり、リソースを取得したりできます.
  • は、他の人に情報を提供したい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にコピーします.
    routionsフォルダでauth.js, middlewares.コピー
    コピー.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サーバは、他のサードパーティサーバからノードbuddデータを取得することを制限することができる.
    制限するには、持って行った人が誰なのかを知らなければならない.
  • なので、サービスにログインしてドメイン名を追加させます.
  • クライアントSecretは鍵を発行します.
  • Enumはstring(10)などを使うことができますが、このタイプは10文字以内のすべての文字列でもいいですが、もっと詳しくしたいならenumを使うことができます.
  • Enum(「free」,「premium」)を使用し、2つのうち1つしか入れられません.Rareのような文字列を使用するとエラーが発生します.
  • タイプは、有料制が無料なのか、オリジナルなのかを区別するために使用されます.
  • ドメイン登録ルータの作成


    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です.JWTトークンはsignメソッドで発行されます.1回目のフェース買収にPaytod、2回目の買収にJWT秘密鍵、3回目のフェース買収にTokenオプションを追加します.
    (expiresln満期、発行者発行者)
  • expireslnはミリ秒単位で、例えば1 m=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メソッドの2番目のパラメータとして使用される場合、セッションは使用されません.
  • ...
    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フォルダを作成し、パッケージします.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ルータの作成

  • 位で作られたv 1.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のルータの作成

  • トークンは、送信要求の部分を要求関数
  • とする.
  • 要求はaxiosに送信され、セッショントークンチェックが実行され、
  • が再起動する.
    上に作成したルーティング/インデックス.変更
    //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: '새로운 버전이 나왔습니다. 새로운 버전을 사용하세요.',
      });
    };
  • deprecateミドルウェアは使用できないルータに貼り付けられ、使用時に警告されます.
  • レスポンスコードの整理



    新しいルーターバージョンの更新


    使用制限機能が追加され、既存のAPIと互換性がありません。

  • apiLimiterのv 2が追加されました.jsおよびv 1を作成します.jsの場合、廃棄処理が与えられます.
  • //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に置き換えられたバージョンv 2

    //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로 바꿔준거
    
    ...

    v 1上のAPIの使用または使用量を超えた場合にエラー


    CORSについて


    [CORSエラー例]

    これは、
  • ブラウザがサーバに要求を送信したときに、ドメインが異なる場合にのみ発生するエラーです.
    (localhost:4000からlocalhst:4000に送信された場合は発生しません)
  • CORSはブラウザを生成します.
  • のセキュリティ設計

    CORSエラー解決方法


    1.CORSモジュールの設定


    応答ヘッダにAccess-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);
        });