예전에는 출처(프로토콜, 호스트명, 포트)가 다르더라도 서버 요청과 응답이 가능했다. 하지만 이러한 방식에는 보안에 취약하다는 문제가 있었다.
실제로 해커들은 이러한 보안 취약점을 해킹에 사용하였다. 해킹 방식에는 여러 가지가 있다. (자세한 설명)
Cross-Site Scripting (XSS)
1. 사용자의 쿠키 정보를 가져오는 자바스크립트 코드(악성코드)를 포함한 게시글을 작성한다.
2. 어떤 사용자가 로그인하면 쿠키에 그 정보가 저장됨
3. 해커가 작성해둔 게시글을 사용자가 클릭했을 때 악성코드가 작동
4. 쿠키에 저장된 사용자 정보가 해커의 웹사이트에 전송됨
Cross-site request forgery (CSRF) - 사이트 간 요청 위조
1. 사용자가 보안이 취약한 서비스에 로그인 한다.
2. 사용자 정보가 쿠키에 저장된다.
3. 해커가 만든 피싱 사이트에 접속한다. (클릭 유도, 위조된 광고 등을 통해)
4. 해커가 원하는 대로 사용자 정보가 바뀐다.
서로 다른 출처에서도 서버 요청과 응답이 가능하다는 게 원인이기 때문에 브라우저는 이를 불가능하게 만드는 방식을 도입했다. 바로 SOP(동일 출처 정책)이다.
위에서 설명했듯이 기존에는 보안상의 이유로 다른 출처로부터 리소스를 가져오는 것이 불가능했다. (SOP, Same-Origin-Policy) 이 전의 웹 브라우저에서 서버와 통신하는 방법은 아주 심플했다. 브라우저에서 해당 페이지에 대한 리소스를 요청하면 서버에서 해당 문서(HTML)를 보내주는 방식.
따라서 다른 출처로 리소스를 요청한다는 것은 피싱이나 정보 유출 등의 악의적인 행위로 간주했다.
최신 웹사이트는 이전과 다르게 단순 문서만 제공하는 것이 아닌, 다양한 기능을 제공하는 웹 애플리케이션 역할을 한다.
예를 들어 네이버 API 서버에서 데이터를 요청한다고 해보자. 그러면 API 서버 -> 웹 서버 -> 브라우저
와 같은 단계보다는 API 서버 -> 브라우저
와 같은 심플한 단계였으면 좋겠다는 생각이 들 것이다.
하지만 기존 브라우저 보안 정책에 의해 도메인이 다른 네이버 API와 다이렉트로 통신이 불가할 것이다.
이를 해결하기 위해 개발자들은 JSONP라는 우회적인 방법을 사용하기도 했다. 하지만 이 방법은 보안 정책을 무력화하기 때문에 브라우저 입장에서도 위험하다고 판단할 것이다.
결국 브라우저에서 우회하지 않아도 사용할 수 있는 공식적인 루트를 제공하였으니, 바로 CORS이다.
CORS(교차 출처 리소스 공유)란, 다른 출처에서 리소스 요청 시 접근 권한을 부여하는 메커니즘이다.
여기에서 출처란 프로토콜, 호스트명, 포트를 말한다. 이 중 하나라도 다르면 다른 출처로 인식한다.
요청 헤더에 Origin 정보를 담아서 보낸다.
서버에서 해당 요청에 대한 응답 시 응답 헤더에 Access-Control-Allow-Origin 정보를 담아 보낸다.
요청할 때 보낸 Origin 정보와 서버에서 보낸 Access-Control-Allow-Origin 정보를 비교해 서버에서 보내준 Access-Control-Allow-Origin을 차단할지 말지 클라이언트 단에서 정한다.
유효하지 않다면 해당 응답은 버린다.
위 방식은 아주 기본적인 동작 방식이며, 실제로는 예비 요청(preflighted request), 단순 요청(simple request), 인증된 요청(Credentialed Request)세 가지 방식으로 나뉜다.
브라우저는 다른 출처로 요청을 보낼 때 다 같은 방식으로 동작하지 않는다.
(자세한 설명은 여기)
CORS 이슈는 외부 API 서버에서 데이터를 가지고 올 때 헤더에 접근을 허락하는 내용이 없으면 발생한다.
단순히 로컬 환경에서 API 요청 시의 CORS 이슈를 해결하고 싶다면, 크롬 확장 프로그램인 Allow CORS: Access-Control-Allow-Origin을 설치하면 된다.
로컬에서 뿐만 아니라 서버에서부터 근본적인 해결을 하고 싶다면 Access-Control-Allow-Origin를 설정해주면 된다.
(server.js)
app.express.get('/login', (req, res) => {
...
// 모든 출처를 허락하므로 비추
res.setHeader('Access-Control-Allow-origin', '*');
// 특정 도메인만 허락하므로 추천
res.setHeader('Access-Control-Allow-origin', 'https://openapi.naver.com');
// 쿠키 주고받기 허용
res.setHeader('Access-Control-Allow-Credentials', 'true');
});
위와 같이 요청에 대한 응답 헤더를 직관적으로 설정해줄 수 있다. 위 헤더는 '클라이언트 도메인 요청을 허락하겠다'라는 의미이다.
cors 미들웨어를 설치하면 모든 응답에 setHeader를 해주지 않아도 설정한 헤더를 자동으로 추가하여 내보내준다.
설치
npm install cors
적용
const cors = require("cors");
// 옵션
let corsOptions = {
// 해당 출처 허용
origin: 'https://openapi.naver.com',
credentials: true // 사용자 인증 리소스 접근
}
app.use(cors(corsOptions));
서버 간에 출처가 다른 경우
credentials: true
로 설정하지 않으면 다른 출처 간에 쿠키를 공유 하지 못하게 되며 로그인 되지 않을 수 있다.
CORS는 브라우저 보안 정책이므로 서버에서 서버로 요청 시 CORS 이슈가 발생하지 않는다.
서버에 대한 지식이 있다면 이 방식을 통해 간단히 해결할 수 있다.
우선 클라이언트에서 요청 가능한 API를 하나 만들어준다.
(client)
const request = axios('/api/search')
.then(response => response.data)
(server.js)
app.get('/api/search', (req, res) => {
// 외부 API에 요청
axios.get('https://openapi.naver.com/v1/search/movie.json', {
params: {
...
},
headers: {
...
}
}).then((response) => {
res.send(data);
}).catch((error) => {
alert(error.message);
})
})
서버와 서버 간 요청은 CORS 이슈가 발생하지 않는다는 것을 이용한 방식이다.
클라이언트에서 다이렉트로 서버로 요청 시 CORS 이슈가 발생하므로 중간에 Proxy 서버를 두어 해당 이슈가 발생하지 않도록 하는 방법이다.
클라이언트 - Proxy 서버 - 백엔드 서버
proxy 서버를 쉽게 만들 수 있는 npm 패키지 설치
npm install http-proxy-middleware
setupProxy.js 파일 생성하여 클라이언트와 같은 도메인의 프록시 서버 생성
(setupProxy.js)
const { createProxyMiddleware } = require('http-proxy-middleware');
module.exports = function(app) {
app.use(
'/api',
createProxyMiddleware({
target: 'http://localhost:5000',
changeOrigin: true,
})
);
};
- 클라이언트에서
/api
로 시작되는 요청http://localhost:5000
프록시 서버 생성- 프록시 '서버'가 백엔드 '서버'에 요청