지금까지 회사 내부에서 진행한 프로젝트에서는 access token과 refresh token 모두 쿠키로 관리해왔습니다. Nuxt.js를 기술 스택으로 활용하면서, Nuxt에서 제공하는 useCookie 컴포저블을 사용해 쿠키 값을 관리해왔습니다.
하지만 이전 JWT 관련 포스팅의 마지막 부분에서 언급했듯이, 나는 보안적인 이유로 access token은 전역 변수로, refresh token은 httpOnly, secure, samesite 옵션이 적용된 쿠키로 저장하는 것이 더 적절하다고 판단했습니다.
따라서, 기존의 토큰 관리 방식을 개선하기 위해 회사 내부 템플릿 코드를 수정한 경험을 공유하고자 합니다.
useCookie
https://nuxt.com/docs/api/composables/use-cookie
useCookie · Nuxt Composables
useCookie is an SSR-friendly composable to read and write cookies.
nuxt.com
우선 Nuxt3에서 제공하는 useCookie 컴포저블에 대해서 살펴볼 필요가 있습니다.
공식 문서에서는 useCookie를 SSR-friendly한 컴포저블로 설명하고 있습니다.
"SSR-friendly"라는 표현은 말 그대로, 서버 사이드 렌더링(SSR) 환경에서도 정상적으로 작동할 수 있다는 뜻입니다. 일반적으로 쿠키는 브라우저 환경(클라이언트)에서 주로 사용되기 때문에, 서버 환경에서 이를 다루는 것이 조금 더 복잡할 수 있습니다. 하지만 useCookie는 SSR-friendly하게 설계되어 있어, 서버와 클라이언트 모두에서 안전하고 일관된 동작을 기대할 수 있습니다.
그렇다면 이제 핵심은 httpOnly, secure, samsite 옵션을 설정해주는 것입니다. 이 옵션들은 useCookie의 옵션으로 제공됩니다.
위의 내용에 기반하여, 기존에 사용하던 token Store 관련 코드에서 useCookie에 다음과 같이 httpOnly, secure, samesite 값을 true로 설정해줬습니다.
// useTokenStore.ts
// ...
const refreshToken: Ref<string> = useCookie('refreshToken', {
httpOnly: true,
secure: true,
sameSite: true,
});
const setRefreshToken = (token: string) => {
accessToken.value = token;
};
아래 사진은, 로그인을 눌렀을 때 로그인을 성공하여 access token와 refresh token을 받아오는 데에는 성공하고 있는 상황에서 실행한 결과입니다.
하지만 토큰 값이 쿠키에 추가되지 않는 것을 볼 수 있습니다.
이유가 무엇일까요? 옵션을 추가하기 전에는 쿠키가 정상적으로 설정되었기 때문에, 추가한 옵션에 문제가 있는 것이 분명했습니다.
하나씩 옵션을 제거하며 테스트한 결과, httpOnly 옵션을 true로 설정했을 때 쿠키가 추가되지 않는 상황임을 확인했습니다. 그 외에 secure, samesite 옵션을 잘 설정되고 있었습니다.
왜 그럴까요? 여기서 httpOnly 속성에 대해 정리해볼 필요가 있습니다.
httpOnly
httpOnly 속성이 설정된 쿠키는 클라이언트 측 Javascript에서 접근할 수 없습니다. 이는 보안상의 이유로, XSS 공격으로부터 보호하기 위한 중요 방어 수단입니다.
또한, httpOnly 쿠키는 오직 서버 측에서만 설정할 수 있으며 , 클라이언트 측에서 이를 설정하려고 하면 무시됩니다.
따라서 제가 작성한 코드처럼 클라이언트 측에서 useCookie를 사용하여 쿠키를 설정하여 httpOnly 속성을 지정하려고 하면, 이 쿠키는 브라우저에서 설정되지 않습니다. 앞서 언급했듯이, 브라우저의 클라이언트 측 코드는 httpOnly 쿠키를 생성하거나 수정할 수 없기 때문입니다. 이는 브라우저에서 보안을 위해 엄격하게 관리되는 부분입니다.
그렇다면 어떻게 해야할까요? 여기서 useCookie의 SSR-friendly 특성을 활용해야 합니다. Nuxt에서는 서버 측에서 실행되는 코드를 따로 작성할 수 있으며, 이를 통해 httpOnly 속성을 포함한 쿠키를 설정할 수 있습니다. server 폴더를 만들어 해당 폴더 내에서 작성된 코드들은, Nuxt에서 자동으로 스캔하여 API 및 서버 핸들러로 등록합니다. 이렇게 하면 httpOnly 속성을 포함한 쿠키를 서버 측에서 안전하게 설정할 수 있습니다.
server 디렉토리에 대한 자세한 내용은 역시 공식문서에서 확인할 수 있습니다.
https://nuxt.com/docs/guide/directory-structure/server
server/ · Nuxt Directory Structure
The server/ directory is used to register API and server handlers to your application.
nuxt.com
여기서 또 한 가지 중요한 점은, server API에서 쿠키를 처리할 때는 useCookie 대신에 getCookie + setCookie 메소드를 활용해야 한다는 것입니다. 관련 내용 역시 공식 문서에 언급되어 있습니다.
https://nuxt.com/docs/api/composables/use-cookie#cookies-in-api-routes
useCookie · Nuxt Composables
useCookie is an SSR-friendly composable to read and write cookies.
nuxt.com
적용한 개선 사항
위 내용을 바탕으로 기존 로직을 다음과 같이 수정했습니다.
기존 로직:
- 백엔드에 로그인 API를 요청
- 로그인에 성공하여 받은 access token과 'efresh token을 모두 useCookie로 설정 (쿠키 설정 로직이 클라이언트 단에서 처리됨)
수정된 로직:
- 백엔드에 로그인 요청을 보내는 API를 server/api/login.ts 내에 정의하여, 이를 통해 로그인 요청을 처리
- 커스텀 API에서 백엔드 로그인 API를 호출하고, access token은 전역 변수(store)에, refresh token은 쿠키로 설정
하지만 이 로직에서 두 가지 문제가 발생했습니다.
- 기존 API 호출 로직이 Nuxt 플러그인으로 커스텀 되어 있음: 이 커스텀 메소드를 사용하려면 useNuxtApp에 접근해야 했는데, server API 핸들러에서 useNuxtApp에 접근할 수 없었습니다. 따라서, 해당 커스텀 Server API에서 백엔드 API 호출 로직을 적용할 수 없었습니다.
- 전역 변수(store)에 access token을 설정할 수 없음: 첫 번째 문제와 유사하게, server API 핸들러에서는 store에 접근할 수 없었습니다.
위 문제를 해결하기 위해 로직을 다시 구성했습니다.
- /server/api/set-refresh-token 경로에 Server API를 생성
- 기존 클라이언트 단에서 실행되던 로그인 API 호출 로직은 그대로 유지
- 로그인 성공 시 반환된 access token은 클라이언트에서 store의 값을 업데이트
- 반한된 refresh token은 /api/set-refresh-token API를 호출하여, 이 API의 이벤트 핸들러 내에서 쿠키를 설정
// set-refresh-token-cookie.ts
export default defineEventHandler(async (event) => {
const { refreshToken } = await readBody(event);
setCookie(event, 'refreshToken', refreshToken, {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: true,
maxAge: 60 * 60 * 24 * 30,
});
});
// login.vue
// 백엔드 단에 로그인 요청을 보냄
const data = await $api.auth.login(...)
// 응답 받은 refresh 값으로 쿠키를 설정하는 api를 요청
await $fetch('/api/set-refresh-token-cookie/', {
method: 'POST',
body: { refreshToken: data.refresh }
});
이렇게 access token을 전역 변수에 저장하고, refresh token을 httpOnly, secure, samesite 옵션이 적용된 쿠키값으로 관리할 수 있게 되었습니다.
이제 마지막으로 남은 것은, 쿠키를 가져와서 쓰는 로직을 수정하는 것입니다.
해당 부분은 간단합니다. /api/set-refresh-token과 마찬가지로, /api/get-refresh-token의 server API를 정의하고 getCookie를 통해 토큰값을 반환해주면 됩니다.
// get-refresh-token.ts
export default defineEventHandler(async (event) => {
const refreshToken = getCookie(event, 'refreshToken');
return { refreshToken }
});
위의 수정 내용에 맞게, 기존에 401 에러가 났을 때, refresh token 값으로 토큰 값을 갱신하는 로직도 수정을 해줬습니다.
// httpHandler/index.ts
export const handle401Error = async (context: FetchContext): Promise<void> =>{
const { $api } = useNuxtApp();
const tokenStore = useTokenStore();
// 기존: store에서 가져옴 -> 수정: server api로 받아옴
const { refreshToken } = await $fetch('/api/get-refresh-token-cookie');
try {
const { access: newAccessToken, refresh: newRefreshToken} = await $api.auth.refreshToken(refreshToken);
tokenStore.setAccessToken(newAccessToken)
const fetchOptions = {
...context.options,
headers: {
...context.options.headers,
Authorization: `Bearer ${newRefreshToken}`
},
};
await $fetch(context.request, fetchOptions);
} catch (error) {
navigateTo({ path: useNuxtApp().$config.public.loginPath });
}
}
이렇게 JWT (access token, refresh token) 관련 로직을 재정의하여, 기존의 토큰 관리 방식을 개선하는 경험을 가져봤습니다.
'Nuxt.js' 카테고리의 다른 글
[Nuxt3] 스마트한 API 호출 관리: Factory Pattern, 모듈화 (0) | 2024.10.24 |
---|---|
Caching in Nuxt3 (0) | 2024.07.30 |
[Nuxt3] useAsyncData vs useFetch: 결정적 차이, 최적의 선택은? (0) | 2024.07.22 |
Data fetching in Nuxt3: Nuxt3 개발자가 알아야 할 데이터 페칭 방법($fetch, useAsyncData, useFetch) (0) | 2024.07.06 |
Nuxt.js 렌더링 이해하기: 성공적인 웹 개발의 시작 (1) | 2024.07.06 |