프론트는 react, 백엔드는 spring으로 쿠키를 저장하는 기능을 구현하고 있던 중(백엔드와 프론트엔드 사이의 통신은 axios 사용),
분명히 백엔드로부터 Set-Cookie 값을 전달 받았음에도 불구하고, 쿠키값이 Application 탭의 Cookie에 저장이 되지 않는 일이 있었다.
여러 시행착오 끝에 원인을 파악하여 문제를 해결하였고, 그 과정을 기록으로 남기고자 한다.
스프링 코드
Cookie idCookie = new Cookie("memberId", String.valueOf(member.getUserEmail()));
// response에 쿠키 정보를 담는다.
// 쿠키의 이름은 memberId이고, 값은 회원의 id를 담아둔다.
idCookie.setHttpOnly(true); // Http 환경에서 동작
response.addCookie(idCookie); // 응답에 cookie를 넣어서 보낸다.
return ResponseEntity.ok("success");
개발자도구 결과
위와 같이 응답으로 Set-Cookie 값이 제대로 넘어왔지만, Cookie 저장소에는 제대로 저장되지 않은 모습이었다.
정보를 찾아보니 서로 다른 도메인을 가지고 있는 경우(react는 4000번 포트, spring은 8080번 포트), 분명 서버에서 분명 따로 Access-Control-Allow-origin 헤더 값에 4000번 포트를 허용 가능하게 설정했음에도 불구하고 왜 이런 에러가 뜨는 것일까?
이를 해결하기 위해서는 with credential 옵션을 쿠키값에 넣어서 보내야한다.
Credentials 이란 쿠키, Authorization 인증 헤더, TLS client certificates(증명서)를 내포하는 자격 인증 정보를 말한다.
기본적으로 브라우저가 제공하는 요청 API 들은 별도의 옵션 없이 브라우저의 쿠키와 같은 인증과 관련된 데이터를 함부로 요청 데이터에 담지 않도록 되어있다. 이는 응답을 받을때도 마찬가지이다. 따라서 요청과 응답에 쿠키를 허용하고 싶을 경우, 이를 해결하기 위한 옵션이 바로 withCredentials 옵션이다.
withCredentials 옵션은 단어 그대로, 다른 도메인(Cross Origin)에 요청을 보낼 때 요청에 인증(credential) 정보를 담아서 보낼 지를 결정하는 항목이다. 즉, 쿠키나 인증 헤더 정보를 포함시켜 요청하고 싶다면, 클라이언트에서 API 요청 메소드를 보낼때 withCredentials 옵션을 true로 설정해야한다. 또한 인증된 요청을 정상적으로 수행하기 위해선 클라이언트 뿐만 아니라 서버에서도 Access-Control-Allow-Credentials 헤더를 true로 함으로써 인증 옵션을 설정해주어야 한다.
정리하자면 클라이언트나 서버나 둘다 Credentials 부분을 true로 설정해줘야 한다는 말이다.
바로 적용을 해보자.
Spring config 파일
package YouToo.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
@EnableWebMvc
public class WebConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
// 모든 경로에 대해 모든 HTTP 메서드에 대한 CORS를 허용
registry.addMapping("/**")
.allowedOrigins("http://localhost:8080", "http://localhost:4000")
.allowedMethods("GET", "POST", "PUT", "DELETE")
.allowCredentials(true) // 이 부분 추가
.allowedHeaders("*");
}
}
React axios 설정 파일
const axiosBaseURL = axios.create({
baseURL: process.env.NEXT_PUBLIC_API_URL,
withCredentials: true, // 이 부분 추가
});
개발자도구 결과
이번에는 Set-Cookie의 value 옆에 이상한 경고 문구가 생겼으며, 마찬가지로 Cookie 저장소에 값이 저장되지 않았다.
오류 메시지를 살펴보니
"Set-Cookie"의 헤더에는 "SameSite" 속성에 대한 값이 명시되어 있지 않으며, 이 값은 기본적으로 "Lax"이다.
라고 적혀있다. 그렇다면 SameSite 속성은 무엇이고 Lax는 무엇인가?
SameSite
Cookie의 SameSite 속성은 서로 다른 도메인간의 쿠키 전송에 대한 보안을 설정을 의미한다.
그리고 SameSite 속성은 크게 "None", "Lax", "Strict" 세 가지로 나뉜다.
SameSite=None
None은 현재까지 사용되던 Default 정책으로 SameSite를 검증하지 않는다. 그래서 A 사이트에서 B 사이트로 요청을 전송하게 되면 B 사이트의 쿠키가 붙어서 전송된다(보안상 좋지 않음).
SameSite=Strict
Strict는 쿠키의 SameSite 검사를 강하게 제한하는 정책으로 소스가 되는 도메인과 대상 도메인이 일치해야만 쿠키가 포함되어 전송된다. 예를 들면
(O) http://www.google.com=> http://www.google.com
(X) http://www.hahwul.com => http://www.google.com
위와 같이 통신하는 도메인이 서로 같아야만 쿠키를 포함한 정보를 전송할 수 있다.
SameSite=Lax
마지막으로 Lax는 기존 Strict 정책에서 예외 처리가 몇 개 된 정책이다. 일반적으로 GET을 사용하는 요청 중 앵커태그(<a href>) , form의 get 메소드(<form method=get>) 정도만 예외되고 나머지는 Strict와 동일하게 SameSite가 아닌 경우 쿠키 전송이 차단된다.
허나 20년 2월 4일 릴리즈된 구글 크롬(Google Chrome)80버전부터 새로운 쿠키 정책이 적용 되어 Cookie의 SameSite 속성의 기본값이 "None"에서 "Lax"로 변경되었다.
그 말인 즉슨, 크롬 80 버전 이상에서 개발하고 있는 나는 서로 다른 도메인을 사용하고 있는 리액트(4000번 포트)와 스프링(8080번 포트) 사이에서 쿠키값을 주고 받을 수 없다라는 뜻이다.
여러 시행착오 결과, 내가 해결한 방법은 다음과 같다
백엔드에서 쿠키값을 넣어서 보낼 때, SameSite 속성을 "None"으로 바꿔서 보낸다..!!!
허나 아쉽게도 cookie 객체 속성에는 setSecure만 있을 뿐, setSameSite 속성은 없었다..
따라서 차선책으로 cookie의 attribute를 직접 수정하여 보내는 방식을 선택했다.
그러자 이번에는 다른 오류가 떴다.
그래도 거의 다 온 거 같다. 오류 메시지를 살펴보자.
"SameSite=None" 속성을 사용하려면 "Secure" 속성이 필요하다는 의미인 거 같다.
마지막으로 스프링의 cookie를 설정하는 코드에 Secure를 true로 지정하여 수정해보자.
Cookie idCookie = new Cookie("memberId", String.valueOf(member.getUserEmail()));
// response에 쿠키 정보를 담는다.
// 쿠키의 이름은 memberId이고, 값은 회원의 id를 담아둔다.
idCookie.setHttpOnly(true);
idCookie.setSecure(true); // 이 속성과
idCookie.setAttribute("SameSite", "None"); // 이 속성 추가
response.addCookie(idCookie);
return ResponseEntity.ok("success");
최종 결과
api response의 cookie에 아무런 에러 메시지가 없으며, Cookie에 값이 잘 저장된 모습을 확인할 수 있다.
다만 이 방법은 http 개발 환경에서만 유효한 방법인 거 같다. 배포된 https 환경에서도 동일하게 해결할 수 있을 지는 추후에 알아보도록 해야겠다.
[출처]
https://inpa.tistory.com/entry/AXIOS-📚 - CORS-쿠키-전송withCredentials-옵션 [Inpa Dev 👨💻:티스토리]
https://ifuwanna.tistory.com/223 - 쿠키 SameSite 설정하기(Chrome 80 쿠키 이슈)
https://www.hahwul.com/2020/01/18/samesite-lax/ - SameSite=Lax가 Default로? SameSite Cookie에 대해 정확하게 알아보기