• 카테고리

    질문 & 답변
  • 세부 분야

    풀스택

  • 해결 여부

    해결됨

token을 Cookie에 저장하는 것에 관한 에러 질문

23.07.06 16:11 작성 조회수 1.24k

1

안녕하세요! 강사님의 강의를 듣고 질문 타이틀에 적힌 대로 시도하는 과정에 어려움이 생겨 질문 드립니다.

제가 시도한 인증/인가 흐름은 다음과 같이 진행됩니다.

  1. 클라이언트가 로그인 요청 (성공)

  2. 서버에서는 이에 대해 access token과 refresh token을 생성하고, access token은 response body에, refresh token은 cookie에 담아 응답을 보냅니다. (성공)

  3. 클라이언트의 요청에 대해 응답이 성공적으로 들어오면 / 화면으로 redirect되고, response body에 담긴 access token은 변수에 저장되고 (성공), refresh token은 brower cookie에 저장됩니다. (실패)

2번까지는 성공했지만 3번에서 cookie에 저장하는 것에 실패했습니다.

코드는 다음과 같습니다.

클라이언트단 코드

    const onFinish = (values) => {

        setFieldErrors({});

        async function fn() {
            try {
                const { data: token } = await Axios.post("http://localhost:8000/accounts/auth",
                    values,
                    { credentials: "include" }
                );
                axios.defaults.headers.common["Authorization"] = `Bearer ${token.access}`
                navigation("/");

                ...

서버단 코드 (accounts/auth)

아래 코드를 작성하는데 참고한 django 공식문서는 [여기](https://docs.djangoproject.com/en/3.0/ref/request-response/#django.http.HttpResponse.set_cookie) 입니다.

Access-Control-Allow-Credentials의 경우, django-cors-header가 있어도 발생하여 해당 헤더를 추가하니 해결되었습니다. 또한 클라이언트단에도 credentials를 추가했습니다.

from rest_framework_simplejwt.views import TokenObtainPairView

class AuthView(TokenObtainPairView):
    def post(self, request, *args, **kwargs)):
        ...

        response = Response(
            {"access": serializer.validated_data.get("access", None)},
            status=status.HTTP_200_OK,
        )
        refresh_token = serializer.validated_data.get("refresh", None)
        response.set_cookie(
            "refresh",
            refresh_token,
            httponly=True,
            samesite=None,
            max_age=24 * 60 * 60,
            secure=False,
            domain="localhost",
        )
        response.headers["Access-Control-Allow-Credentials"] = True
        return response

 

위 코드의 응답 결과는 다음 이미지와 같습니다.

Screen Shot 2023-07-06 at 4.02.30 PM.pngresponse headers에 담긴 전체 정보이고, Set-Cookie에 관한 내용은 다음과 같이 나왔습니다.

Set-Cookie:

refresh=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0b2tlbl90eXBlIjoicmVmcmVzaCIsImV4cCI6MTY4ODcxMzE5OSwiaWF0IjoxNjg4NjI2Nzk5LCJqdGkiOiIwZTNkNGYwN2RiZTI0NGUyYWMyMmVlODIzYjAzMzZkMCIsInVzZXJfaWQiOjd9.xAsVsvuoUcukBZ0t2iVo-yIlplDy2JlwE_R4cX1YwmU; Domain=localhost; expires=Fri, 07 Jul 2023 06:59:59 GMT; HttpOnly; Max-Age=86400; Path=/

 

그래서 시도한 것들은 다음과 같습니다.

  1. httponly, samesite, secure 각 값들에 대해서 하나씩 값을 바꿔가면서 시도했습니다.

  2. domain keyword 인자를 없애기도 했습니다.

  3. chrome의 캐쉬 설정이 문제인가 하여 third-party cache 허용으로도 바꿨습니다.

  4. middleware가 문제인가 하여 django-cors-header와 debug toolbar를 꺼서 시도해봤습니다.

  5. application/Cookies 뭔가 남아있으면 삭제하고 다시 로그인으르 시도했습니다.

하지만 위 시도들을 했음에도 불구하고도 해결이 되지 않아 강사님에게 여쭤봅니다 ㅠㅠ

답변 1

답변을 작성해보세요.

0

안녕하세요.

저는 Axios 요청마다 withCredentials = true 설정(config)을 넣으니까 안 되고, 디폴트 설정으로 넣으니까 되더라구요.

Axios 디폴트 설정으로 withCredentials 설정을 넣어서 테스트해보시겠어요?
Axios.defaults.withCredentials = true;

그리고 response.headers["Access-Control-Allow-Credentials"] = True 헤더를 직접 세팅하지 마시고,

django-cors-headers의 CORS_ALLOW_CREDENTIALS 설정을 활용해보세요.

# https://github.com/adamchainz/django-cors-headers#cors_allow_credentials-bool
CORS_ALLOW_CREDENTIALS = True

--

Axios.defaults.withCredentials = true; 설정을 하니, django단에서 아래와 같이 쿠키를 설정해도 동작하더라구요.
response.set_cookie(key="refresh_token", value=refresh_token, httponly=True)

확인해보시고 댓글 부탁드립니다.

화이팅입니다. :-)

참고로 헤더 설정 시에 True로 값을 설정하시면, 응답 헤더에는 문자열 "True"로 설정되어 전송됩니다. // "true" 로 문자열로 지정하시는 것이 맞습니다.

그리고 post 메서드를 오버라이딩하는 것보다, DRF에서는 finalize_response 메서드가 제공되니, 요청 처리 후에 후처리할 작업이 있다면 활용하시면 좋습니다.

image

김제하님의 프로필

김제하

질문자

2023.07.09

잘 작동됩니다! 감사합니다.

하나 추가로 여쭤볼게 있습니다. 현재 simplejwt의 경우에는 access_token을 갱신 하기 위해서는 /refresh api에 post로 현재 가지고 있는 refresh_token을 보내야하는 걸로 알고 있습니다.

그렇다면 리액트 단에서 쿠키에 저장된 refresh token을 가져와서 post 정보로 보내야하는데, httponly=true인 상황에서는 js code로 가져오지 못해서 보낼 수 없는 상황에 다음 아이디어를 고려해봤는데요. (만약 가져올 수 있는 방법을 아신다면 그걸로 손쉽게 해결될 것 같습니다!)

cookie에 저장되면 clinet에서 server로 보낼 때 자동적으로 보내지는 걸로 알고 있습니다.

그래서 get method로 보내서 쿠키에 담겨진 refresh를 사용해서 access token을 갱신하는 방법을 생각했고, 다음과 같이 코드를 짰는데요.

from rest_framework_simplejwt.views import TokenRefreshView
from rest_framework_simplejwt.serializers import TokenRefreshSerializer
from rest_framework.response import Response

class RefreshTokenView(TokenRefreshView):
    def get(self, request):
        refresh_in_cookie = request.COOKIES.get("refresh")
        refresh = TokenRefreshSerializer.token_class(refresh_in_cookie)
        new_access_token = {"access": str(refresh.access_token)}

        return Response(new_access_token, status=status.HTTP_200_OK)

위 view에 대한 url 은 다음과 같습니다.

    path("token/refresh2", views.RefreshTokenView.as_view(), name="refresh"),

pytest 로 한 결과로는 access_token 갱신이 가능했습니다.

   
   refresh_url = urls.reverse("refresh")
   response = client.get(refresh_url, {"refresh": refresh01})

그러면 response.data에 {"access": 토큰 내용} 으로 발급됩니다. 그리고, drf api 문서에서 해당 url을 입력하면 갱신된 access_token이 발급됩니다.

하지만 정작 react에서 response.data 를 출력하면 undefined 만 나오는 상황입니다. react로 할 때 print문으로 해당 view의 request.COOKIES를 출력하면 {} 가 나옵니다.

react code는 다음과 같습니다.

const response = useAxios("http://localhost:8000/accounts/token/refresh2")
console.log("response.data :", response.data) // undefined 출력  

서버 로그 상에 뜬 것은 다음과 같습니다.

[08/Jul/2023 15:40:27] "GET /accounts/token/refresh2 HTTP/1.1" 200 225

제가 뭘 놓치고 있는 것일까요?

httponly 쿠키는 JS 단에서 접근할 수 없습니다. 그러니 POST 요청 data로 httponly 쿠키에 있는 값을 담을 수는 없습니다.

TokenRefreshView의 Serializer 생성자로 refresh 값을 넘겨줘야 유효성 검사를 수행할 수 있는 데요. 그럼 Serializer 생성자 호출 전에 쿠키에 있는 값을 request.data 로 먼저 저장하는 방식은 어떨까요? 그럼 TokenRefreshView의 로직을 온전히 활용할 수 있게 됩니다.

image그리고, useAxios 훅은 아래와 같은 반환값을 가집니다. response로 바로 받으셨기에, response는 array가 될 것이고 array에는 .data 속성이 없으니 undefined가 반환된 것은 아닐까요?

image관련 공식문서 : https://github.com/simoneb/axios-hooks#quick-start

그리고, 서버로부터 응답을 받기까지에는 시간이 다소 걸립니다. 그러니 응답객체를 담는 상탯값인 data는 처음에는 undefined 것이며, 응답을 받았을 때 비로서 그 응답내역을 가질 것입니다.


살펴보시고, 댓글 남겨주세요. ;-)