โ
๐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ํ์ผ์ ๋ง๋ ๊ณณ์ ๋ถ์ฌ ๋ฃ์ต๋๋ค.
๋ณธ ๊ต์ฌ์ ์ฐธ๊ณ ํ๋ผ ์ฃผ์ด์ง ๊นํ๋ธ๋ฅผ ์ ์ฉํฉ๋๋ค.
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/ ์ฑ์ ์์ด๋๋ก ๋ก๊ทธ์ธํด ๋ค์ด๊ฐ๋ฉด ๋ฉ๋๋ค.
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์ ๊ตฌ์กฐ๋ ์๋ ์ธ ๊ฐ์ง์ด๋ค.
์๊ทธ๋์ฒ๋ ์จ๊ธธ ํ์๊ฐ ์์ง๋ง, ๋น๋ฐํค๋ ์จ๊ฒจ์ผ ํ๋ค.
ํ ํฐ ๋ด์ฉ์ ํด์ํด ์ฃผ๋ ์ฌ์ดํธ๋ ์๋ค.
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 ๋ฏธ๋ค์จ์ด๋ฅผ ๋ผ์ฐํฐ์ ๋ฃ์ผ๋ฉด ๋ผ์ฐํฐ์ ์ฌ์ฉ๋ ์ ํ์ด ๊ฑธ๋ฆฐ๋ค.
์๋์ ๊ฐ์ ์ต์ ์ด ์๋ค.
์ ํ ์ด๊ณผ์ 439 ์ํ ์ฝ๋์ ํจ๊ป ํ์ฉ๋ ์ด๊ณผํ๋ค๋ ์๋ต ์ ์ก.
ํด๋ผ์ด์ธํธ๋ก ๋ณด๋ด๋ ์๋ต ์ฝ๋
์๋ต ์ฝ๋
|
๋ฉ์์ง
|
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์ ์ ์ฉํ๋ค.
[๋ ธ๋ 2] 12์ฅ. ์น ์์ผ์ผ๋ก ์ค์๊ฐ ๋ฐ์ดํฐ ์ ์กํ๊ธฐ (0) | 2024.01.05 |
---|---|
[๋ ธ๋ 2] 11์ฅ. ๋ ธ๋ ์๋น์ค ํ ์คํธํ๊ธฐ (1) | 2023.12.29 |
[๋ ธ๋ 2] 9์ฅ. ์ต์คํ๋ ์ค๋ก SNS ์๋น์ค ๋ง๋ค๊ธฐ (1) | 2023.12.01 |
[๋ ธ๋ 2] 8์ฅ. MongoDB (0) | 2023.11.24 |
[๋ ธ๋ 2] 7์ฅ. MySQL (0) | 2023.11.17 |