SOP(동일 출처 정책) / CORS(교차 출처 리소스 공유)
첫 글인 만큼 무엇을 쓸까 고민하다가 제가 웹 공부하면서 처음으로 가장 큰 좌절을 준 CORS에 대해서 이야기 해 보자고 합니다.

SOP 란?
어떤 출처에서 불러온 문서나 스크립트가 다른 출처에서 가져온 리소스와 상호작용하는 것을 제한하는 중요한 보안 방식.
여기서 출처(origin)란, 3가지 요소 (protocol , host , port)중 하나만 틀려도 다른 출처로 인식합니다.
ex) http:www.naver.com/hello/nested/dir
1. https:www.naver.com/hello/nested/dir - ( X ) : protocol mismatch
2. http:m.naver.com/hello/nested/dir - ( X ) : host mismatch
3. http:www.naver.com:80/hello/nested/dir = ( X ) : port mismatch
4. http:www.naver.com/wtf/nested/dir/123123 = ( O ) : SAME ORIGIN
왜 필요할까?
한가지 예시 상황을 들자면,
클라이언트가 A 사이트에 로그인 합니다.
로그인이 성공적일 경우, A사이트에서 클라이언트 측으로, 로그인 구현 방식에 따라 응답을 보냅니다.
그 후, 클라이언트가 A사이트에 방문할때마다, 요청 헤더에 인증관련 정보를 넣어서 보내게 됩니다.
만약 이 상황에서 클라이언트가 불순한 의도를 가진 사이트 B를 방문한다면 어떻게 될까요?
B에 접속 시, A사이트 내 사용자 정보를 조작하는 스크립트가 있다고하면, 공격당하게 됩니다(CSRF | XSRF).

그렇기에, 적어도 사용자를 보호하기 위해서 SOP 정책이 생겨난 겁니다.
그렇다면 CORS 란?
추가 HTTP 헤더를 사용하여, 한 출처에서 실행 중인 웹 애플리케이션이 다른 출처의 선택한 자원에 접근할 수 있는 권한을 부여하도록 브라우저에 알려주는 프로토콜
왜 필요할까?
SOP의 배경은 이제 알겠는데, 그렇다고 교차 출처 요청을 안 할 수도없는게, 이미 사용하는게 너무 흔하기에(소셜 로그인 등..), 교차 출처 요청을 특정 방식으로 요청하면 허용할 수 있겠금 하였는데, CORS 표준을 지킨 요청에 한해 허용하기로 하였습니다. SOP이 성벽이라면 CORS가 문지기 느낌으로 봐도 될거 같습니다.

시나리오
교차 출처 리소스 공유 시, 일어날 수 있는 시나리오가 크게 3가지 있습니다.
1. Simple Request(단순 요청)
단순 요청의 경우, CORS는 사전 요청(preflight)을 하지 않습니다.(사전 요청의 경우 맨 밑에서 설명)
단순 요청에 속하려면 다음 3가지 조건을 만족해야합니다.
1) GET , HEAD , POST 중 하나
2) readableStream X
3) 자동으로 설정된 헤더만 있어야함 (자세한 설명은 공식문서에서 : https://developer.mozilla.org/ko/docs/Web/HTTP/CORS)
- Accept
- Accept-Language
- Content-Language
- ..
// app.tsx
const SERVER = 'http://localhost:8000';
function App() {
const [responseMessage, setResponseMessage] = useState<string | null>();
const fetchToServer = async () => {
const response: AxiosResponse<string> | void = await axios.get(SERVER + '/simple').catch((e) => {
console.log(e);
});
setResponseMessage(response?.data);
};
return (
<div className="App">
<Button onClick={fetchToServer}>Fetch!!</Button>
<h1>message : {responseMessage}</h1>
</div>
);
}
//server.js
app.use((req, res, next) => {
// Access-Control-Allow-Origin 헤더를 설정해주므로써, CORS 시나리오 만족!
res.setHeader('Access-Control-Allow-Origin', '*');
next();
});
const apiRouter = express.Router();
apiRouter.get('/simple', (req, res) => {
console.log('hello');
return res.send('hi from simple');
});
app.use('/', apiRouter);

2. Preflighted Request
단순 요청과 달리, 요청을 전송하기에 안전한지 환경인지 확인하기 위해 사전 요청을 보냅니다. 일반적으로 , 커스텀 header을 사용하거나, UPDATE, DELETE메소드 사용 시, 시행 됩니다.
1. 본 요청 전에 OPTIONS 메서드로 사전 요청을 보냅니다.
2. 응답이 성공적일 경우, 본 요청을 보냅니다.
app.tsx에서 get -> delete으로 바꿔 요청을 보내보겠습니다.


과정 1처럼 사전 요청을 OPTION 방식으로 보낸것을 확인할 수 있는데, 본 요청에 있어서, 에러가 생겼습니다.
이는 서버에서 응답을 보낼 때, 다음과 같이 DELETE 메소드 허용 헤더를 붙여주는 방법으로 해결할 수 있습니다.
app.use((req, res, next) => {
res.setHeader('Access-Control-Allow-Origin', '*');
//DELETE method 만! 허용!
//만약 모든 method 허용시키고 싶으면 와일드카드 '*'로 설정
//복수의 method 허용시키고 싶다면 ','로 구분시켜서 쓰면됩니다 ex) 'POST,GET,DELETE'
res.setHeader('Access-Control-Allow-Methods', 'DELETE');
next();
});
그러면 다음과 같이 성공적인 응답을 얻을 수 있습니다. Response Header에 'Access-Control-Allow-Method' 헤더가 붙은게 보이네요


3. Credentialed Request
해당 요청은, 자격 증명 요소들을 인식하는데, 일반적으로 요청을 보낼 때, 브라우저는 자격 증명을 보내지 않기에, 추가적인 설정을 통하여 보낼 수 있습니다.
먼저 post 요청으로 바꾸고 , 정보를 담은 후, credential 요청을 보내겠습니다. 이전 preflight 요청의 다양한 옵션들을 체험할 겸, 커스텀 헤더도 넣어서 보내겠습니다.
const fetchToServer = async () => {
const response: AxiosResponse<string> | void = await axios
.post(
SERVER + '/credential',
{
ids: 'dovi-id',
passwords: 'dovi-password'
},
{
withCredentials: true,
headers: {
'dovi-header': '0123456789',
Authorization: 'mybearer bearer-token-exampleee'
}
}
)
.catch((e) => {
console.log(e);
});

예상했던대로, 에러가 뜹니다.
Access-Control-Allow-Credentials 헤더와 커스텀 헤더에 대해 대응되는 헤더를 추가함으로써 해결할 수 있습니다.
//server.js
app.use((req, res, next) => {
//credential request시, 와일드 카드 사용 x
res.setHeader('Access-Control-Allow-Origin', 'http://127.0.0.1:5173');
//credential 요청 허용
res.setHeader('Access-Control-Allow-Credentials', 'true');
res.setHeader('Access-Control-Allow-Methods', '*');
//body에 데이터 담으면 , Content-type , content-length header 필요합니다 , app.tsx에서 커스텀 헤더인 dovi-header을 넣었으므로, 해당 헤더도 허용해 줍시다
res.header(
'Access-Control-Allow-Headers',
'Content-Type, Authorization, Content-Length, X-Requested-With, dovi-header'
);
next();
});
