반응형
API 서버 이해하기
API: 다른 애플리케이션에서 현재 프로그램의 기능을 사용할 수 있게 허용하는 접점을 의미
- 웹 API는 다른 웹 서비스의 기능을 사용하거나 자원을 가져올 수 있는 창구이다
- 크롤링: 표면저그올 보이는 웹 사이트의 정보를 일정 주기로 수집해 자체적으로 가공하는 기술
- 자체적으로 제공하는 API가 없거나 API이용에 제한이 있는 경우 사용
프로젝트 구조 갖추기
다른 서비스가 nodebird의 데이터나 서비스를 이용할 수 있도록 만들 것이다.
{
"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": "Zero Cho",
"license": "ISC",
"dependencies": {
"bcrypt": "^5.0.0",
"cookie-parser": "^1.4.5",
"dotenv": "^16.0.1",
"express": "^4.17.1",
"express-session": "^1.17.1",
"morgan": "^1.10.0",
"mysql2": "^2.1.0",
"nunjucks": "^3.2.1",
"passport": "^0.6.0",
"passport-kakao": "1.0.0",
"passport-local": "^1.0.0",
"sequelize": "^6.19.2",
"uuid": "^8.3.2"
},
"devDependencies": {
"nodemon": "^2.0.16"
}
}
package.json을 위와 같이 구성해 패키지를 설치한다
- 도메인을 등록하는 기능이 새로 생겼으므로 도메인 모델을 추가해보자
const Sequelize = require('sequelize');
class Domain extends Sequelize.Model {
static initiate(sequelize) {
Domain.init({
host: {
type: Sequelize.STRING(80),
allowNull: false,
},
type: {
type: Sequelize.ENUM('free', 'premium'),
allowNull: false,
},
clientSecret: {
type: Sequelize.UUID,
allowNull: false,
},
}, {
sequelize,
timestamps: true,
paranoid: true,
modelName: 'Domain',
tableName: 'domains',
});
}
static associate(db) {
db.Domain.belongsTo(db.User);
}
};
module.exports = Domain;
도메인 모델에는 인터넷 주소, 도메인 종류, 클라이언트 비밀 키가 들어간다
const express = require('express');
const { renderLogin, createDomain } = require('../controllers');
const { isLoggedIn } = require('../middlewares');
const router = express.Router();
router.get('/', renderLogin);
router.post('/domain', isLoggedIn, createDomain);
module.exports = router;
GET / 라우터와 도메인 등록 라우터를 만들어준다
JWT 토큰으로 인증하기
NodeBird 앱이 아닌 다른 클라이언트가 NodeBird의 데이터를 가져갈 수 있게 해야 하는 만큼 별도의 인증 과정이 필요하다. JWT 토큰을 사용해 인증하는 방법을 사용하자
- JWT(JSON Web Token): JSON 형식의 데이터를 저장하는 토큰이다.
- JWT의 구성
- 헤더: 토큰 종류와 해시 알고리즘 정보가 들어있다
- 페이로드: 토큰의 내용물이 인코딩된 부분이다
- 시그니처: 일련의 문자열로, 시그니처를 통해 토큰이 변조되었는지 여부를 확인할 수 있다
JWT_SECRET = jwtSecret
.env에서 설정해주고
exports.verifyToken = (req, res, next) => {
try {
res.locals.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: '유효하지 않은 토큰입니다',
});
}
};
미들웨어의 index.js에서 요청 헤더에 저장된 토큰을 사용해 검증을 할 수 있다.
const express = require('express');
const { verifyToken } = require('../middlewares');
const { createToken, tokenTest } = require('../controllers/v1');
const router = express.Router();
// POST /v1/token
router.post('/token', createToken);
// POST /v1/test
router.get('/test', verifyToken, tokenTest);
module.exports = router;
토큰을 발급하는 라우터(POST)와 사용자가 토큰을 테스트해볼 수 있는 라우터(GET)를 만들었다
다른 서비스에서 호출하기
- API 제공 서버를 만들었으니, API 사용하는 서비스도 만들어봅시다.
- 이 서비스는 다른 서버에 요청을 보내므로 클라이언트 역할을 하고, API 사용자의 입장에서 진행
- 이 서버의 주 목적은 nodebird-api의 API를 통해 데이터를 가져오는 것이다
{
"name": "nodecat",
"version": "0.0.1",
"description": "NodeBird 2차 서비스",
"main": "app.js",
"scripts": {
"start": "nodemon app"
},
"author": "Zero Cho",
"license": "ISC",
"dependencies": {
"axios": "^0.27.2",
"cookie-parser": "^1.4.6",
"dotenv": "^16.0.1",
"express": "^4.18.1",
"express-session": "^1.17.3",
"morgan": "^1.10.0",
"nunjucks": "^3.2.3"
},
"devDependencies": {
"nodemon": "^2.0.16"
}
}
- .env 파일에 client_secret 키를 넣고
const express = require('express');
const { test } = require('../controllers');
const router = express.Router();
// POST /test
router.get('/test', test);
module.exports = router;
const axios = require('axios');
exports.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?.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);
}
};
index.js를 만들어 nodecat 서비스가 토큰 인증 과정을 테스트해보는 라우터를 동작해본다
요청이 왔을 때 토큰이 없으면 v1에서 token 라우터로부터 토큰을 발급받고
성고하면 토큰을 저장하며 발급받은 토큰을 테스트 해보도록 구성했다.
SNS API 서버 만들기
- 다시 nodebird-api의 입장으로 돌아와서 나머지 api 라우터를 완성시킬 것이다
const express = require('express');
const { verifyToken } = require('../middlewares');
const { createToken, tokenTest, getMyPosts, getPostsByHashtag } = require('../controllers/v1');
const router = express.Router();
// POST /v1/token
router.post('/token', createToken);
// POST /v1/test
router.get('/test', verifyToken, tokenTest);
// GET /v1/posts/my
router.get('/posts/my', verifyToken, getMyPosts);
// GET /v1/posts/hashtag/:title
router.get('/posts/hashtag/:title', verifyToken, getPostsByHashtag);
module.exports = router;
get /posts/my - 내가 올린 포스트 가져오는 라우터
get /posts/hashtag/:title - 해시태그 검색 결과를 가져오는 라우터
사용량 제한 구현하기
- 일차적으로 인증된 사용자만 API를 사용할 수 있게 필터를 두긴 했지만, 아직 충분하지는 않다
- 일정 기간 내에 API를 사용할 수 있는 횟수를 제한해 서버의 트래픽을 줄이는 것이 좋다
npm i express-rate-limit
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 {
const message = encodeURIComponent('로그인한 상태입니다.');
res.redirect(`/?error=${message}`);
}
};
exports.verifyToken = (req, res, next) => {
try {
res.locals.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 = rateLimit({
windowMs: 60 * 1000, // 1분
max: 10,
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: '새로운 버전이 나왔습니다. 새로운 버전을 사용하세요.',
});
};
미들웨어도 추가해 요청을 조절할 수 있다
API 응답 목록
200: json 데이터
401: 유효하지 않은 토큰
410: 새로운 버전
419: 토큰 만료
429: 1분에 한번만
500~: 기타 서버 에러
const express = require('express');
const { verifyToken, apiLimiter } = require('../middlewares');
const { createToken, tokenTest, getMyPosts, getPostsByHashtag } = require('../controllers/v2');
const router = express.Router();
// POST /v2/token
router.post('/token', apiLimiter, createToken);
// POST /v2/test
router.get('/test', apiLimiter, verifyToken, tokenTest);
// GET /v2/posts/my
router.get('/posts/my', apiLimiter, verifyToken, getMyPosts);
// GET /v2/posts/hashtag/:title
router.get('/posts/hashtag/:title', apiLimiter, verifyToken, getPostsByHashtag);
module.exports = router;
사용량 제한이 생겼으므로 새로운 v2를 만들어 라우터에 추가한다
CORS 이해하기
- 이전 절에서 nodecat이 nodebird-api를 호출하는 것은 서버에서 서버로 API를 호출한 것이다. 만약 nodecat의 프런트에서 서버 API를 호출하면 어떻게 될까?
→ 브라우저와 서버의 도메인이 일치하지 않으면, 기본적으로 요청이 차단된다.
- 이와 같이 현재 요청을 보내는 클라이언트와 요청을 받는 서버의 도메인이 다르기때문에 생기는 문제를 CORS 문제라고 부른다.
반응형
'WINK-(Web & App) > Express.js (Node.js) 스터디' 카테고리의 다른 글
[2024-2 Node.js 스터디] 류상우 #6주차 (0) | 2024.11.25 |
---|---|
[2024-2 Node.js 스터디] 김민재 #5주차 (0) | 2024.11.18 |
[2024-2 Node.js 스터디] 류상우 #5주차 (2) | 2024.11.18 |
[2024-2 Node.js 스터디] 류상우 #4주차 (1) | 2024.11.11 |
[2024-2 Node.js 스터디] 김민재 #4주차 (0) | 2024.11.11 |