Set-Cookie로 브라우저에 쿠키 저장하기

Haiseong Jeong·2023년 2월 13일
11
post-thumbnail

삽질의 과정이 많이 포함됭어있는 글이므로 정리가 잘 안되어 있습니다. 정리된 글을 보고싶으시면 다른 블로그 글을 보시는걸 추천드립니다.

세션 스토리지, 로컬 스토리지, 등등 웹 브라우저에는 정보를 저장할 수 있는 공간이 있다. 최근에 jwt 인증방식을 위해 쿠키를 구워야 했다. 쿠키는 유저들의 효율적이고 안전한 웹 사용을 보장하기 위해 웹사이트 접속시 브라우저에 저장되는 작은 텍스트 파일이다. jwt 인증은 인증토큰을 브라우저에 저장하고 필요할때마다 이 토큰을 이용해 인가를 받는다.

아무튼 쿠키에 담을 토큰 정보는 잘 만들었고 이제 브라우저에 저장만 하면 됬는데 저장이 안됬다. 삽질의 시작 3일동안 내가 한 삽질을 누군가는 하지 않길 바라면서 정리할 겸 오랜만에 블로그를 작성한다.

쿠키의 속성들

path

url값으로 이 경로 혹은 하위의 경로에서만 쿠키에 접근할 수 있다. 예를 들어, path=/auth으로 설정하면 /auth, /auth/login, /auth/refresh등 도메인으로 설정한 주소의 하위 주소에서만 쿠키에 접근할 수 있다.
나의 경우에는 /auth/login을 통해서 토큰을 받은 후에 다른 api를 통해서도 쿠키를 사용하는 경우가 있어서 / 으로 설정했다.

domain

쿠키에 접근 가능한 도메인을 설정한다. test.com을 도메인으로 입력한다면 test.com은 물론 blog.test.com, news.test.com 같은 서브도메인에서도 쿠키를 이용할 수 있다. 그러나 만약 이 필드를 입력하지 않는다면 test.com 에서 호출되어 생성된 쿠키는 서브도메인에서 이용할 수 없고 오직 test.com에서만 이용이 가능하다.
나는 우선 로컬환경에서 테스트할 목적으로 localhost로 설정해주었다.

expires와 max-age

expires와 속성을 지정하지 않으면 브라우저를 닫을때 쿠키가 삭제되는 세션 쿠키가 된다. 설정한 기한이 되면 쿠키는 삭제된다.
max-age는 expires의 대안이고 쿠키를 얼마나 유지할 것인지를 설정합니다. 0으로 지정하면 바로 삭제된다.

secure ⛏

https통신에서만 쿠키에 접근할 수 있게 한다.

samsite ⛏

XSRF(크로스 사이트 요청 위조)라 불리는 공격을 막기위한 속성이다. 사이트 외부에서의 쿠키에 대한 접근을 막는다.

httpOnly ⛏

클라이언트에서 document.cookie를 활용해 쿠키를 조작할 수 없도록 한다. 클라이언트에서 쿠키를 조작하면 쿠키를 위조해 서버에 피해를 끼칠 수 있기 때문이다.

⛏ ??

⛏ 이모지를 붙인 속성은 내가 삽질 한 속성을 뜻한다. 쿠키라는 것을 개념적으로만 알고 직접 사용해본적이 거의 없었기때문에 속성의 의미도 잘 모르고 대충 사용했었다. 우선 다시 위에 내가 마주한 상황으로 돌아가 보자.

response에 Set-Cookie 헤더를 추가해서 보내면 브라우저에 쿠키가 저장된다. 그래서 아래처럼 코드를 작성해 보았다. (Cookie 객체를 통해 정보를 넣은 후 addCookie 메서드를 실행해 넣는 방법도 있다. 하지만 SameSite 옵션을 지정하지 못하는 문제가 있어 헤더에 String으로 때려박는 방법을 선택했다.)

@PostMapping("/login")
    public ResponseEntity<?> login (@RequestBody LoginDTO loginDTO, HttpServletResponse response, HttpServletRequest request){
    
    . . .
    
        try {
            String jwt = authService.login(loginDTO, response);
            response.setHeader("Set-Cookie",
                    "token=" + jwt + "; " +
                            "Path=/;" +
                            "Domain=localhost; " +
                            "HttpOnly; " +
                            "Max-Age=604800; "
                            );
            return new ResponseEntity<>(jwt, HttpStatus.OK);
           	}
            
    . . .
    

응답에는 쿠키가 잘뜨는데 로컬에는 안뜨네



몇번을 시도해도 뜨지 않았다. 이때부터 맨탈붕괴였다. 모든 옵션들 조금씩 바꿔가면서 서버를 몇번이고 재부팅 해보고 쿠키 지정방법도 바꿔보고 많은 방법을 시도해 봤지만 안됐다. 그렇게 하루를 종일 날려버리고 같이 프로젝트하는 프론트 형에게 부탁해봤다. "그냥 response 바디에 토큰 넘겨주면 프론트에서 저장하면 안돼?" 바로 퇴짜 맞았다.

httpOnly

왜냐하면 프론트에서 저장하는것은 위험하기 때문이다. 악의적인 사용자가 쿠키를 조작하는것이 가능해지기 때문이다. 이때문에 httpOnly는 반드시 켜야했다. 다시 정신 차리고 구글링 시작했다.

SameSite

sameSite 속성에는 3가지 속성값이 있다.

None : 동일 사이트와 크로스 사이트 모두에 쿠키 전송이 가능합니다. 이로인해 CSRF의 공격에 취약한 등 보안에 취약점을 가지고 있습니다. 크롬 80 버전부터는 SameSite를 None으로 설정할 경우 쿠키에 암호화된 HTTPS 연결이 필요함을 나타내는 Secure 속성을 함께 넣어주어야 합니다.
Strict : Strict로 설정하면 이 값으로 설정된 쿠키는 도메인 자체에서 시작된 요청에서만 전송됩니다. 즉 SameSite에서만 쿠키의 전송을 허용합니다.
Lax : Lax 모드는 기본적으로는 CrossSite 쿠키값 전송을 차단하는 Strict 모드와 동일하지만 몇가지 예외사항 둬 CrossSite임에도 일부 요청 방식으로는 쿠키를 보낼 수도 있습니다.

문제점 하나를 찾았다. 크롬 정책으로 기존 Default 였던 None 설정이 Lax가 되었다. 이제 이거만 바꿔주면 되는건가 싶었다. Secure 속성을 함께 넣어주어야 한다니 같이 넣어 주었다.

response.setHeader("Set-Cookie","token=" + jwt +. ;Path=/; Domain=localhost; HttpOnly; Max-Age=604800; SameSite=None; Secure;");

secure

secure 옵션은 위에서 말했듯이 https통신에서만 쿠키에 접근할 수 있게 하는 옵션이다. http는 안된다. https를 한번도 써본적이 없어서 걱정했지만 생각보다 쉽게 설명해주는 자료가 많았다. 백기선 유튜브 - 웹서버에 HTTPS를 적용해보자.


스프링 서버에 https를 적용했다. 이제 되나 싶었지만 미동도 없었다. 그러던중 ..
CORS를 허용했는데도 쿠키가 넘어가지 않는 현상
이 글을 보고 문제를 최종적으로 해결했다.

CORS 설정과 withCredentials

우리는 프론트로 리엑트 기반 프레임워크를 사용하기때문에 클라이언트와 백앤드 서버가 따로 분리되어있다. 클라이 언트를 http://localhost:5173로, 백앤드 API 서버를 https://localhost:8080 로 설정했다. 서로 같은 Host이고 Port만 다르다. 하지만 컴퓨터는 이 두 서버를 같은 Origin으로 보지 않는다. 따라서 CORS 설정이 필요했다.

CORS를 이해하기 전에 SOP를 이해해야 한다. SOP는 Same-Origin Policy 의 줄임말이다. 직역하면, 같은 출발지(?) 정책이다. SOP가 없다면 이런 다른 사이트간 요청이 악의적으로 실행될 수 있다. 기본적으로 다른 서버의 요청을 막아놓지 않으면 요청을 조작해 누군가에게 계좌의 돈이나 개인정보를 가져가는 일이 생길수도 있다.

CORS는 Cross-Origin Resource Sharing이다. 다른 출처의 자원을 공유하는것, 다른 서버의 요청을 풀어주는 것이다. 우리 프로젝트의 경우에는 Set-Cookie 헤더를 통해 쿠키를 설정하게 하는 요청이다. 하지만 기본적으로 SOP가 적용되기 때문에 우리는 CORS 설정을 해줘야 한다

또한 기본적으로 브라우저가 제공하는 요청 API 들은 별도의 옵션 없이 브라우저의 쿠키와 같은 인증과 관련된 데이터를 함부로 요청 데이터에 담지 않도록 되어있다. 프론트와 백엔드 서버의 Origin이 다를 경우, 백엔드에서 프론트엔드로 쿠키를 생성해줄 수도, 프론트엔드에서 백엔드로 쿠키를 보내줄 수도 없는데 이를 해결하기 위한 옵션이 바로 withCredentials 옵션이다.

설명이 길어지는데 결론만 말하면 서버의 response에 Set-Cookie 헤더를 담아 쿠키를 담고 싶으면 1. 클라이언트에서 withCredentials 옵션을 활성화 2. 서버에서 Access-Control-Allow-Credentials 항목을 true로 설정 3. 서버에서 CORS 설정 해주기 (와일드 카드 불가능) 을 해야한다. 언어별, 프레임워크별 자세한 방법은 링크에 잘 나와있다.
CORS를 허용했는데도 쿠키가 넘어가지 않는 현상

클라이언트도 https로

마지막으로 삽질한 부분이다. 지금 클라이 언트는 http://localhost:5173로, 백앤드 API 서버를 https://localhost:8080 로 설정되어 있다. 클라이언트도 ssl 인증을 받지 않으면 쿠키가 전송되지 않았다. 인증을 잘 받아주고 쿠키 굽기는 성공했다. (👏👏👏)

정리

어수선하게 작성한것 같아 보는사람에게는 미안하게 되었다. 마지막으로 한번 정리하면 다음과 같다.

1. 서버, 클라 둘다 ssl인증서를 받아 https로 접근하게 한다.

2. httpOnly, Secure, SameSite=None 옵션을 쿠키에 담는다.

3. Cors 설정과 withCredentials을 설정한다.

여러분도 꼭 해결하시길..!

profile
나는 개발자다. 5000만큼 코딩한다.

3개의 댓글

comment-user-thumbnail
2023년 9월 20일

잘보고갑니당

답글 달기
comment-user-thumbnail
2023년 10월 23일

혹시 최종적으로 저장한 쿠키를 React쪽에서 사용할 수 있었나요?
저는 쿠키 저장은 됐는데 아무리 해도 React 쪽에서 사용 할 수가 없네요... 값이 절대 안읽힙니다.
재발급 요청을 위해 Spring 서버로 토큰 값을 전달 해야 하는데.....

1개의 답글