쿠키를 사용한 API 통신을 할 때 주의할 점

Jihoon Oh·2022년 10월 8일
3

팀 프로젝트를 진행하면서 Refresh token을 HTTP only 쿠키에 담기로 결정하여, 인증 인가 서비스에 쿠키를 사용하는 로직이 추가로 들어가게 되었습니다. 저희 팀은 단순히 서버에서 토큰을 응답으로 내려줄 때 Set-Cookie 헤더로 쿠키 값을 설정해주도록 내려주기만 하면 이후의 API 통신은 자동으로 쿠키를 보내게 될거라고 생각을 했는데요, 브라우저에 쿠키가 잘 세팅되기는 했지만 저희가 생각한대로 작동하지 않았습니다. 이번 포스팅에서는 어째서 쿠키가 보내지지 않게 되었는지, 어떻게 하면 쿠키를 서버로 잘 보낼 수 있는지 해결 과정을 공유하는 시간을 가져보도록 하겠습니다.

Cross-Site 요청 시 Axios에 쿠키를 담는 법

우선은 당연하게도 클라이언트가 요청을 보낼 때 쿠키를 요청에 담아서 보내줘야 합니다. 이 부분은 백엔드의 영역은 아니지만, 백엔드 개발자도 알면 좋을 것 같습니다. 일반적으로 백엔드 단에서 간단하게 클라이언트 페이지와 서버 설정을 하고 테스트해보게 되면, 서버에서 응답에 Set-Cookie 헤더를 내려주는 것 만으로도 이후 요청에 쿠키값이 담아지는 것을 확인할 수 있었습니다. 하지만 실제 운영 환경으로 가서 테스트해보면 서버로 쿠키 값을 보내지 않았습니다.

이는 CORS로 인한 문제였습니다. CORS를 허용하는 옵션 중에 기존에 사용하지 않아서 대수롭지 않게 넘어갔던 옵션이 있었는데요, 바로 withCredentials 옵션입니다. 기본적으로 쿠키는 Same-Origin에만 담도록 설정이 되어 있고, Cross-Origin에 대해서는 CORS 허용을 해주는 추가적인 옵션이 필요합니다.

스프링에서는 WebMvcConfigureraddCorsMappings 메서드를 오버라이딩 할 때 CorsRegistry의 allowCredentials 메서드에 true 값을 넣어주는 것으로 허용 처리를 해줄 수 있습니다. 그리고 Set-Cookie 헤더가 Cross-Site 요청에서 기본적으로 사용할 수 있는 헤더가 아니므로, exposedHeaders 메서드 안에 Set-Cookie도 넣어주어야 합니다.

또한 주의할 점으로, allowCredentials=true 옵션을 주게 되면 요청을 허용하는 Origin 값에 와일드카드(*)를 사용할 수 없습니다. 보안에 대해 생각해 봤을 때, 쿠키를 사용하여 인증 옵션을 켰는데 아무 origin에나 허용해주면 안되겠죠?

@Override
public void addCorsMappings(final CorsRegistry registry) {
    registry.addMapping("/api/**")
            .allowedMethods(CORS_ALLOWED_METHODS.split(","))
            .allowedOrigins(MAIN_SERVER_DOMAIN, MAIN_SERVER_WWW_DOMAIN, TEST_SERVER_DOMAIN, FRONTEND_LOCALHOST)
            .allowCredentials(true)
            .exposedHeaders(HttpHeaders.LOCATION, HttpHeaders.SET_COOKIE);
}

하지만 이것만으로는 withCredentials 옵션이 켜지지 않고, 클라이언트에서도 처리를 해주어야 합니다. 저희 F12팀은 클라이언트 단에서 API 통신에 Axios라는 라이브러리를 사용하는데요, Axios 에서도 옵션을 추가해야 합니다.

const data = await axios.get(url, {
  withCredentials: true
});

이렇게 서버와 클라이언트 양쪽 설정을 해주면 withCredentials 옵션을 활성화하고 쿠키를 담아 보낼 수 있습니다.

SameSite 주의

하지만 여기서 끝나지 않습니다. 위 설정을 하더라도 어떤 요청에서는 쿠키가 정상적으로 담기지 않는 상황이 발생했습니다. 이는 SameSite 옵션 때문인데요, 쿠키는 도메인을 기준으로 퍼스트 파티 쿠키와 서드 파티 쿠키로 나뉩니다. 기본적으로 쿠키는 도메인 별로 관리되고 보내지게 됩니다. 이 때 요청을 보내는 곳, 즉 Referrer와 같은 도메인의 쿠키는 퍼스트 파티, 다른 도메인의 쿠키는 서드 파티라고 하는데요, 서드 파티 쿠키의 전송을 막는 SameSite 정책 때문에 쿠키가 제대로 날아가지 않는 것이었습니다.

Cross-Site 간 쿠키 전송은 CSRF 공격에 취약하다는 문제점이 있는데요, 이 문제를 해결하기 위해 나온 정책이 SameSite입니다. 과거에는 기본적으로 SameSite 정책이 none이었습니다. none으로 설정된 경우 Cross-Site로 쿠키를 보내더라도, 즉 서드 파티 쿠키를 보내더라도 전혀 제한이 없었습니다. 그러나 이제 정책이 바뀌고 있는데요, 크롬이 2020년 2월 4일 80버전부터 포문을 열었습니다. 브라우저의 SameSite 정책을 none에서 Lax로 변경한 것이죠. SameSite 정책이 Lax일 경우, <a href=...>과 같은 페이지 이동 링크나 GET, HEAD 요청을 제외하고는 서드 파티 쿠키 사용이 불가능합니다. 그리고 크롬을 시작으로 파이어폭스 등 다른 브라우저들도 SameSite 정책을 변경하고 있습니다.

결국 브라우저가 기본적으로 서드 파티 쿠키를 전송하지 않도록 설정하고 있기 때문에 쿠키가 정상적으로 요청에 담기지 않는 문제가 발생하는 것이었습니다. 보통 클라이언트가 접속하는 사이트와 API 통신을 받는 백엔드 서버의 도메인은 다르기 때문이죠.

때문에 쿠키를 만들어줄 때 SameSite=Lax 정책이 적용되지 않도록 다음과 같은 설정을 해줄 필요가 있습니다.

private ResponseCookieBuilder createTokenCookieBuilder(final String value) {
    return ResponseCookie.from(REFRESH_TOKEN, value)
            .httpOnly(true)
            .secure(true)
            .path("/")
            .sameSite(SameSite.NONE.attributeValue());
}

ResponseCookieBuilder를 만들 때 sameSite(SameSite.NONE.attributeValue())를 호출해 주면 됩니다.

이렇게 withCredentails 옵션을 켜주고, SameSite 설정까지 끝나면 API 통신에서 쿠키를 통해 값을 주고받을 수 있게 됩니다.

profile
Backend Developeer

1개의 댓글

comment-user-thumbnail
2022년 10월 11일

좋은글 감사합니다!!

답글 달기