์ƒ์„ธ ์ปจํ…์ธ 

๋ณธ๋ฌธ ์ œ๋ชฉ

[๋…ธ๋“œ 2] 10์žฅ. ์›น API ์„œ๋ฒ„ ๋งŒ๋“ค๊ธฐ

23-24/Node.js 2

by _๋„๋‹ด 2023. 12. 22. 10:00

๋ณธ๋ฌธ

728x90

โ€‹

 

๐ŸŒŸ10์žฅ ํ‚ค์›Œ๋“œ๐ŸŒŸ

๋ผ์šฐํ„ฐ

JWT

ํ† ํฐ

์„œ๋ฒ„

์‹œํฌ๋ฆฟํ‚ค

 

 

 
 
 

10์žฅ ์›น API ์„œ๋ฒ„ ๋งŒ๋“ค๊ธฐ

 

10.1 API ์„œ๋ฒ„ ์ดํ•ดํ•˜๊ธฐ

 

API: Application Programming Interface. ๋‹ค๋ฅธ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์—์„œ ํ˜„์žฌ ํ”„๋กœ๊ทธ๋žจ์˜ ๊ธฐ๋Šฅ ์‚ฌ์šฉ ๊ฐ€๋Šฅํ•˜๊ฒŒ ํ•˜๋Š” ์ ‘์ 

 

์›น API: ๋‹ค๋ฅธ ์›น ์‚ฌ๋ฒ„์Šค์˜ ๊ธฐ๋Šฅ์„ ์‚ฌ์šฉํ•˜๊ฑฐ๋‚˜ ์ž์›์„ ๊ฐ€์ ธ์˜ฌ ์ˆ˜ ์žˆ๋Š” ์ฐฝ๊ตฌ.

 

์ œํ•œ: ์‚ฌ์šฉ์ž์˜ ์ธ์ฆ ์œ ๋ฌด, ๊ฐ€์ ธ๊ฐ„ ํšŸ์ˆ˜ ๋“ฑ์„ ๊ธฐ์ค€์œผ๋กœ ์ œํ•œ์„ ๋‘˜ ์ˆ˜ ์žˆ์Œ.

 

์›น API ์„œ๋ฒ„: ์„œ๋ฒ„์— API๋ฅผ ์˜ฌ๋ ค์„œ URL์„ ํ†ตํ•ด์„œ ์ ‘๊ทผํ•  ์ˆ˜ ์žˆ๊ฒŒ ๋งŒ๋“  ๊ฒƒ.

 

ํฌ๋กค๋ง: ์›น์‚ฌ์ดํŠธ๊ฐ€ ์ž์ฒด์ ์œผ๋กœ ์ œ๊ณตํ•˜๋Š” API๊ฐ€ ์—†๊ฑฐ๋‚˜, ์ด์šฉ์— ์ œํ•œ์— ์žˆ์„ ๋•Œ ์‚ฌ์šฉํ•˜๋Š” ๋ฐฉ๋ฒ•. ์›น ์‚ฌ์ดํŠธ์˜ ์ •๋ณด๋ฅผ ์ผ์ • ์ฃผ๊ธฐ๋กœ ์ˆ˜์ง‘ํ•ด ์ž์ฒด์ ์œผ๋กœ ๊ฐ€๊ณตํ•˜๋Š” ๊ธฐ์ˆ . ํ•˜์ง€๋งŒ ์›น์‚ฌ์ดํŠธ์—์„œ ์ง์ ‘ ์ œ๊ณตํ•˜๋Š” API๋Š” ์•„๋‹˜. ๋ฒ•์ ์ธ ๋ฌธ์ œ๊ฐ€ ๋ฐœ์ƒํ•  ์ˆ˜ ์žˆ์Œ.

 

 

 

 

10.2 ํ”„๋กœ์ ํŠธ ๊ตฌ์กฐ ๊ฐ–์ถ”๊ธฐ

 

NodeBird ์„œ๋น„์Šค์™€ ๋ฐ์ดํ„ฐ ๋ฒ ์ด์Šค ๊ณต์œ . ํ”„๋ก ํŠธ์ชฝ ๋ณด๋‹ค ๋ฐ์ดํ„ฐ๋‚˜ ์„œ๋น„์Šค๋ฅผ ์ด์šฉํ•˜๋Š” ์ฐฝ๊ตฌ ์ชฝ ์ง‘์ค‘.

 

์ธ์ฆ๋ฐ›์€ ์‚ฌ์šฉ์ž๋กœ ํ•˜์—ฌ๊ธˆ ์„œ๋น„์Šค ๊ฒŒ์‹œ๊ธ€, ํ•ด์‹œํƒœ๊ทธ, ์‚ฌ์šฉ์ž ์ •๋ณด๋ฅผ JSONํ˜•์‹์œผ๋กœ ์ œ๊ณตํ•˜๋„๋ก ๋งŒ๋“ค ๊ฒƒ.

 

๋จผ์ € ํ”„๋กœ์ ํŠธ๋ฅผ ์ง„ํ–‰ํ•  ํด๋”๋ฅผ ์ƒ์„ฑ, package.json ํŒŒ์ผ์„ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค.

//package.json

{
  "name": "nodebird-api",
  "version": "0.0.1",
  "description": "NodeBird API ์„œ๋ฒ„",
  "main": "app.js",
  "scripts": {
    "start": "nodemon app",
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "author": "Jimin",
  "license": "ISC",
  "dependencies": {
    "bcrypt": "^4.0.1",
    "cookie-parser": "^1.4.5",
    "dotenv": "^8.2.0",
    "express": "^4.17.1",
    "express-session": "^1.17.1",
    "morgan": "^1.10.0",
    "mysql2": "^2.1.0",
    "nunjucks": "^3.2.1",
    "passport": "^0.4.1",
    "passport-kakao": "1.0.0",
    "passport-local": "^1.0.0",
    "sequelize": "^5.21.7",
    "uuid": "^7.0.3"
  },
  "devDependencies": {
    "nodemon": "^2.0.3"
  }
}
 

์ดํ›„ npm init์œผ๋กœ dependencies๋ฅผ ์„ค์น˜ํ•ด๋„ ๋˜๊ณ , ์ฝ”๋“œ๋ฅผ ๋ณต์‚ฌํ•ด๋„ ๋ฉ๋‹ˆ๋‹ค.

 

npm i ๋ฅผ ํ†ตํ•ด์„œ ํ•ด๋‹น ํŒจํ‚ค์ง€๋ฅผ ์„ค์น˜ํ•ฉ๋‹ˆ๋‹ค.

๋Œ€ํ‘œ์‚ฌ์ง„ ์‚ญ์ œ

์‚ฌ์ง„ ์„ค๋ช…์„ ์ž…๋ ฅํ•˜์„ธ์š”.

 

์ดํ›„ config, models, passport ํด๋”์™€ ๋‚ด์šฉ๋ฌผ์„ ๋ชจ๋‘ ๋ณต์‚ฌํ•ด ์ด jsonํŒŒ์ผ์„ ๋งŒ๋“  ๊ณณ์— ๋ถ™์—ฌ ๋„ฃ์Šต๋‹ˆ๋‹ค.

๋ณธ ๊ต์žฌ์‹œ ์ฐธ๊ณ ํ•˜๋ผ ์ฃผ์–ด์ง„ ๊นƒํ—ˆ๋ธŒ๋ฅผ ์• ์šฉํ•ฉ๋‹ˆ๋‹ค.

์ด๋ฏธ์ง€ ์ธ๋„ค์ผ ์‚ญ์ œ
GitHub - gilbutITbook/006982: Node.js ๊ต๊ณผ์„œ

Node.js ๊ต๊ณผ์„œ. Contribute to gilbutITbook/006982 development by creating an account on GitHub.

github.com

 

์ดํ›„ views ํด๋”๋ฅผ ์ƒ์„ฑํ•ด ์—๋Ÿฌ๋ฅผ ํ‘œ์‹œํ•  ํŒŒ์ผ์„ ๋งŒ๋“ค์–ด์ค๋‹ˆ๋‹ค.

ํผ๊ทธ, ๊ธฐ์–ตํ•˜์‹œ๋‚˜์š”?

// error.pug

h1= message
h2= error.status 
pre #{error.stack}
 

์ดํ›„ views๋ฐ–์œผ๋กœ ๋‚˜์™€ ์ƒ์œ„ ํด๋”์— app.jsํŒŒ์ผ์„ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค.

// app.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);
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'), '๋ฒˆ ํฌํŠธ์—์„œ ๋Œ€๊ธฐ์ค‘');
});
 

 

ํ˜„์žฌ ํฌํŠธ ๋ฒˆํ˜ธ๋Š” 8002๋ฒˆ์ž…๋‹ˆ๋‹ค.

์ถ”ํ›„ ๋งŒ๋“ค ํด๋ผ์ด์–ธํŠธ์ธ nodebird-call ์„œ๋ฒ„๋Š” 8003๋ฒˆ ๋ฒˆํ˜ธ์—์„œ ์‹คํ–‰๋ฉ๋‹ˆ๋‹ค.

 

๋„๋ฉ”์ธ(์ธํ„ฐ๋„ท ์ฃผ์†Œ)์„ ์ถ”๊ฐ€ํ•ฉ๋‹ˆ๋‹ค.

modelsํŒŒ์ผ์— domain.js ํŒŒ์ผ์„ ์ถ”๊ฐ€ํ•ฉ๋‹ˆ๋‹ค.

// domain.js

const Sequelize = require('sequelize');

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: {
        type: Sequelize.STRING(36),
        allowNull: false,
      },
    }, {
      sequelize,
      timestamps: true,
      paranoid: true,
      modelName: 'Domain',
      tableName: 'domains',
    });
  }

  static associate(db) {
    db.Domain.belongsTo(db.User);
  }
};
 

์œ„์— ์žˆ๋Š” ์ •๋ณด๋Š” host(์ธํ„ฐ๋„ท ์ฃผ์†Œ), ๋„๋ฉ”์ธ ์ข…๋ฅ˜(type), ํด๋ผ์ด์–ธํŠธ ๋น„๋ฐ€ํ‚ค(๊ด€๋ฆฌ ์œ ์˜)์ž…๋‹ˆ๋‹ค.

๊ทธ๋ฆฌ๊ณ  validate๋ผ๋Š” ๋ฐ์ดํ„ฐ ์ถ”๊ฐ€ ๊ฒ€์ฆ ์†์„ฑ๊ณผ

unknownType์ด๋ผ๋Š” ๊ฒ€์ฆ ๋ฉ”์†Œ๋“œ๊ฐ€ ์žˆ์Šต๋‹ˆ๋‹ค. free, premium ๋‘ ๊ฐ€์ง€ ์™ธ์˜ ๊ฒฝ์šฐ์—๋Š” ์—๋Ÿฌ๊ฐ€ ๋ฐœ์ƒํ•ฉ๋‹ˆ๋‹ค.

 

๋„๋ฉ”์ธ ๋ชจ๋ธ์„ ์‹œํ€„๋ผ์ด์ฆˆ์™€ ์—ฐ๊ฒฐํ•˜๋Š”๋ฐ index.jsํŒŒ์ผ์€ ์•„๋ž˜์™€ ๊ฐ™๊ณ 

// index.js

const express = require('express');
const { v4: uuidv4 } = require('uuid');
const { User, Domain } = require('../models');
const { isLoggedIn } = require('./middlewares');

const router = express.Router();

router.get('/', async (req, res, next) => {
  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(),
    });
    res.redirect('/');
  } catch (err) {
    console.error(err);
    next(err);
  }
});

module.exports = router;
 

์‚ฌ์šฉ์ž ๋ชจ๋ธ๊ณผ ์ผ๋Œ€ ๋‹ค ๊ด€๊ณ„๋ฅผ ๊ฐ€์ง„ ๋„๋ฉ”์ธ ๊ด€๊ณ„๋ฅผ login.pug ํŒŒ์ผ์— ์ž‘์„ฑํ•ด ์ค๋‹ˆ๋‹ค.

 

// login.pug

doctype
html
    head
        meta(charset='utf-8')
    title NodeBird ๋กœ๊ทธ์ธ
    style.
        .input-group label {
            width: 200px;
            display: inline-block;
    }
    body
    if user && user.id
        span.user-name='์•ˆ๋…•ํ•˜์„ธ์š”!' + user.nick +'๋‹˜'
        a(href='/auth/logout'): button ๋กœ๊ทธ์•„์›ƒ
        fieldset
            legend ๋„๋ฉ”์ธ ๋“ฑ๋ก
            form(action='/domain' method='post')
            div
            label(for='type-free') ๋ฌด๋ฃŒ
            input#type-free(type='radio' name='type' value='free')
            label(for='type-premium') ํ”„๋ฆฌ๋ฏธ์—„
            input#type-premium(type='radio' name='type' value='premium')
        div
            label(for='host') ๋„๋ฉ”์ธ
            input#host(name='host' placeholder="ex) zerocho.com")
        button ์ €์žฅ
        table
            tr
            th ๋„๋ฉ”์ธ ์ฃผ์†Œ
            th ํƒ€์ž…
            th ํด๋ผ์ด์–ธํŠธ ๋น„๋ฐ€ํ‚ค
        for domain in user.domains
            tr
                td= domain.host
                td= domain.type
                td= domain.clientSecret
    else
        form#login-form(action='/auth/login' method='post')
            h2 NodeBird ๊ณ„์ •์œผ๋กœ ๋กœ๊ทธ์ธํ•˜์„ธ์š”.
            .input-group
                label(for='email') ์ด๋ฉ”์ผ
                input#email(type='email' name='email' required autofocus)
            .input-group
                label(for='password') ๋น„๋ฐ€๋ฒˆํ˜ธ
                input#password(type='password' name='password' required)
            if loginError
                .error-message= loginError
            a(href='/join'): button#join(type='button') ํšŒ์›๊ฐ€์ž…
            button#login(type='submit') ๋กœ๊ทธ์ธ
 
 

 

์ดํ›„์—๋Š” ๋„๋ฉ”์ธ์„ ๋“ฑ๋กํ•˜๋Š” ํ™”๋ฉด์„ ๋งŒ๋“ญ๋‹ˆ๋‹ค. ์•„๋ž˜์™€ ๊ฐ™์€ ๊ตฌ์กฐ๋ฅผ ๊ฐ€์ง‘๋‹ˆ๋‹ค.

๋กœ๊ทธ์ธ O -> ๋„๋ฉ”์ธ ๋“ฑ๋ก ํ™”๋ฉด

๋กœ๊ทธ์ธ X -> ๋กœ๊ทธ์ธ ์ฐฝ

 

๋ฃจํŠธ ๋ผ์šฐํ„ฐ(Get /)์™€ ๋„๋ฉ”์ธ ๋“ฑ๋ก ๋ผ์šฐํ„ฐ(Post /domain)์ž…๋‹ˆ๋‹ค. ๋ฃจํŠธ ๋ผ์šฐํ„ฐ๋Š” ์ ‘์† ์‹œ ๋กœ๊ทธ์ธ ํ™”๋ฉด์„ ๋ณด์—ฌ์ฃผ๊ณ  ๋„๋ฉ”์ธ ๋“ฑ๋ก ๋ผ์šฐํ„ฐ๋Š” ํผ์œผ๋กœ๋ถ€ํ„ฐ ์˜จ ๋ฐ์ดํ„ฐ๋ฅผ ๋„๋ฉ”์ธ ๋ชจ๋ธ์— ์ €์žฅํ•ฉ๋‹ˆ๋‹ค.

 

์ด์— ๋Œ€ํ•œ ๋‚ด์šฉ์€ rountes ํด๋” ๋‚ด index.jsํŒŒ์ผ์— ์ €์žฅํ•ฉ๋‹ˆ๋‹ค.

// index.js

const express = require('express');
const { v4: uuidv4 } = require('uuid');
const { User, Domain } = require('../models');
const { isLoggedIn } = require('./middlewares');

const router = express.Router();

router.get('/', async (req, res, next) => {
  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(),
    });
    res.redirect('/');
  } catch (err) {
    console.error(err);
    next(err);
  }
});

module.exports = router;
 

clientSecret์„ uuid(๋ฒ”์šฉ ๊ณ ์œ  ์‹๋ณ„์ž) ๋ชจ๋“ˆ์„ ํ†ตํ•ด ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. ๊ณ ์œ ํ•œ ๋ฌธ์ž์—ด ๋ถ€์—ฌ๊ฐ€ ๋ชฉํ‘œ์ž…๋‹ˆ๋‹ค.

 

์„œ๋ฒ„๋ฅผ ์‹คํ–‰ํ•˜๊ณ 

http://localhost:8002/ ์ด ์ฃผ์†Œ๋กœ ์ด๋™ํ•ฉ๋‹ˆ๋‹ค.

 

NodeBird https://nodebird.com/ ์•ฑ์˜ ์•„์ด๋””๋กœ ๋กœ๊ทธ์ธํ•ด ๋“ค์–ด๊ฐ€๋ฉด ๋ฉ๋‹ˆ๋‹ค.

์ด๋ฏธ์ง€ ์ธ๋„ค์ผ ์‚ญ์ œ
NodeBird

dd๋‹˜์ด ๋ฆฌํŠธ์œ— ํ•˜์…จ์Šต๋‹ˆ๋‹ค. 2023.12.20 ๋ฆฌ ๋ฆฌ์•กํŠธ๋งˆ์Šคํ„ฐ๊ธฐ์›1์ผ์ฐจ ์•ˆ๋…•ํ•˜์„ธ์š” :) =) 2023.12.20 d dd ddf ๋ฆฌ์•กํŠธ๋งˆ์Šคํ„ฐ๊ธฐ์›1์ผ์ฐจ๋‹˜์ด ๋ฆฌํŠธ์œ—ํ•˜์…จ์Šต๋‹ˆ๋‹ค. 2023.12.17 ์ œ ์ œ๋กœ์ดˆ 5MB ์ด์ƒ ํŒŒ์ผ๊ณผ ์ด๋ฆ„์— ๋„์–ด์“ฐ๊ธฐ ์žˆ๋Š” ์ด๋ฏธ์ง€๋Š” ์—…๋กœ๋“œ๋˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค. 2023.12.17 ๋ฆฌ ๋ฆฌ์•กํŠธ๋งˆ์Šคํ„ฐ๊ธฐ์›1์ผ์ฐจ ์•ˆ๋…•ํ•˜์„ธ์š” :) =) 2023.11.23 ์ œ ์ œ๋กœ์ดˆ 5MB ์ด์ƒ ํŒŒ์ผ๊ณผ ์ด๋ฆ„์— ๋„์–ด์“ฐ๊ธฐ ์žˆ๋Š” ์ด๋ฏธ์ง€๋Š” ์—…๋กœ๋“œ๋˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค. 2023.11.23 ์ œ ์ œ๋กœ์ดˆ ์ด๋ฏธ์ง€ ์—…๋กœ๋“œ ๋ณต๊ตฌ ํ…Œ์ŠคํŠธ 111๋‹˜์ด ๋ฆฌํŠธ์œ—ํ•˜์…จ์Šต๋‹ˆ๋‹ค. 2023....

nodebird.com

๋Œ€ํ‘œ์‚ฌ์ง„ ์‚ญ์ œ

์‚ฌ์ง„ ์„ค๋ช…์„ ์ž…๋ ฅํ•˜์„ธ์š”.

๋Œ€ํ‘œ์‚ฌ์ง„ ์‚ญ์ œ

์‚ฌ์ง„ ์„ค๋ช…์„ ์ž…๋ ฅํ•˜์„ธ์š”.

์œ„์™€ ๊ฐ™์ด ์‚ฌ์ดํŠธ์—์„œ ํšŒ์›๊ฐ€์ž… ํ›„ ๊ฒŒ์‹œ๊ธ€ ์ž‘์„ฑ์ด ๊ฐ€๋Šฅํ•˜๋‹ค.

์ด์™€ ๊ฐ™์€ ๊ณ„์ •์„ ์ด์šฉํ•ด์„œ ๋„๋ฉ”์ธ์„ ๋“ฑ๋กํ•  ์ˆ˜ ์žˆ๋‹ค.

 

ํด๋ผ์ด์–ธํŠธ ๋น„๋ฐ€ํ‚ค๋Š” ๋žœ๋ค ๋ฌธ์ž์—ด์ด๋‹ค.

 

 

 

 

10.3 JWT ํ† ํฐ์œผ๋กœ ์ธ์ฆํ•˜๊ธฐ

 

๋‹ค๋ฅธ ํด๋ผ์ด์–ธํŠธ๊ฐ€ NodeBird API๋ฅผ ๊ฐ€์ ธ๊ฐˆ ์ˆ˜ ์žˆ๊ฒŒ ํ•ด์•ผ ํ•˜๋Š” ๊ณผ์ •์€ ๋ณ„๋„์˜ ์ธ์ฆ๊ณผ์ •์ด ํ•„์š”ํ•˜๋‹ค.

 

JWT๋Š” JSON Web Token์˜ ์•ฝ์–ด๋กœ JSON ํ˜•์‹ ๋ฐ์ดํ„ฐ๋ฅผ ์ €์žฅํ•˜๋Š” ํ† ํฐ์ด๋‹ค.

 

JWT์˜ ๊ตฌ์กฐ๋Š” ์•„๋ž˜ ์„ธ ๊ฐ€์ง€์ด๋‹ค.

  • ํ—ค๋”: ํ† ํฐ ์ข…๋ฅ˜, ํ•ด์‹œ ์•Œ๊ณ ๋ฆฌ์ฆ˜ ์ •๋ณด
  • ํŽ˜์ด๋กœ๋“œ: ํ† ํฐ ๋‚ด์šฉ๋ฌผ์ด ์ธ์ฝ”๋”ฉ
  • ์‹œ๊ทธ๋‹ˆ์ฒ˜: ํ† ํฐ ๋ณ€์กฐ ์—ฌ๋ถ€๋ฅผ ํ™•์ธํ•˜๋Š” ๋ฌธ์ž์—ด -> JWT๋ฅผ ๋น„๋ฐ€ํ‚ค๋กœ ๋งŒ๋“ ๋‹ค.

 

์‹œ๊ทธ๋‹ˆ์ฒ˜๋Š” ์ˆจ๊ธธ ํ•„์š”๊ฐ€ ์—†์ง€๋งŒ, ๋น„๋ฐ€ํ‚ค๋Š” ์ˆจ๊ฒจ์•ผ ํ•œ๋‹ค.

 

ํ† ํฐ ๋‚ด์šฉ์„ ํ•ด์„ํ•ด ์ฃผ๋Š” ์‚ฌ์ดํŠธ๋„ ์žˆ๋‹ค.

์ด๋ฏธ์ง€ ์ธ๋„ค์ผ ์‚ญ์ œ
JWT.IO

JSON Web Tokens are an open, industry standard RFC 7519 method for representing claims securely between two parties.

jwt.io

encode๋ถ€๋ถ„์— ๊ฐ’์„ ๋„ฃ์œผ๋ฉด decode๋ถ€๋ถ„์—์„œ ๋‚ด์šฉ์„ ์•Œ๋ ค์ค€๋‹ค. id, node, message ๋“ฑ์˜ ์ •๋ณด๋„ ๋ณด์ธ๋‹ค. ๋•Œ๋ฌธ์— ์ค‘์š”ํ•œ ๋‚ด์šฉ์„ ๋‹ด์œผ๋ฉด ์•ˆ ๋œ๋‹ค.

 

ํ† ํฐ์€ ๋‚ด์šฉ์ด ๋“ค์–ด์žˆ๊ธฐ ๋•Œ๋ฌธ์— ์‚ฌ์šฉํ•˜๋Š”๋ฐ, JWT ํ† ํฐ์€ ๋น„๋ฐ€ํ‚ค๋ฅผ ๋ชจ๋ฅด๋ฉด ๋ณ€์กฐ๋ฅผ ํ•  ์ˆ˜์—†๋‹ค. ์ฆ‰ ๋‚ด์šฉ๋ฌผ์˜ ์ •๋ณด๋ฅผ ๋ฏฟ๊ณ  ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋‹ค.

๋‹จ์ ์€ ์šฉ๋Ÿ‰์ด ํฌ๊ณ , ์™ธ๋ถ€ ๋…ธ์ถœ ๊ฐ€๋Šฅํ•œ ์ •๋ณด๋งŒ ๋‹ด์„ ์ˆ˜ ์žˆ๋‹ค๋Š” ์ ์ด๋‹ค.

 

๊ตฌ๋ถ„ ๊ธฐ์ค€์€ ๋น„์šฉ์ด ๋  ์ˆ˜ ์žˆ๋‹ค.

๋žœ๋ค ์ŠคํŠธ๋ง์œผ๋กœ ์‚ฌ์šฉ์ž ์ •๋ณด๋ฅผ ๋งค๋ฒˆ ์กฐํšŒํ•˜๋Š” ์ž‘์—… vs ๋‚ด์šฉ๋ฌผ์ด ๋“ค์–ด์žˆ๋Š” JWT ํ† ํฐ ์‚ฌ์šฉ ์ž‘์—…

 

JWT ๋ชจ๋“ˆ ์„ค์น˜๋ถ€ํ„ฐ ์‹œ์ž‘ํ•ด ์ด๋ฅผ ์ด์šฉํ•œ ํ† ํฐ ์ธ์ฆ ๊ณผ์ • ๊ตฌํ˜„์„ ์‚ดํŽด๋ณด์ž

 

๋ชจ๋“ˆ์„ ์•„๋ž˜์˜ ๋ช…๋ น์–ด๋ฅผ ํ†ตํ•ด์„œ ์„ค์น˜ํ•œ๋‹ค.

$ npm i jsonwebtoken

 

์ดํ›„ API๋ฅผ ๋งŒ๋“ค ๊ฒƒ์ธ๋ฐ ์ด์ „์— ์ง„ํ–‰ํ•œ ๊ฒƒ์ฒ˜๋Ÿผ, ์ƒˆ๋กœ์šด nodebird ํด๋”๋ฅผ ์ƒ์„ฑํ•œ ํ›„, ์ด ํด๋”์—์„œ ์ง„ํ–‰ํ•œ๋‹ค.

 

JWT ํ† ํฐ์„ ๋ฐœ๊ธ‰๋ฐ›๊ณ  ์ธ์ฆ๋ฐ›์•„์•ผ ํ•˜๋Š”๋ฐ env ํŒŒ์ผ๊ณผ middlewares.js ํŒŒ์ผ์„ ์ƒ์„ฑํ•œ๋‹ค.

// .env

COOKIE_SECRET=nodebirdsecret
KAKAO_ID=5d4daf57becfd72fd9c919882552c4a6
JWT_SECRET=jwtSecret
 
// middlewares.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 {
    req.decoded = jwt.verify(req.headers.authorization, process.env.JWT_SECRET);
    return next();
  } catch (error) {  // ๋น„๋ฐ€ํ‚ค ์ผ์น˜ x, ์œ ํšจ๊ธฐ๊ฐ„ ์ง€๋‚  ๊ฒฝ์šฐ
    if (error.name === 'TokenExpiredError') { // ์œ ํšจ๊ธฐ๊ฐ„ ์ดˆ๊ณผ
      return res.status(419).json({
        code: 419,
        message: 'ํ† ํฐ์ด ๋งŒ๋ฃŒ๋˜์—ˆ์Šต๋‹ˆ๋‹ค',
      });
    }
    return res.status(401).json({
      code: 401,
      message: '์œ ํšจํ•˜์ง€ ์•Š์€ ํ† ํฐ์ž…๋‹ˆ๋‹ค',
    });
  }
};
 

 

์š”์ฒญ ํ—ค๋”์— ์ €์žฅ๋œ ํ† ํฐ req.headers.authorization ์„ ์‚ฌ์šฉํ•˜๋Š”๋ฐ, ์ฟ ํ‚ค์ฒ˜๋Ÿผ ํ—ค๋”์— ํ† ํฐ์„ ๋„ฃ์–ด ๋ณด๋‚ธ๋‹ค.

 

๋ฐ›์•„๋‚ธ ํ† ํฐ ๋‚ด์šฉ์„ ๋ฏธ๋“ค์›จ์–ด์—์„œ ์‚ฌ์šฉํ•˜๋„๋ก ํ•˜๋Š” v1.js์ฝ”๋“œ

// v1.js

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.find({
      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,         // 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;
 

 

๋ผ์šฐํ„ฐ๋Š” ํ•œ ๋ฒˆ ๋ฒ„์ „์ด ์ •ํ•ด์ง„ ์ดํ›„ ํ•จ๋ถ€๋กœ ์ˆ˜์ •ํ•˜๋ฉด ์•ˆ ๋œ๋‹ค.

๋ฒ„์ „์„ ์˜ฌ๋ ค์•ผ ํ•œ๋‹ค๋ฉด ์ƒˆ๋กœ์šด ๋ผ์šฐํ„ฐ ํŒŒ์ผ์„ ์ƒˆ๋กœ ๋“ฑ๋กํ•˜๊ณ , ๊ธฐ์กด ์‚ฌ์šฉ์ž๋“ค์—๊ฒŒ ์ƒˆ๋กœ์šด ๊ฒŒ ๋‚˜์™”์Œ์„ ์•Œ๋ ค์•ผ ํ•จ.

 

์ด๋ ‡๊ฒŒ ๋งŒ๋“  ๋ผ์šฐํ„ฐ๋ฅผ ์„œ๋ฒ„์— ์—ฐ๊ฒฐํ•œ๋‹ค.

// app.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 v1 = require('./routes/v1');
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);
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('/v1', v1);     // ์—ฐ๊ฒฐ ๋ถ€๋ถ„
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'), '๋ฒˆ ํฌํŠธ์—์„œ ๋Œ€๊ธฐ์ค‘');
});
 

 

 

 

10.4 ํ˜ธ์ถœ ์„œ๋ฒ„ ๋งŒ๋“ค๊ธฐ

 

์„œ๋ฒ„์ง€๋งŒ ๋‹ค๋ฅธ ์„œ๋ฒ„์— ์š”์ฒญ์„ ๋ณด๋‚ด๋ฏ€๋กœ ํด๋ผ์ด์–ธํŠธ ์—ญํ• ์„ ํ•œ๋‹ค.

API์‚ฌ์šฉ์ž์˜ ์ž…์žฅ-NodeBird์˜ ์•ฑ ๋ฐ์ดํ„ฐ๋ฅผ ๊ฐ€์ ธ์˜ค๊ณ  ์‹ถ์€-์—์„œ ์ด์šฉํ•œ๋‹ค.

 

์ƒˆ๋กœ NodeBird ํด๋”๋ฅผ ๋งˆ๋“ค๊ณ  ์ด์™€ ๊ฐ™์€ ํดํ„ฐ์— Nodecat์ด๋ผ๋Š” ์ƒˆ๋กœ์šด ํด๋”๋ฅผ ๋งŒ๋“ ๋‹ค.

 

jsonํŒŒ์ผ๊ณผ app.jsํŒŒ์ผ, error.pugํŒŒ์ผ ๊ทธ๋ฆฌ๊ณ  env ํŒŒ์ผ์„ ๋งŒ๋“ค์–ด ๋‚˜์˜ ๋น„๋ฐ€ํ‚ค๋ฅผ ๋„ฃ๋Š”๋‹ค.

 

์ดํ›„ ์ค‘์š”ํ•œ ์ž‘์šฉ์„ ํ•  index.jsํŒŒ์ผ์ด๋‹ค.

// 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) { // ์„ธ์…˜์— ํ† ํฐ์ด ์—†์œผ๋ฉด ํ† ํฐ ๋ฐœ๊ธ‰ ์‹œ๋„
      const tokenResult = await axios.post('http://localhost:8002/v1/token', {
        clientSecret: process.env.CLIENT_SECRET,
      });
      if (tokenResult.data && tokenResult.data.code === 200) { // ํ† ํฐ ๋ฐœ๊ธ‰ ์„ฑ๊ณต
        req.session.jwt = tokenResult.data.token; // ์„ธ์…˜์— ํ† ํฐ ์ €์žฅ
      } else { // ํ† ํฐ ๋ฐœ๊ธ‰ ์‹คํŒจ
        return res.json(tokenResult.data); // ๋ฐœ๊ธ‰ ์‹คํŒจ ์‚ฌ์œ  ์‘๋‹ต
      }
    }
    // ๋ฐœ๊ธ‰๋ฐ›์€ ํ† ํฐ ํ…Œ์ŠคํŠธ
    const result = await axios.get('http://localhost:8002/v1/test', {
      headers: { authorization: req.session.jwt },
    });
    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;
 

localhost:8002๋Š” API์„œ๋น„์Šค๋ฅผ ์ œ๊ณตํ•˜๋Š” NodeBird-api์„œ๋ฒ„์ด๊ณ 

localhose:8003์€ API์„œ๋น„์Šค๋ฅผ ์‚ฌ์šฉํ•˜๋Š” nodecate ์„œ๋ฒ„์ด๋‹ค.

์ ‘์†ํ•ด ํ† ํฐ ๋‚ด์šฉ์„ ํ™•์ธํ•  ์ˆ˜ ์žˆ๋‹ค.

์œ ํšจ๊ธฐ๊ฐ„์— ๋”ฐ๋ผ ๋งŒ๋ฃŒ ์•Œ๋ฆผ ์œ ๋ฌด๋„ ํ™•์ธํ•  ์ˆ˜ ์žˆ๋‹ค.

๋‹ค์Œ ์ ˆ์—์„œ ๋งŒ๋ฃŒ ์‹œ ๊ฐฑ์‹ ํ•˜๋Š” ์ฝ”๋“œ๋ฅผ ์•Œ๋ ค์ค€๋‹ค

 

 

10.5 SNS API์„œ๋ฒ„ ๋งŒ๋“ค๊ธฐ

 

๋‹ค์‹œ nodebird-api ์ž…์žฅ์œผ๋กœ ๋Œ์•„์™€ ๋ผ์šฐํ„ฐ๋ฅผ ์ž‘์„ฑํ•œ๋‹ค.

// v1.js

const express = require('express');
const jwt = require('jsonwebtoken');

const { verifyToken } = require('./middlewares');
const { Domain, User, Post, Hashtag } = 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);
});

router.get('/posts/my', verifyToken, (req, res) => {    // cnrk
  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;
 

ํฌ์ŠคํŠธ์™€ ๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ๋ฅผ ๊ฐ€์ ธ์˜ค๋Š” ๋ผ์šฐํ„ฐ.

 

index.jsํŒŒ์ผ์— ์œ„์˜ api๋ฅผ ์ด์šฉํ•˜๋Š” ์ฝ”๋“œ๋ฅผ ์ถ”๊ฐ€ํ•œ๋‹ค.

// index.js
const express = require('express');
const axios = require('axios');
const router = express.Router();
const URL ='http://localhost:8002/v1';
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}, {
headers: { authorization: req.session.jwt },
}); // API ์š”์ฒญ
} catch (error) {
console.error(error);
if (error.response.status < 500) { // 410์ด๋‚˜ 419์ฒ˜๋Ÿผ ์˜๋„๋œ
์—๋Ÿฌ๋ฉด ๋ฐœ์ƒ
return error.response;
}
throw error;
}
};
router.get('/mypost', async (req, res, next) => {
try {
const result = await request(req,'/posts/my');
res.json(result.data);
} catch (error) {
console.error(error);
next(error);
}
});
ff
router.get('/search/:hashtag', async (req, res, next) => {
try {
const result = await request(
req, /posts/hashtag/${encodeURIComponent(req.params.ha
shtag)},
);
res.json(result.data);
} catch (error) {
if (error.code) {
console.error(error);
next(error);
}
}
});
module.exports = router;
 

..์‹ค์Šต ํ•˜์‹ญ์‹œ์˜ค..! (์œ„ ๋‚ด์šฉ์€ ๊นƒํ—ˆ๋ธŒ์— ์žˆ๋Š” ๊ฒƒ๊ณผ ๋‹ค๋ฆ„)

 

 

 

 

10.6 ์‚ฌ์šฉ๋Ÿ‰ ์ œํ•œ ๊ตฌํ˜„ํ•˜๊ธฐ

 

์ผ๋‹จ ์ธ์ฆ๋œ ์‚ฌ์šฉ์ž๋งŒ API๋ฅผ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์ง€๋งŒ, ๊ณผ๋„ํ•˜๊ฒŒ ์ด๋ฅผ ์ด์šฉํ•˜๋ฉด ์„œ๋ฒ„์— ๋ฌด๋ฆฌ๊ฐ€ ๊ฐ„๋‹ค.

ํšŸ์ˆ˜ ์ œํ•œ์„ ๋‘ ์œผ๋กœ์„œ ์„œ๋ฒ„์˜ ํŠธ๋ž˜ํ”ฝ์„ ์ œํ•œํ•˜๋Š” ๊ฒƒ์ด ์ค‘์š”ํ•˜๋‹ค.

์œ ๋ฃŒ ๋ผ๋ฉด ๊ณผ๊ธˆ ์ฒด๊ณ„์™€ ํšŸ์ˆ˜๋กœ ์ด๋ฅผ ์—ฐ๊ฒฐ ์ง€์„ ์ˆ˜ ์žˆ๋‹ค.

 

์ด๋Š” ๊ธฐ์กด npm ํŒจํ‚ค์ง€๋ฅผ ์ด์šฉํ•ด์„œ ๋งŒ๋“ค ์ˆ˜ ์žˆ๋‹ค.

npm i express-rate-limit

 

๊ทธ๋ฆฌ๊ณ  apiLimiter ๋ฏธ๋“ค์›จ์–ด๋ฅผ ์ถ”๊ฐ€ํ•œ๋‹ค.

...

exports.apiLimiter = new RateLimit({
  windowMs: 60 * 1000, // 1๋ถ„
  max: 10,
  delayMs: 0,
  handler(req, res) {
    res.status(this.statusCode).json({
      code: this.statusCode, // ๊ธฐ๋ณธ๊ฐ’ 429
      message: '1๋ถ„์— ํ•œ ๋ฒˆ๋งŒ ์š”์ฒญํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.',
    });
  },
});


exports.deprecated = (req, res) => {  // ์‚ฌ์šฉํ•˜๋ฉด ์•ˆ๋˜๋Š” ๋ผ์šฐํ„ฐ์— ๋ถ™์—ฌ์คŒ
  res.status(410).json({
    code: 410,
    message: '์ƒˆ๋กœ์šด ๋ฒ„์ „์ด ๋‚˜์™”์Šต๋‹ˆ๋‹ค. ์ƒˆ๋กœ์šด ๋ฒ„์ „์„ ์‚ฌ์šฉํ•˜์„ธ์š”.',
  });
};

 
...
 

 

์ด์ œ apiLimiter ๋ฏธ๋“ค์›จ์–ด๋ฅผ ๋ผ์šฐํ„ฐ์— ๋„ฃ์œผ๋ฉด ๋ผ์šฐํ„ฐ์— ์‚ฌ์šฉ๋Ÿ‰ ์ œํ•œ์ด ๊ฑธ๋ฆฐ๋‹ค.

 

์•„๋ž˜์™€ ๊ฐ™์€ ์˜ต์…˜์ด ์žˆ๋‹ค.

  • windowMs ๊ธฐ์ค€ ์‹œ๊ฐ„
  • max ํ—ˆ์šฉ ํšŸ์ˆ˜
  • delayMs ํ˜ธ์ถœ ๊ฐ„๊ฒฉ
  • handler ์ œํ•œ ์ดˆ๊ณผ ์‹œ ์ฝœ๋ฐฑํ•จ์ˆ˜

 

์ œํ•œ ์ดˆ๊ณผ์‹œ 439 ์ƒํƒœ ์ฝ”๋“œ์™€ ํ•จ๊ป˜ ํ—ˆ์šฉ๋Ÿ‰ ์ดˆ๊ณผํ–ˆ๋‹ค๋Š” ์‘๋‹ต ์ „์†ก.

 

 

ํด๋ผ์ด์–ธํŠธ๋กœ ๋ณด๋‚ด๋Š” ์‘๋‹ต ์ฝ”๋“œ

  • 0์—ด ์„ ํƒ0์—ด ๋‹ค์Œ์— ์—ด ์ถ”๊ฐ€
  • 1์—ด ์„ ํƒ1์—ด ๋‹ค์Œ์— ์—ด ์ถ”๊ฐ€
  • 0ํ–‰ ์„ ํƒ0ํ–‰ ๋‹ค์Œ์— ํ–‰ ์ถ”๊ฐ€
  • 1ํ–‰ ์„ ํƒ1ํ–‰ ๋‹ค์Œ์— ํ–‰ ์ถ”๊ฐ€
  • 2ํ–‰ ์„ ํƒ2ํ–‰ ๋‹ค์Œ์— ํ–‰ ์ถ”๊ฐ€
์…€ ์ „์ฒด ์„ ํƒ
์—ด ๋„ˆ๋น„ ์กฐ์ ˆ
ํ–‰ ๋†’์ด ์กฐ์ ˆ
์‘๋‹ต ์ฝ”๋“œ
๋ฉ”์‹œ์ง€
200
Json๋ฐ์ดํ„ฐ
 
 
  • ์…€ ๋ณ‘ํ•ฉ
  • ํ–‰ ๋ถ„ํ• 
  • ์—ด ๋ถ„ํ• 
  • ๋„ˆ๋น„ ๋งž์ถค
  • ์‚ญ์ œ

 

 

v2๋ผ์šฐํ„ฐ๋ฅผ ์ƒˆ๋กœ ์ƒ์„ฑํ•ด ์‚ฌ์šฉ๋Ÿ‰ ์ œํ•œ์— ๋Œ€ํ•œ ๋‚ด์šฉ ์ถ”๊ฐ€

+ ํ† ํฐ ์œ ํšจ๊ธฐ๊ฐ„ 30๋ถ„์œผ๋กœ ์—ฐ์žฅ

+ v1์ด์šฉ ์‹œ ๊ฒฝ๊ณ  ๋ฉ”์‹œ์ง€

 

// 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) => {
  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;
 

 

๋ผ์šฐํ„ฐ์™€ ์„œ๋ฒ„ ์—ฐ๊ฒฐ

// app.js

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

 

๊ทธ๋ฆฌ๊ณ  index.js ํŒŒ์ผ์— ์•„๋ž˜ ์ฝ”๋“œ ์ถ”๊ฐ€

const URL = 'http://localhost:8002/v2';

 

 

 

 

 

 

 

10.7 CORS ์ดํ•ดํ•˜๊ธฐ

 

nodecat์˜ ํ”„๋ก ํŠธ์—์„œ nodebird-api์˜ ์„œ๋ฒ„ api๋ฅผ ํ˜ธ์ถœํ•œ ๊ฒฝ์šฐ

 

 

nodecat์˜ indexํŒŒ์ผ ์ˆ˜์ •

// index.js

const express = require('express');
const axios = require('axios');

const router = express.Router();
const URL = 'http://localhost:8002/v2';

axios.defaults.headers.origin = 'http://localhost:4000'; // 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}`, {
      headers: { authorization: req.session.jwt },
    }); // API ์š”์ฒญ
  } catch (error) {
    if (error.response.status === 419) { // ํ† ํฐ ๋งŒ๋ฃŒ์‹œ ํ† ํฐ ์žฌ๋ฐœ๊ธ‰ ๋ฐ›๊ธฐ
      delete req.session.jwt;
      return request(req, api);
    } // 419 ์™ธ์˜ ๋‹ค๋ฅธ ์—๋Ÿฌ๋ฉด
    return error.response;
  }
};

router.get('/mypost', async (req, res, next) => {
  try {
    const result = await request(req, '/posts/my');
    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)}`,
    );
    res.json(result.data);
  } catch (error) {
    if (error.code) {
      console.error(error);
      next(error);
    }
  }
});

router.get('/', (req, res) => {
  res.render('main', { key: process.env.CLIENT_SECRET });
});

module.exports = router;
 

๊ทธ๋ฆฌ๊ณ  mainํผ๊ทธ ํŒŒ์ผ ์ˆ˜์ •

// main.pug

doctype
html
head
title ํ”„๋ŸฐํŠธ API ์š”์ฒญ
body
#result
script.
var xhr = new XMLHttpRequest();
xhr.onreadystatechange = function () {
if (xhr.readyState === xhr.DONE) {
if (xhr.status === 200) {
document.querySelector('#result').textContent = xhr.responseText;
} else {
console.error(xhr.responseText);
}
}
};
xhr.open('POST','http://localhost:8002/v2/token');
xhr.setRequestHeader('Content-Type','application/json');
xhr.send(JSON.stringify({ clientSecret:'#{key}' }));      // ๋น„๋ฐ€ํ‚ค๋กœ ๋žœ๋”๋ง 
 

 

 

ํ˜„์žฌ ์ƒํƒœ์—์„œ ์‹คํ–‰ํ•˜๋ฉด CORS๋ฌธ์ œ ๋ฐœ์ƒ

ํ˜„์žฌ ์š”์ฒญ์„ ๋ณด๋‚ด๋Š” ํด๋ผ์ด์–ธํŠธ์™€ ์š”์ฒญ์„ ๋ฐ›๋Š” ์„œ๋ฒ„์˜ ๋„๋ฉ”์ธ ๋ฌธ์ œ.

Network ํƒญ(๊ฐœ๋ฐœ์ž ํƒญ)์—์„œ Method๊ฐ€ Post๊ฐ€ ์•„๋‹Œ Option์œผ๋กœ ํ‘œ์‹œ๋˜๋Š”๋ฐ ์ด ๋ฉ”์„œ๋“œ๋Š” ์‹ค์ œ ์š”์ฒญ ๋ณด๋‚ด๊ธฐ ์ „์—, ์ด์„œ๋ฒ„๊ฐ€ ๋„๋ฉ”์ธ์„ ํ—ˆ์šฉํ•˜๋Š”์ง€ ์ฒดํฌํ•œ๋‹ค

 

CORS๋ฌธ์ œ ํ•ด๊ฒฐ์„ ์œ„ํ•ด์„œ ์‘๋‹ต ํ•ด๋”์— Access-Control-Allow-Origin์ด๋ผ๋Š” ํ•ด๋” ๋„ฃ์–ด์ฃผ์–ด์•ผ ํ•œ๋‹ค.

ํ˜น์€ npm์„ ์„ค์น˜ํ•œ๋‹ค. ๋‹จ nodebird-api์—์„œ ์„ค์น˜ํ•œ๋‹ค.

npm i cors

 

์ดํ›„ v2.js์— ์ ์šฉํ•œ๋‹ค.

 

 


Node.js #2 

Editor : ๋ผ๋งˆ

 

 

 

 

 

728x90

๊ด€๋ จ๊ธ€ ๋”๋ณด๊ธฐ