CORS
API를 사용하는 서버에서의 호출만 다뤘었다. 만약 프런트에서 호출한다면 어떻게 해야 할까??
CORS를 이용하여 처리하자.
router.get('/', (req, res) => {
res.render('main', {key: process.env.CLIENT_SECRET});
});
nodecat/routes/index.js
프런트 화면을 렌더링하는 라우터를 추가했다.
프런트 화면도 추가하자.
<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.log(err);
});
</script>
</body>
</html>
nodecat/views/main.html
clientSecret의 {{key}} 부분이 넌적스에 의해 실제 키로 치환돼서 렌더링된다.
Access-Control-Allow-Origin이라는 헤더가 없다는 내용의 에러이다.
브라우터와 서버의 도메인이 일치하지 않으면, 기본적으로 요청이 차단된다. 이러한 문제를 CORS문제라고 한다.
API 서버 콘솔에 OPTIONS 요청이 기록된다, OPTIONS 메서드는 실제 요청을 보내기 전에 서버가 이 도메인을 허용하는지 체크하는 역할이다.
CORS문제를 해결하기 위해 응답 헤서에 Access-Control-Allow-Origin 헤더를 넣어야 한다.
npm i cors
const express = require('express');
const jwt = require('jsonwebtoken');
const cors = require('cors');
const { verifyToken, apiLimiter } = require('./middlewares');
const { Domain, User, Post, Hashtag } = require('../models');
const router = express.Router();
router.use(cors({
credentials: true,
}));
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((err) => {
console.error(err);
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(err){
console.error(err);
return res.status(500).json({
code: 500,
message: '서버 에러',
});
}
});
module.exports = router;
nodebird-api/routes/v2.js
v2에 적용하자. credential: true라는 옵션은 다른 도메인 간에 쿠키를 공유하는 옵션이다.
요청이 성공했다.
Access-Control-Allow-Origin이 *로 되어 있다. *는 모든 클라이언트의 요청을 허용한다는 뜻이다.
하지만 지금 비밀키(process.env.CLIENT_SECRET)가 모두에게 노출되어있다.
이 문제를 막기 위해 처음엔 비밀 키 발급 시 허용한 도메인을 적게 하자. 호스트와 비밀 키가 모두 일치할 때만 CORS를 허용하게 수정하자.
const express = require('express');
const jwt = require('jsonwebtoken');
const cors = require('cors');
const url = require('url');
const { verifyToken, apiLimiter } = require('./middlewares');
const { Domain, User, Post, Hashtag } = require('../models');
const router = express.Router();
router.use(cors({
credentials: true,
}));
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();
}
});
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((err) => {
console.error(err);
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(err){
console.error(err);
return res.status(500).json({
code: 500,
message: '서버 에러',
});
}
});
module.exports = router;
nodebird-api/routes/v2.js
먼저 도메인 모델로 클라이언트의 도메인(req.get('origin'))과 호스트가 일치하는 것이 있는지 검사한다.
http나 https 같은 프로토콜을 떼어낼 때는 url.parse 메서드를 사용한다. 일치하는 것이 있다면 cors를 허용해서 다음 미들웨어로 보낸다.
localhost 값이 http://localhost:4000으로 적용되어 있다.