티스토리 뷰
이 장에서는 NodeBird 앱의 REST API 서버를 만든다. API 서버는 프런트엔드와 분리되어 운영되므로 모바일 서버로도 사용할 수 있다. 노드를 모바일 서버로 사용하려면 이번 장과 같이 서버를 REST API 구조로 구성하면 된다. 특히 JWT 토큰은 모바일 앱과 노드 서버 간에 사용자 인증을 구현할 때 자주 사용된다.
API(Application Programming Interface) : 다른 애플리케이션에서 현재 프로그램의 기능을 사용할 수 있게 허용하는 접점
웹 API는 다른 웹 서비스의 기능을 사용하거나 자원을 가져올 수 있는 창구.
위와 같은 서버에 API를 올려서 URL을 통해 접근할 수 있게 만든 것을 웹 API 서버라고 한다.
크롤링 : 웹 사이트가 자체적으로 제공하는 API가 없거나 API 이용에 제한이 있을 때 사용하는 방법
표면적으로 보이는 웹 사이트의 정보를 일정 주기로 수집해 자체적으로 가공하는 기술. 하지만 웹 사이트에서 직접 제공하는 API가 아니므로 원하는 정보를 얻지 못할 가능성이 있다. 또한, 웹 사이트에서 제공하길 원치 않는 정보를 수집한다면 법적인 문제가 발생할 수도 있다. 서비스 제공자 입장에서도 주기적으로 크롤링을 당하면 웹 서버의 트래픽이 증가하므로 공개해도 되는 정보들은 API로 만들어 API를 통해 가져가게 하는 것이 좋다.
type: {
type: Sequelize.ENUM('free', 'premium'),
allowNull: false,
},
clientSecret: {
type: Sequelize.UUID,
allowNull: false,
},
ENUM은 넣을 수 있는 값을 제한하는 데이터 형식.
UUID는 충돌 가능성이 매우 적은 랜덤한 문자열
JWT 토큰
JSON Web Token : JSON 형식의 데이터를 저장하는 토큰
헤더 : 토큰 종류와 해시 알고맂므 정보가 들어 있다.
페이로드 : 토큰의 내용물이 인코딩된 부분
시그니처 : 일련의 문자열, 시그니처를 통해 토큰이 변조되었는지 여부를 확인
JWT 토큰은 비밀키를 알지 않는 이상 변조가 불가능하다. 즉, 사용자 이름, 권한 같은 것을 넣어두고 사용할 수 있다. 단, 외부에 노출되어도 좋은 정보만. 이러면 DB조회없이 권한 주는것가능
단점은 용량이 크다는 것이다. 매 요청 시 토큰이 오고 가서 데이터양이 증가한다.
.env
JWT_SECRET=jwtSecret
middlewares.js
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: '유효하지 않은 토큰입니다.',
});
}
};
verify 첫번째 인수는 토큰, 두번째 인수는 토큰의 비밀키
인증에 성공한다면 req.decoded에 저장.
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.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: 1500,
message: '서버 에러',
});
}
});
router.get('/test', verifyToken, (req, res) => {
res.json(req.decoded);
});
module.exports = router;
sign 메서드의 첫번째 인수는 토큰의 내용
두번째 인수는 토큰의 비밀키
세번째 인수는 토큰의 설정
* JWT 토큰 로그인
최근에는 JWT 토큰으로 로그인하는 방법이 많아지고 있다. 로그인 완료 시 세션에 데이터를 저장하고 세션 쿠키를 발급하는 대신 JWT 토큰을 쿠키로 발급하면 된다.
다음과 같이 authenticcate 메서드의 두번째 인수로 옵션을 주면 세션을 사용하지 않을 수 있다.
router.post('/login', isNotLoggedIn, (req,res,next)=>{
passport.authenticate('local', {session:false}, (authError,user,info)=>{
if(authError){
세션에 데이터를 저장하지 않으므로 serializeUser와 deserializeUser는 사용하지 않는다. 그 후 모든 라우터에 verifyToekn 미들웨어를 넣어 클라이언트에서 쿠키를 검사한 후 토큰이 유효하면 라우터로 넘어가고, 그렇지 않으면 401이나 419 에러를 응답하면 된다.
사용자 권한 확인을 위해 데이터베이스를 사용하지 않으므로 서비스의 규모가 클수록 데이터베이스의 부담을 줄일 수 있다.
nodecat
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;
4000/test
// 20210226234930
// http://localhost:4000/test
{
"id": 2,
"nick": "123",
"iat": 1614350970,
"exp": 1614351030,
"iss": "nodebird"
}
사용량 제한하기
npm i express-rate-limit
const RateLimit = require('express-rate-limit');
exports.apiLimiter = new RateLimit({
windowMs: 60 * 1000, // 1분
max: 1,
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: '새로운 버전이 나왔습니다. 새로운 버전을 사용하세요.',
});
};
이 미들웨어의 옵션으로는 windowMs(기준 시간), max(허용 횟수), delayMs(호출 간격), handler(제한 초과 시 콜백 함수) 등이 있다.
사용량 제한이 추가되었으므로 기존 API 버전과 호환X
새로운 v2라우터를 만들어야 해
v1으로 접속하면 deprecated use해서 알려주고
현재는 nodebird-api 서버가 재시작되면 사용량이 초기화되므로 실제 서비스에서 사용량을 저장할 데이터베이스를 따로 마련하는 것이 좋다. 보통 레디스가 많이 사용. 단, express-rate-limit은 데이터베이스와 연결되지 않음
CORS
routes/index.js
router.get('/', (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.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>
실제 서비스에서는 서버에서 사용하는 비밀키와 프런트에서 사용하는 비밀키를 따로 두는 게 좋다.
NodeCat의 프런트에서 nodebird-api의 서버 API를 호출하면 오류가 뜬다.
Access-Control-Allow-Origin이라는 헤더가 없다는 내용의 에러이다. 이처럼 브라우저와 서버의 도메인이 일치하지 않으면 기본적으로 요청이 차단된다. 이 현상은 브라우저에서 서버로 요청을 보낼때만 발생한다.
nodebird-api에서 npm i cors
v2.js
const cors = require('cors');
const router = express.Router();
router.use(
cors({
credentials: true,
})
);
네트워크에서 token을 보면 Access-Control-Allow-Origin:*
*은 모든 클라이언트의 요청을 허용한다는 뜻
이러면 비밀키가 모두에게 요청돼
수정
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();
}
});
req.get('origin'),
http://localhost:4000
url.parse(req.get('origin')),
Url {
protocol: 'http:',
slashes: true,
auth: null,
host: 'localhost:4000',
port: '4000',
hostname: 'localhost',
hash: null,
search: null,
query: null,
pathname: '/',
path: '/',
href: 'http://localhost:4000/'
}
url.parse(req.get('origin')).host
localhost:4000
cors 미들웨어에 옵션 인수를 주었는데 origin 속성에 허용할 도메인만 따로 적으면 된다.
여러 개의 도메인을 허용하고 싶다면 배열을 사용
또 cors 미들웨어에도 (req,res,next) 인수를 줘서 호출했다. 이는 미들웨어의 작동 방식을 커스터마이징하고 싶을 때 사용하는 방법(passport에서도 사용했어) 아래 코드 같다는걸 기억하자
router.use(cors());
rou0ter.use((req,res,next)=>{
cors()(req,res,next);
});
현재 클라이언트와 서버에서 같은 비밀키를 써서 문제가 될 수 있어. 카카오처럼 환경별로 키를 구분하자
* 프록시 서버
CORS 문제를 해결하는 또 다른 방법.
서버에서 서버로 요청할때는 CORS 문제가 발생하지 않는다는 것을 이용
추천할것
팔로워나 팔로잉 목록을 가져오는 api 만들기(nodebird-api에 새로운 라우터 추가)
무료 도메인과 프리미엄 도메인 간에 사용량 제한을 다르게 적용하기(apiLimiter를 두 개 만들어서 도메인별로 다르게 적용)
클라이언트용 비밀 키와 서버용 비밀 키를 구분해서 발급하기(domain 모델 수정)
클라이언트를 위해 api 문서 작성하기(swagger나 apidoc 사용)
'책 > nodejs 교과서' 카테고리의 다른 글
12. 웹 소켓으로 실시간 데이터 전송하기 (0) | 2021.03.04 |
---|---|
11. 노드 서비스 테스트하기 (0) | 2021.03.02 |
9. 익스프레스로 SNS 서비스 만들기 (1) | 2021.02.25 |
8. 몽고디비 (0) | 2021.02.24 |
7. MYSQL (0) | 2021.02.22 |