...
🤬 CORS를 허용했는데도 쿠키가 넘어가지 않는 현상
보통 웹을 구성할때 리액트(React)나 뷰(Vue)와 같은 라이브러리 / 프레임워크를 사용한다면 따로 프론트 서버를 실행하여 개발하게 된다. 만일 클라이언트 서버가 http://localhost:3000 이고 API 서버가 http://localhost:8080 이라고 가정하자. 서로 같은 Host이고 Port만 다른 셈이다. 로그인 화면을 구성을 완료했고 테스트를 위해 axios로 로그인 요청을 서버에 보냈다.
axios.post('http://localhost:8080/login', {
profile: { username: username, password: password }
})
하지만 결과는 로그인 성공이 아닌 다음과 같은 시뻘건 CORS 에러가 반겨주었다.
또한 개발자 도구를 열어보면 서버로부터 생성된 세션 ID가 들은 쿠키값도 없다. 분명 서버에서 따로 Access-Control-Allow-origin 헤더 값을 모든 출처를 허용으로 설정했음에도 불구하고 왜 이런 에러가 뜨는 것일까?
🍪 withCredentials 옵션으로 쿠키 보내기
Credentials 이란 쿠키, Authorization 인증 헤더, TLS client certificates(증명서)를 내포하는 자격 인증 정보를 말한다.
기본적으로 브라우저가 제공하는 요청 API 들은 별도의 옵션 없이 브라우저의 쿠키와 같은 인증과 관련된 데이터를 함부로 요청 데이터에 담지 않도록 되어있다. 이는 응답을 받을때도 마찬가지이다. 따라서 요청과 응답에 쿠키를 허용하고 싶을 경우, 이를 해결하기 위한 옵션이 바로 withCredentials 옵션이다.
withCredentials 옵션은 단어 그대로, 다른 도메인(Cross Origin)에 요청을 보낼 때 요청에 인증(credential) 정보를 담아서 보낼 지를 결정하는 항목이다. 즉, 쿠키나 인증 헤더 정보를 포함시켜 요청하고 싶다면, 클라이언트에서 API 요청 메소드를 보낼때 withCredentials 옵션을 true로 설정해야한다. 또한 인증된 요청을 정상적으로 수행하기 위해선 클라이언트 뿐만 아니라 서버에서도 Access-Control-Allow-Credentials 헤더를 true로 함으로써 인증 옵션을 설정해주어야 한다.
정리하자면 클라이언트나 서버나 둘다 Credentials 부분을 true로 설정해줘야 한다는 말이다.
- 표준 CORS요청은 기본적으로 쿠키를 설정하거나 보낼 수 없다.
- 프론트에서 ajax 요청할 때,
withCredentials부분을 true로 해서 수동으로 CORS 요청에 쿠키값을 넣어줘야 한다. - 마찬가지로 서버도 응답헤더에
Access-Control-Allow-Credentials를 true로 설정해야 한다.
1. 클라이언트 처리 부분
어떤 메소드나 라이브러리를 사용하느냐에 따라 withCredentials 옵션을 활성화하는 문법이 다르다. 따라서 이번 포스팅에서는 여러 ajax요청 방법을 모두 소개해 본다.
axios 설정
withCredentials 옵션 부분을 axios 전역 설정으로 처리하거나, axios 요청 메소드의 옵션 인자로 넣어 보낼수 있다. (둘중 택)
// 1. axios 전역 설정
axios.defaults.withCredentials = true; // withCredentials 전역 설정
// 2. axios 옵션 객체로 넣기
axios.post(
'https://example.com:1234/users/login',
{ profile: { username: username, password: password } },
{ withCredentials: true }
).then(response => {
console.log(response);
console.log(response.data);
})
fetch 설정
만일 axios가 아닌 fetch 메서드로 api 요청을 하고 싶다면 다음과 같이 구성한다. (문법이 약간 다르다)
fetch("https://example.com:1234/users/login", {
method: "POST",
credentials: "include", // 클라이언트와 서버가 통신할때 쿠키 값을 공유하겠다는 설정
})
jQuery 설정
$.ajax({
url: "https://example.com:1234/users/login",
type: "POST",
contentType: "application/json; charset=utf-8",
dataType: "json",
xhrFields: {
withCredentials: true // 클라이언트와 서버가 통신할때 쿠키 값을 공유하겠다는 설정
},
success: function (retval, textStatus) {
console.log( JSON.stringify(retval));
}
error: function () {
console.log("error");
}
});
XMLHttpRequest 객체
const xhr = new XMLHttpRequest();
xhr.open('GET', 'http://example.com/', true);
xhr.withCredentials = true;
xhr.send(null);
2. 서버 처리 부분
credential 정보가 포함되어 있는 요청이 정상적으로 처리되기 위해서는, 해당 요청을 받는 서버 측에서도 다음과 같은 설정이 필요하다. 만일 서버에서 별도의 처리 없이 클라이언트 부분의 withCredentials 옵션만 활성화 한채 서버에 cors 요청하게 되면 모두 거부가 된다.
이때 서버에서 응답 헤더를 설정하는데 있어 몇가지 제약이 있어 주의하여야 한다.
- 응답 헤더의
Access-Control-Allow-Credentials항목을 true로 설정 - 응답 헤더의
Access-Control-Allow-Origin의 값에 와일드카드 문자("*")는 보안상 사용할 수 없다. - 응답 헤더의
Access-Control-Allow-Methods의 값에 와일드카드 문자("*")는 보안상 사용할 수 없다. - 응답 헤더의
Access-Control-Allow-Headers의 값에 와일드카드 문자("*")는 보안상 사용할 수 없다.
만일 이를 어길경우 아래와 같은 또다른 종류의 CORS 에러 메세지를 접하게 될 것이다.
참고로 예비 요청(Preflight)가 필요 없는 단순 요청의 경우(GET 요청), Access-Control-Allow-Methods와 Access-Control-Allow-Headers 헤더는 없어도 된다.
Node 서버 설정
서버에 response 헤더(Header) 값으로 Access-Control 설정을 해준다.
var http = require('http');
const PORT = process.env.PORT || 3000;
var httpServer = http.createServer(function (request, response) {
// Setting up Headers
response.setHeader('Access-Control-Allow-origin', '*'); // 모든 출처(orogin)을 허용
response.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS, PUT, PATCH, DELETE'); // 모든 HTTP 메서드 허용
response.setHeader('Access-Control-Allow-Credentials', 'true'); // 클라이언트와 서버 간에 쿠키 주고받기 허용
// ...
response.writeHead(200, { 'Content-Type': 'text/plain' });
response.end('ok');
});
httpServer.listen(PORT, () => {
console.log('Server is running at port 3000...');
});
Express 서버 설정
> npm install cors
const express = require('express')
const cors = require('cors');
const app = express();
app.use(cors({
origin: '*', // 출처 허용 옵션
credential: 'true' // 사용자 인증이 필요한 리소스(쿠키 ..등) 접근
}));
...라우터
Spring 서버 설정
// 스프링 서버 전역적으로 CORS 설정
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOrigins("http://localhost:8080", "http://localhost:8081") // 허용할 출처
.allowedMethods("GET", "POST") // 허용할 HTTP method
.allowCredentials(true) // 쿠키 인증 요청 허용
.maxAge(3000) // 원하는 시간만큼 pre-flight 리퀘스트를 캐싱
}
}
# 참고 자료
https://portswigger.net/web-security/cors
https://livebook.manning.com/book/cors-in-action/chapter-5/81
이 글이 좋으셨다면 구독 & 좋아요
여러분의 구독과 좋아요는
저자에게 큰 힘이 됩니다.