HTTP 기본 포스트
먼저 위 포스트를 읽어본 후 온다면 도움이 될 것이다.
1. 개념과 구조
SOP은 Same-Origin Policy의 줄임말로, 동일 출처 정책을 뜻한다.
- 한 마디로 ‘같은 출처의 리소스만 공유가 가능하다’라는 정책인데여기서 말하는 ‘출처(Origin)’는 다음과 같습니다.
- 출처는 프로토콜, 호스트, 포트의 조합으로 되어있으며
이 중 하나라도 다르면 동일한 출처로 보지 않습니다https://www.code.com:443/user
프로토콜, 호스트, 포트며 /user을 제외한 부분이 출처(origin)이다.2. 역할
- 잠재적으로 해로울 수 있는 문서를 분리함으로써 공격받을 수 있는 경로를 줄여준다.
즉, 출처가 다른 사이트와의 리소스 공유를 제한하여 정보가 새어나가는 것과 같은 문제를 방지할 수 있다.
CORS 정의
CORS는 Cross-Origin Resource Sharing의 줄임말로 교차 출처 리소스 공유를 뜻한다.
MDN에서의 정의
교차 출처 리소스 공유(Cross-Origin Resource Sharing, CORS)는 추가 HTTP 헤더를 사용하여, 한 출처에서 실행 중인 웹 애플리케이션이 다른 출처의 선택한 자원에 접근할 수 있는 권한을 부여하도록 브라우저에 알려주는 체제입니다.
위와 같이 CORS를 통해 다른 출처의 리소스를 받아 올 수 있게 된다.
CORS의 역할
브라우저는 SOP에 의해 기본적으로 다른 출처의 리소스 공유를 막지만, CORS를 사용하면 접근 권한을 얻을 수 있게 되는 것이다.
CORS policy에러는 SOP 때문에 발생하며 이를 해결해 주는 것이 CORS다!
기본적으로 CORS 설정을 통해 서버의 응답 헤더에 ‘Access-Control-Allow-Origin’을 작성하면 접근 권한을 얻을 수 있게 된다.
1. 프리플라이트 요청 (Preflight Request)
실제 요청을 보내기 전, OPTIONS 메서드로 사전 요청을 보내 해당 출처 리소스에 접근 권한이 있는지 먼저 확인하는 것이다.
잠시 HTTP 요청과 응답 헤더를 알아보자
- In CORS 에서, OPTIONS 메소드를 통해 프리플라이트 요청 (preflight, 사전 전달), 즉 사전 요청을 보내면 서버가 해당 parameters를 포함한 요청을 보내도 되는지에 대한 응답을 줄 수 있게 한다.
프리플라이트 HTTP 요청 헤더
- cross-origin 공유 기능을 사용하기 위해 클라이언트가 HTTP 요청을 발행할 때 사용할 수 있는 헤더를 보자.
이 헤더는 서버를 호출할 때 설정됩니다
1. Origin 헤더Origin: <origin>
- Origin 헤더는 cross-site 접근 요청 또는 preflight request의 출처를 나타낸다.
- origin 은 요청이 시작된 서버를 나타내는 URI 이며 경로 정보는 포함하지 않고, 오직 서버 이름만 포함한다.
- 접근 제어 요청에는 항상 Origin 헤더가 전송된다.
2. Access-Control-Request-Method 헤더- 제 요청에서 어떤 HTTP 메서드를 사용할지 서버에게 알려주기 위해, preflight request 할 때에 사용된다.
3. Access-Control-Request-Headers 헤더- 실제 요청에서 어떤 HTTP 헤더를 사용할지 서버에게 알려주기 위해, preflight request 할 때에 사용 된다.
// preflight request 요청 헤더의 예시 OPTIONS /resources/post-here/ HTTP/1.1 Host: bar.other User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.14; rv:71.0) Gecko/20100101 Firefox/71.0 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8 Accept-Language: en-us,en;q=0.5 Accept-Encoding: gzip,deflate Connection: keep-alive Origin: http://foo.example //1번 Access-Control-Request-Method: POST //2번 Access-Control-Request-Headers: X-PINGOTHER, Content-Type //3번
프리플라이트 HTTP 응답 헤더
- Cross-Origin 리소스 공유 명세에 정의된 대로 서버가 접근 제어 요청을 위해 보내는 HTTP 응답 헤더에 대한 내용이다.
1. Access-Control-Allow-Origin 헤더
Access-Control-Allow-Origin: <origin> | *
- 리턴된 리소스에는 위 구문과 함께 하나의 Access-Control-Allow-Origin 헤더가 있을 수 있다.
- Access-Control-Allow-Origin 은 단일 출처를 지정하여 브라우저가 해당 출처가 리소스에 접근하도록 허용한다.
또는 자격 증명이 없는 요청의 경우 "*" 와일드 카드는 브라우저의 origin에 상관없이 모든 리소스에 접근하도록 허용한다.
2. Access-Control-Max-Age (en-US) 헤더Access-Control-Max-Age: <delta-seconds>
- preflight request 요청 결과를 캐시할 수 있는 시간을 나타낸다.
- delta-seconds 파라미터는 결과를 캐시할 수 있는 시간(초)를 나타낸다.
3.Access-Control-Allow-Credentials 헤더Access-Control-Allow-Credentials: true
- redentials 플래그가 true일 때 요청에 대한 응답을 표시할 수 있는지를 나타낸다.
- preflight request에 대한 응답의 일부로 사용하는 경우, credentials을 사용하여 실제 요청을 수행할 수 있는지를 나타낸다.
4. Access-Control-Allow-Methods 헤더Access-Control-Allow-Methods: <method>[, <method>]*
- 리소스에 접근할 때 허용되는 메서드를 지정하며
이 헤더는 preflight request에 대한 응답으로 사용된다.
5. Access-Control-Allow-Headers 헤더Access-Control-Allow-Headers: <header-name>[, <header-name>]*
- preflight request 에 대한 응답으로 Access-Control-Allow-Headers 헤더가 사용된다.
실제 요청시 사용할 수 있는 HTTP 헤더를 나타낸다.// preflight response 응답 헤더 예시 HTTP/1.1 204 No Content Date: Mon, 01 Dec 2008 01:15:39 GMT Server: Apache/2 Access-Control-Allow-Origin: https://foo.example Access-Control-Allow-Methods: POST, GET, OPTIONS Access-Control-Allow-Headers: X-PINGOTHER, Content-Type Access-Control-Max-Age: 86400 Vary: Accept-Encoding, Origin Keep-Alive: timeout=2, max=100 Connection: Keep-Alive
오늘 다룰 내용 중 SOP과 CORS에 관련된 내용으로는 간단한 접근 제어 프로토콜은 Origin 헤더와 Access-Control-Allow-Origin 을 사용하는 것이다
참고: MDN
성공 예시
브라우저는 서버에 실제 요청을 보내기 전에 프리플라이트 요청을 보내고, 응답 헤더의 Access-Control-Allow-Origin으로 요청을 보낸 출처가 돌아오면 실제 요청을 보내게 된다.
실패 예시
요청을 보낸 출처가 접근 권한이 없다면 브라우저에서 CORS 에러를 띄우게 되고, 실제 요청은 전달되지 않는다.
실제 요청을 보내기 전에 미리 권한 확인을 할 수 있기 때문에, 실제 요청을 처음부터 통째로 보내는 것보다 리소스 측면에서 효율적임
CORS에 대비가 되어있지 않은 서버를 보호할 수 있다.
CORS에 대비가 되어있지 않은 서버라도 프리플라이트 요청을 먼저 보내게 되면, 프리플라이트 요청에서 CORS 에러를 띄우게 되어 PUT과 DELETE같은 실제 요청이 서버에서 수행되지 않게 막아줄 수 있다.
2. 단순 요청 (Simple Request)
특정 조건이 만족되면 프리플라이트 요청을 생략하고 요청을 보내는 것이다.
조건
- GET, HEAD, POST 요청 중 하나여야 한다.
- 자동으로 설정되는 헤더 외에, Accept, Accept-Language, Content-Language, Content-Type 헤더의 값만 수동으로 설정할 수 있다.
- Content-Type 헤더에는 application/x-www-form-urlencoded, multipart/form-data, text/plain 값만 허용 된다.
3. 인증정보를 포함한 요청 (Credentialed Request)
#### 이 내용은 추후에 포스팅이 필요하다.(링크 필요)
요청 헤더에 인증 정보를 담아 보내는 요청이다.
출처가 다를 경우에는 별도의 설정을 하지 않으면 쿠키를 보낼 수 없다(민감한 정보이기 때문).
이 경우에는 프론트, 서버 양측 모두 CORS 설정이 필요하다.
- 프론트 측에서는 요청 헤더에 withCredentials : true 를 넣어줘야 한다.
- 서버 측에서는 응답 헤더에 Access-Control-Allow-Credentials : true 를 넣어줘야 한다.
- 서버 측에서 Access-Control-Allow-Origin 을 설정할 때, 모든 출처를 허용한다는 뜻의 와일드카드(*)로 설정하면 에러가 발생 한다.
인증 정보를 다루는 만큼 출처를 정확하게 설정해야 한다.
--> 이부분을 알아야 될 것 같으니 추가적으로 공부하자
const http = require('http');
const server = http.createServer((request, response) => {
// 모든 도메인
response.setHeader("Access-Control-Allow-Origin", "*");
// 특정 도메인
response.setHeader("Access-Control-Allow-Origin", "https://codestates.com");
// 인증 정보를 포함한 요청을 받을 경우
response.setHeader("Access-Control-Allow-Credentials", "true");
})
const cors = require("cors");
const app = express();
//모든 도메인
app.use(cors());
//특정 도메인
const options = {
origin: "https://codestates.com", // 접근 권한을 부여하는 도메인
credentials: true, // 응답 헤더에 Access-Control-Allow-Credentials 추가
optionsSuccessStatus: 200, // 응답 상태 200으로 설정
};
app.use(cors(options));
//특정 요청
app.get("/example/:id", cors(), function (req, res, next) {
res.json({ msg: "example" });
});
이 외 다양한 개발 환경에서도, 헤더의 값을 설정하는 방법만 알면 CORS 설정을 해줄 수 있다.
class App {
init() {
document
.querySelector('#to-upper-case')
.addEventListener('click', this.toUpperCase.bind(this));
document
.querySelector('#to-lower-case')
.addEventListener('click', this.toLowerCase.bind(this));
}
post(path, body) {
fetch(`http://localhost:4999/${path}`, {
method: 'POST',
body: JSON.stringify(body),
headers: {
'Content-Type': 'application/json'
}
})
.then(res => res.json())
.then(res => {
this.render(res);
});
}
toLowerCase() {
const text = document.querySelector('.input-text').value;
this.post('lower', text);
}
toUpperCase() {
const text = document.querySelector('.input-text').value;
this.post('upper', text);
}
render(response) {
const resultWrapper = document.querySelector('#response-wrapper');
document.querySelector('.input-text').value = '';
resultWrapper.innerHTML = response;
}
}
const app = new App();
app.init();
const http = require('http');
const PORT = 4999;
const ip = 'localhost';
//서버 생성
const server = http.createServer((request, response) => {
// request객체의 유용한 프로퍼티
// const { method, url, headers } = request;
if (request.method === 'OPTIONS') {
response.writeHead(200, defaultCorsHeader);
response.end('hello mini-server sprints');
return;
}
let body = [];
if (request.method === 'POST' && request.url === '/lower') {
// 요청 바디 받기는
// 스트림의 'data'와 'end' 이벤트에 이벤트 리스너를 등록해서 데이터를 받을 수 있습니다.
//각 'data' 이벤트에서 발생시킨 청크는 Buffer입니다.
//이 청크가 문자열 데이터라는 것을 알고 있다면 이 데이터를 배열에 수집한 다음
//'end' 이벤트에서 이어 붙인 다음 문자열로 만드는 것이 가장 좋습니다.
request
.on('data', (chunk) => {
body.push(chunk);
})
.on('end', () => {
// 여기서 `body`에 전체 요청 바디가 문자열로 담겨있습니다.
body = Buffer.concat(body).toString().toLowerCase();
response.writeHead(200, defaultCorsHeader);
response.end(body);
return;
});
} else if (request.method === 'POST' && request.url === '/upper') {
request
.on('data', (chunk) => {
body.push(chunk);
})
.on('end', () => {
body = Buffer.concat(body).toString().toUpperCase();
response.writeHead(200, defaultCorsHeader);
response.end(body);
return;
});
} else {
//request 오류 처리
request.on('error', (err) => {
response.writeHead(404, defaultCorsHeader);
console.error(err, 'bad request');
});
}
console.log(`http request method is ${request.method}, url is ${request.url}`);
});
// 요청을 실제로 처리되기위해 listen메서드가 server객체에서 호출되어야함
//서버 활성화
server.listen(PORT, ip, () => {
console.log(`http server listen on ${ip}:${PORT}`);
});
const defaultCorsHeader = {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type, Accept',
'Access-Control-Max-Age': 10,
};
참고 : nodejs.org