Skip to main content

Command Palette

Search for a command to run...

[React/Node.js] 쿠키를 이용해 refresh-token 구현

Updated
5 min read
[React/Node.js] 쿠키를 이용해 refresh-token 구현

JSON 로그인 프론트엔드 + 백엔드 구현

프로젝트를 진행하면서 JWT (Json Web Token) 방식으로 로그인을 구현하기로 하였다. (JWT에 대한 설명은 생략, 이 글을 읽는 사람들은 에러를 해결하기 위해 온거겟지?)

백엔드에서 access-tokenheaderAuthorization에 담아서 보내주고, refresh-tokencookie에 담아서 보내기로 하였다. (보안 이슈 고려 body에 넣지 않음)

프론트에서는 access-tokenlocalStorage에 저장하고, refresh-tokencookie에 저장하기로 하였다. (아래 표와 같음)

프론트엔드(저장 위치)백엔드(response 방식)
access-tokenlocalStorageheader.authorization
refresh-tokencookiecookie

프로젝트 과정에서 해당 방식으로 JWT를 구현하기 위해 삽질했던 기록을 작성한다.

똑똑한 사람들을 위해 최종 코드 먼저 제공

백엔드

return res
    .status(403)
    .cookie('refreshToken', refreshToken, {
        expires: new Date(Date.now() + 259200),
        httpOnly: true,
    })
    .header('Authorization', newAccessToken)
    .json({ message: 'Renewed expired access token' });

프론트엔드

import axios from "axios";
export const API = axios.create({
  baseURL: import.meta.env.VITE_BASE_URL,
  headers: {
    Authorization: `Bearer ${localStorage.getItem("accessToken")}`,
  },
  withCredentials: true,
});


처음에 설계했던 방법은 다음과 같다.

프론트엔드

import axios from "axios";
export const API = axios.create({
  baseURL: import.meta.env.VITE_BASE_URL,
  headers: {
    Authorization: `Bearer ${localStorage.getItem("accessToken")}`,
  },
  withCredentials: true,
});

백엔드

return res
  .status(403)
  .cookie("refreshToken", refreshToken, {
    httpOnly: true,
  })
  .header("Authorization", newAccessToken)
  .json({ message: "Renewed expired access token" });

request는 간단했다. 우리는 login post를 요청하였고 headers에서 Authorization 항목을 받아오고 싶었다.

하지만, CORS ERROR와 마주했다.

[삽질 No.1] CORS 설정

// login request code
try {
      const res = await API.post("/auth/login", userInput);
      // access token 저장
      localStorage.setItem("accessToken", res.headers.get("Authorization"));      
    } 
catch (error) {
  // status에 따른 Error Handling
  const errorResponse = error.response;
  const statusCode = errorResponse.status;
  console.log(statusCode);
  switch (statusCode) {
    case 40X:
      ...
      break;
  }
}

❓CORS란?

CORS를 이해하기 위해서는 SOP를 이해해야 합니다.

브라우저에서는 보안적인 이유로 cross-origin HTTP Request들을 제한합니다. 말 그대로, 서로 다른 origin (간단하게 url) 끼리의 HTTP Request를 제한하는 것을 의미한다.

이를 SOP (Same - Origin Policy)라고 합니다.

즉, SOP는 동일한 출처 사이에서만 리소스를 공유할 수 있다는 규칙을 지니고 있습니다.

→ CSRF(Cross Site Request Frogery, 크로스 사이트 요청 위조 공격)을 대비해 만들어짐 (공격자의 요청이 사용자의 요청인 것처럼 속이는 공격 방식)

⭐예전에는 프론트엔드 / 백엔드가 분리되지 않아서, 처리를 같은 도메인 내에서 했기 때문에 해당 정책이 효과적으로 공격들을 막아낼 수 있었다(CSRF, XSS). 하지만, 이제는 프론트엔드 / 백엔드가 분리되고, 다양한 API들을 사용하기 위해서 동일하지 않은 출처 사이에서도 리소스를 공유해야 했다.

이때, SOP를 지키면서 몇가지 예외사항을 적용하는 것이 CORS이다.

✏️출처는 Protocol + Host + Port (HTTPS://www.domain.com:3000)이다.

즉, 동일하지 않은 출처 사이에서도 HTTP Request를 가능하게끔 하는 것이 CORS이다. (매번 에러가 발생해서 나쁜 놈인줄 알았지만 사실은 좋은 놈이였다)

단, 모든 출처에서 접근할 수 있게 만들면 SOP가 없는것과도 같으니, ⭐가능한 URL들을 지정해주는 방법으로 최소한의 접근만 허용하는 것이다.

해당 프로젝트에서 어떻게 적용해야 할까?

1.CORS설정으로 프론트에서는 백엔드를, 백엔드에서는 프론트엔드를 등록한다.

  1. → 이때, 양쪽에서 등록해야 함을 주의하자.
// 프론트엔드 (package.json)
{
  ...,
  "proxy": "http://localhost:3000" //백엔드 url
}
// 백엔드 (cors_config.js)
const cors = require("cors");
exports.options = cors({
  origin: [
    "http://localhost:5173", // 프론트 url
  ],
});

이렇게 했는데도, CORS Error가 발생했다.

The value of the 'Access-Control-Allow-Credentials' header in the response is '' which must be 'true' when the request's credentials mode is 'include’

2.credentials: true 설정하기

⭐CORS는 기본적으로 보안상의 이유로 쿠키를 request로 보낼 수 없도록 설계되었다.

하지만, 다른 도메인을 가진 API 서버에 자신을 인증해야 하는 정상적인 응답을 받을 수 있는 상황에서는 쿠키를 통한 인증이 필요하므로, 해당 설정을 수정해야 한다.

// 프론트엔드
import axios from "axios";
export const API = axios.create({
  baseURL: import.meta.env.VITE_BASE_URL,
  headers: {
    Authorization: `Bearer ${localStorage.getItem("accessToken")}`,
  },
  withCredentials: true, // 추가하기
});
// 백엔드
const cors = require("cors");
exports.options = cors({
  origin: [
    "http://localhost:5173",
  ],
  credentials: true, // 추가하기
});

1.header의 Authorization에 접근 권한 설정하기

⭐ header는 기본적으로 값들이 접근가능하지 않도록 설정되어 있는 경우가 많다.

백엔드의 CORS 설정에서 exposedHeaders 값으로 노출하는 값을 변경할 수 있다.

// 백엔드
const cors = require("cors");
exports.options = cors({
  origin: [
    "http://localhost:5173",
  ],
  exposedHeaders: ["Authorization"],
  credentials: true,
});

최종 코드

참고로, set-Cookie에 보낸 refreshToken은 브라우저에서 자동으로 쿠키에 저장해준다.

try {
    const res = await API.post("/auth/login", userInput);      
    // access token 저장
    localStorage.setItem("accessToken", res.headers.get("Authorization"));
    // refresh token 저장 : 브라우저 자동
    // ... 유저 정보 저장 / redirect 등
  } 
catch (error) {
    // status에 따른 Error Handling
    const errorResponse = error.response;
    const statusCode = errorResponse.status;
    console.log(statusCode);
    switch (statusCode) {
      case 404:
        alert("해당 이메일로된 아이가 없습니다");
        break;
      case 401:
        alert("비밀번호가 틀렸습니다.");
        break;
      case 400:
        alert("Bad request (요청 형식이 잘못됨)");
        break;
    }
  }

[삽질 No.2] 로그아웃 기능

로그아웃 기능을 구현할때, ✏️access Token과 refresh Token을 삭제하고 싶다.

access Token은 localStorage에 있으므로, 쉽게 삭제 가능하다.

😡문제는 refresh Token이다.

쿠키에 있는 refresh token에 접근해서 삭제하고 싶었다.

암만 찾아봐도 방법이 없었다. 실제로 불가능했기 때문이다.

⭐백엔드에서 refresh Token을 쿠키에 담아서 보낼때 httpOnly:true 를 달아서 보냈는데, 해당 속성을 달아주면 프론트에서 암만 노력해도 접근할 수 없다. 일부러 막아둔 것이다.

잘 생각해보면 저장할 때도 자동으로 저장하는 이유가, 프론트에서 접근하지 못하니까 친절하게 알아서 저장해주는 것이였다.

❓그러면.. 어떻게 삭제해???

쿠키에는 2가지 종류가 있었다.

세션 쿠키사용자가 사이트 탐색 시에 관련한 설정들과 선호사항을 저장하는 임시 쿠키정도라고 생각하면 된다. 브라우저를 닫는 순간 삭제된다.
지속 쿠키지속쿠키는 세션쿠키와 다르게 삭제되지 않고 더 길게 유지가 가능하다. 지속쿠키는 디스크에 저장되며, 브라우저를 닫거나 컴퓨터를 재시작해도 남아닜다. 사용자 로그인 항상 유지와 같은 곳에 사용한다
차이점세션쿠키와 지속쿠키는 파기시점 외에 차이점이 없다. Discard라는 파라미터가 설정되어 있거나, 파기까지 남은시간인 Expires또는 Max-Age라는 파라미터가 없으면 세션쿠키이다.

즉, 우리는 지금까지 세션 쿠키를 사용하였지만, httpOnly 속성으로 인해 직접 쿠키를 제어할 수 없으므로, 지속 쿠키를 생성하기로 하였다.

즉, refresh-token을 삭제하는 대신 ⭐만료기간을 설정하는 방식으로 진행하였다.

백엔드분들한테 refreshToken의 기간을 설정해달라고 요청하였다.

return res
    .status(403)
    .cookie('refreshToken', refreshToken, {
        expires: new Date(Date.now() + 259200), // 기간 설정
        httpOnly: true,
    })
    .header('Authorization', newAccessToken)
    .json({ message: 'Renewed expired access token' });

지금까지, JWT를 구현할 때 삽질했던 기록들을 정리했습니다.

나중에 또 삽질할때 봐야겠다 :)


개인 블로그: https://velog.io/@jihostudy/posts

edited by 김지호

More from this blog

[ZSH] tree 사용하기

들어가며 큰 규모의 프로젝트를 출시한 뒤, 후일을 위해서 더 늦기 전에 파일 정리 및 문서화를 진행해야했다. 문서화 작업을 하는 중에 기왕 정리하는 거 파일 구조를 이쁘게 트리 구조로 나열하여 코멘트를 달면 나중에 보더라도 이해하기 더 쉬울 것 같았다. 어떻게 해야 간지나는 트리 구조를 만들 수 있을까 방법을 찾다보니 역시나 파일 구조를 트리로 이쁘게 출력해주는 커맨드 툴이 존재했다. tree 커맨드에 대해서 알아보고 알짜배기 내용만 정리했다....

Feb 21, 20242 min read

[Next.js] parallel routes & intercepting routes

트위터 로그인 모달창을 만들어보며 넥스트의 parallel routes 와 intercepting routes 을 학습한 내용을 정리해보았습니다. 트위터 로그인 창을 확인해봅시다. 루트 디렉토리 화면을 배경으로 i/flow/login 페이지가 동시에 표시되고 있습니다. 저는 app router 를 학습하기 전까지는 createPortal 을 사용하여 포탈 영역에 로그인 컴포넌트를 띄우는 방식을 사용했었습니다. const NoLogin =()=...

Feb 1, 20244 min read

C/C++ 이진 트리(binary tree) 개요 및 구현(1)

개요 트리는 노드들이 나무 가지처럼 연결된 비선형 계층적 자료구조이다. 하위 트리가 존재하고, 그 노드에 또 하위 트리가 존재하는 자료구조 이다. 트리의 맨 위에 있는 루트 노드가 존재한다. 우리가 알아볼 트리는 이진 트리이다. 이진 트리는 자식 노드(부모로부터 아래로 이어진 노드)가 2개 이하인 구조를 말한다. 트리의 사용 사례로는 다음과 같다 계층 적 데이터 저장(파일,폴더) 효율적인 검색 속도 힙 데이터 베이스의 인덱싱 트리에 ...

Jan 31, 20244 min read

[React] Server component (RSC)

React.js 18 에 도입된 리액트 서버 컴포넌트는 서버에서 동작하는 리액트 컴포넌트를 의미합니다. Next가 권장하는 라우팅 방식인 app router의 기반이 되는 컴포넌트이기 때문에 app router 를 이해하기 위해서는 server component 에 대한 이해가 필요합니다. server component 리액트는 클라이언트단만을 컴포넌트화하는 대신, server component라는 개념을 통해 서버 영역을 컴포넌트화합니다. ...

Jan 29, 20243 min read

Flutter, JavaScript

42 posts