10.1 API서버 이해하기
⇒ API : Application Programing Interface
- 다른 애플리케잇현에서 현재 프로그램의 기능을 사용할 수 있게 허용하는 접점을 의미한다.
- 웹 API는 다른 웹 서비스의 기능을 사용하거나 자원을 가져올 수 있는 창구이다.
- 다른 사람에게 제공하고 싶은 부분만 API로 허용하고, 제공하고 싶지 않은 부분은 제외한다.
- API에 제한을 걸어 일정 횟수, 시간 내에서만 권한허용이 가능하다.
10.2 API 서버 프로젝트 구성하기
1. lecture-api 프로젝트
// middlewares/index.js
// 토큰 유효성 검증 미들웨어
exports.verifyToken = (req, res, next) => {
try {
// api 사용자가 토큰을 넣는 위치 : req.headers.authorization
res.locals.decoded = jwt.verify(req.headers.authorization, process.env.JWT_SECRET);
return next();
} catch (error) {
if(error.name === 'TokenExpiredError') {
res.status(419).json({
code : 419,
message : '토큰이 만료 되었습니다.',
});
} else {
return res.status(401).json({
code : 401,
message : '유효하지 않은 토큰입니다.',
});
}
}
};
// api의 버전이 변경 되었을 경우 유효성 체크
exports.deprecated = (req, res) => {
res.status(410).json({
code : 410,
message : '새로운 버전이 나왔습니다. 새로운 버전을 사용하세요.'
});
};
// routers/v1.js
const express = require('express');
const {verifyToken, deprecated} = require("../middlewares");
const {createToken, getMyPosts, getPostsByHashtag } = require('../controllers/v1');
const router = express.Router();
// v1 router 모든 호출에 현재 버전을 사용하지 않음을 공통적으로 처리한다.
router.use(deprecated);
router.post('/token', createToken); // req.body.clientSecret이 필요하다
router.get('/posts/my', verifyToken, getMyPosts);
router.get('/posts/hashtag/:title', verifyToken, getPostsByHashtag);
module.exports = router;
// middlewares/index.js
// api 호출 전에 파라미터 설정을 위한 미들웨어 확장패턴이다.
// 해당 패턴은 선처리가 필요할 경우 활용할 수 있다.
exports.apiLimiter = async (req, res, next) => {
// let user;
// if(res.locals.decoded) {
// user = User.findOne({where : {id : res.locals.decode.id}});
// }
rateLimit({
windowMs : 60 * 1000, // 1분
// max : user?.type === 'premium' ? 1000 : 10, // 최대건수
max : 10, // 최대건수
handler(req, res) { // 지정요건 초과 시 핸들러 수행
res.status(this.statusCode).json({
code : this.statusCode,
message : '1분에 한 번만 요청할 수 있습니다.',
});
}
})(req, res, next);
};
// 브라우저 요청에 의한 cors 패키지를 통한 header설정
exports.corsWhenDomainMatches = async (req,res,next) => {
const domain = await Domain.findOne({
where : {
host : new URL(req.get('Origin')).host
},
});
if(domain) {
console.log(req.get('Origin'));
cors({
// origin : true, // 모든 요청을 받는다
origin : req.get('Origin'),
// credentials이 true일 경우 origin : '*'속성은 사용할 수 없다.
// origin에 직접 url을 넣거나 true로 설정을 해야한다.
credentials : true, // 쿠키요청 허용여부
})(req,res,next);
}else {
next();
}
}
// routes/v2.js
const express = require('express');
const {verifyToken, apiLimiter, corsWhenDomainMatches} = require("../middlewares");
const {createToken, getMyPosts, getPostsByHashtag } = require('../controllers/v2');
const router = express.Router();
const cors = require('cors');
// cors패키지를 사용하지 않으면 해더정보를 추가해야 한다.
// 그리고 쿠키, https 등 다양한 케이스의 변수가 존재하므로 cors를 사용한다.
// cors 문제를 해결하는 다른방법은 프록시(대리인) 서버를 사용하는 것이다.
// 서버에서 서버로 요청을 보낼 때는 CORS 문제가 발생하지 않기 때문이다.
// cors를 사용하지 않고 브라우져에서 CORS오류를 해소하는 예시
// router.use((req,res, next) => {
// res.setHeader('Access-Control-Allow-Origin', 'http://localhost:4000');
// res.setHeader('Access-Control-Allow-Headers', 'content-type');
// next();
// });
// 브라우저에서 서버요청을 위한 CORS 미들웨어 설정
router.use(corsWhenDomainMatches);
// v2/token
// apiLimiter 미들웨어는 api의 요청제한설정을 위해 추가한다.
router.post('/token', apiLimiter, createToken); // req.body.clientSecret이 필요하다
// GET v2/posts/my : 나의 작성 게시글 불러오기
router.get('/posts/my', verifyToken, apiLimiter, getMyPosts);
// GET v2/posts/hashtag/:title : 해시태그 기준 게시글 불러오기
router.get('/posts/hashtag/:title', verifyToken, apiLimiter, getPostsByHashtag);
module.exports = router;
// controller/v2.js
const { Domain, User, Post, Hashtag } = require('../models');
const jwt = require('jsonwebtoken');
// 토큰발급 시 일반적으로 브라우저키와 서버키를 별도로 발행해야 한다.
// 브라우저 토큰은 화면에 들어나므로 보안에 취약하다
// 클라이언트 시크릿과 프론트 시크릿을 별도로 구분해서 구성하는 것이 이번 챕터의 과제이다.
exports.createToken = async (req, res) => {
const { clientSecret } = req.body;
try {
const domain = await Domain.findOne({
where : {clientSecret},
include : {
model : User,
attributes : ['id', 'nick'],
}
});
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: '5m', // 유효기간
issuer : 'nodebird', // 발급자
});
return res.json({
code : 200,
message : '토큰 발급되었습니다.',
token,
});
} catch (error) {
console.error(error);
return res.status(500).json({
code : 500,
message : '서버 에러',
});
}
}
// 내가 작성한 게시물 조회
exports.getMyPosts = (req, res) => {
Post.findAll({where : { userId : res.locals.decoded.id}})
.then((posts) => {
res.json({
code : 200,
payload : posts,
});
})
.catch((error) => {
return res.status(500).json({
code : 500,
message : '서버 에러',
});
});
};
// 해시태그 검색을 통한 게시물 불러오기
exports.getPostsByHashtag = async (req, res) => {
try {
const hashtag = await Hashtag.findAll({where : {title : req.params.title}});
if(!hashtag) {
return res.status(404).json({
code : 404,
message : '검색결과가 없습니다.',
})
}
const posts = await hashtag.getPosts();
if(posts.length === 0) {
return res.status(404).json({
code : 404,
message : '검색결과가 없습니다.',
});
}
res.json({
code : 200,
payload : posts,
})
} catch(err) {
return res.status(500).json({
code : 500,
message : '서버 에러',
});
}
};
2. lecture-call 프로젝트
// .env
COOKIE_SECRET=nodecat
CLIENT_SECRET=691e54df-d209-47c9-901d-d860176caa5f
ORIGIN=http://localhost:4000
API_URL=http://localhost:8002/v2
// routes/index.js
const express = require('express');
const router = express.Router();
const { getMyPosts, searchByHashtag, renderMain} = require('../controllers/index');
// GET /myposts : 나의 게시물 불러오기
router.get('/myposts', getMyPosts);
// GET /search/:hashtag 해시태그 검색에 따른 게시물 불러오기
router.get('/search/:hashtag', searchByHashtag);
// GET / : CORS테스트를 위한 브라우저에서 api서버 호출
router.get('/', renderMain);
module.exports = router;
// controllers/index.js
const axios = require('axios');
const URL = process.env.API_URL;
axios.defaults.headers.common.origin = process.env.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;
}
// 토큰을 통해 api 호출처리
return await axios.get(`${URL}${api}`, {
headers: {authorization : req.session.jwt},
});
} catch (error) {
if(error.response?.status == 419) { // 세션이 만료 시 재귀함수로 토큰을 재발급 처리
delete req.session.jwt;
return request(req, api);
} else {
throw error.response; // 에러가 발생시 throw를 통해 에러처리 미들웨어로 전달
}
}
};
// 나의 게시글보기 api 호출
exports.getMyPosts = async (req, res, next) => {
try {
const result = await request(req, '/posts/my');
res.json(result.data);
} catch (error) {
next(error);
}
};
// 해시태그를 통한 게시물 검색 api 호출
exports.searchByHashtag = async (req, res, next) => {
try {
const result = await request(req, `/posts/hashtag/${req.params.hashtag}`);
res.json(result.data);
} catch (error) {
next(error);
}
};
// 브라우저 axios 호출을 위한 main화면 랜더링(CORS)
exports.renderMain = (req, res) => {
res.render('main', {key : process.env.CLIENT_SECRET});
}
// views/main.html
<!DOCTYPE html>
<html>
<head>
<title>프론트 API 요청</title>
</head>
<body>
<div id="result"></div>
<script src="https://unpkg.com/axios/dist/axios.min.js"></script>
<script>
// 브라우저에서의 axios호출(CORS이슈가 발생하므로 백엔드에서 CORS 패키지를 활용해라)
axios.post('http://localhost:8002/v2/token', {
clientSecret: '{{key}}',
})
.then((res) => {
document.querySelector('#result').textContent = JSON.stringify(res.data);
})
.catch((err) => {
console.error(err);
});
</script>
</body>
</html>
10.3 JWT 토큰으로 인증하기
1. 인증을 위한 JWT
⇒ NodeBird가 아닌 다른 클라이언트가 데이터를 가져가게 하려면 인증 과정이 필요하다.
⇒ JWT(JSON Web Token)을 사용한다.
- 헤더, 페이로드, 시그니처로 구성되어 있다.
- 헤더 : 토큰 종류와 해시 알고리즘 정보가 들어있다.
- 페이로드 : 토큰의 내용물이 인코딩된 부분이다.
- 시그니처 : 일련의 문자열로, 시그니처를 통해 토큰이 변조 되었는지 여부를 확인한다.
- 시그니처는 JWT 비밀키로 만들어지고, 비밀키가 노출되면 토큰의 위조가 가능하다.
2. JWT 사용 시 주의점
⇒ JWT에 민감한 내용을 넣으면 안된다.
- 페이로드 내용을 볼 수 있지만 토큰 변조가 불가능하고, 내용물을 담을 수 있기 때문에 사용한다.
그렇기 때문에 내용물은 노출되어도 좋은 정보만 넣어야 한다.
- 용량이 커서 요청 시 데이터 양이 증가하는 단점이 있다.
10.6 사용량 제한 구현하기
1. 사용량 제한 구현하기
⇒ DOS 공격 등을 대비해야 한다.
- 일정 시간동안 횟수 제한을 두어 무차별적인 요청을 막을 필요가 있다.
- 미들웨어 설치 : npm i express-rate-limit
- apiLimiter : windowMS(기준 시간), max(허용 횟수), handler(제한 초과 시 콜백 함수)
10.7 CORS 이해하기
1. 프런트에서 서버로의 요청
⇒ 요청을 보내는 프런트(localhost:4000), 요청을 받는 서버(localhost:8002)가 다르면 오류가 발생한다.
- CORS : Cross-Origin Resource Sharing 문제
- POST 대신 OPTIONS 요청을 먼저 보내서 서버가 도메인을 허용하는지 미리 체크할 수 있다.
2. CORS 문제 해결 방법
⇒ Access-Control-Allow-Origin 응답 헤더를 넣어주어야 CORS 문제를 해결할 수 있다.
- res.set 메서드에 직접 선언해도 되지만 패키지를 사용하는게 편리하다.
- npm i cors
- v2 라우터에 적용, credentials : true를 해야 프런트와 백엔드 간 쿠키가 공유된다.
9. 프록시 서버
⇒ CORS 문제에 대한 또다른 해결책
- 서버-서버 간의 요청/응답에는 CORS 문제가 발생하지 않는것을 활용해 프록시서버를 구성한다.
- 직접 구현해도 좋지만 http-proxy-middleware 같은 패키지로 손쉽게 연동이 가능하다.
10.8.1 스스로해보기
- 팔로워나 팔로잉 목록을 가져오는 API 만들기(nodebird-api에 새로운 라우터 추가)
- 무료인 도메인과 프리미엄 도메인 간의 사용량 제한을 다르게 적용하기
(apiLimiter를 두 개 만들어서 도메인 별로 다르게 적용. 9.3.1절의 POST /auth/login 라우터 참조)
- 클라이언트용 비밀 키와 서버용 비밀 키를 구분해서 발급하기(Domain 모델 수정)
- 클라이언트를 위해 API 문서 작성하기(swagger나 apidoc 사용)
10.8.2 핵심정리
- API는 다른 어플리케이션의 기능을 사용할 수 있게 해주는 창구이다.
현재 lecture-call이 lecture-api의 api를 사용하고 있다.
- 모바일 서버를 구성할 때 서버를 REST API 방식으로 구현하면 된다.
- API 사용자가 API를 쉽게 사용할 수 있도록 사용 방법, 요청 형식, 응답 내용에 관한 문서를 준비해야 한다.
- JWT 토근의 내용은 공개되며 변조될 수 있다. 단, 시그니처를 확인하면 변조유무를 확인할 수 있다.
- 토큰을 사용해 API의 오남용을 막는다. 요청 헤더에 토큰이 있는지를 항상 확인해야 한다.
- app.use 외에도 router.use를 활용해 라우터 간에 공통되는 로직을 처리할 수 있다.
- cors나 passport.authenticate처럼 미들웨어 내에서 미들웨어를 실행할 수 있다. 미들웨어를 선택적으로 적용하거나
커스터마이징할 때 이 기법을 사용한다.(미들웨어확장기능)
- 브라우저와 서버의 도메인이 다르면 요청이 거절된다는 특성(CORS)를 이해해야 한다.
서버와 서버간의 요청에서는 CORS 요청이 발생하지 않는다.
10.8.3 함께보면 좋은 자료
- API 설명 : https://ko.wikipedia.org/wiki/API
- JWT 토큰 설명 : https://jwt.io
- JSONWebToken 공식 문서 : https://www.npmjs.com/package/jsonwebtoken
- axios 공식 문서 : https://github.com/axios/axios
- CORS 공식 문서 : https://www.npmjs.com/package/cors
- express-rate-limit 공식 문서 : https://www.npmjs.com/package/express-rate-limit
- UUID 공식 문서 : https://www.npmjs.com/package/uuid
- ms 공식 문서 : https://github.com/vercel/ms
'BackEnd > Node' 카테고리의 다른 글
[노드교과서] 섹션 11. 12장 실시간 GIF 채팅방 만들기(웹소켓, Socket.IO) (0) | 2024.01.28 |
---|---|
[노드교과서] 섹션 10. 11장 테스트 해보기(단위, 통합, 부하) (0) | 2024.01.25 |
[노드교과서] 섹션 9. 노드버드 SNS 만들기 (0) | 2024.01.12 |
[노드교과서] 섹션 7. MongoDB (0) | 2024.01.07 |
[노드교과서] 섹션 6. 데이터베이스 (0) | 2024.01.03 |