본문 바로가기
next.js

API Route에서 jwt 토큰 유효성검증, next/headers cookies 함수

by DevPearl 2024. 9. 10.

짧은 지식으로 인해 글의 신뢰성이 높지 않습니다. 정확한 내용은 공식문서를 참고해 주세요.

 

목차)

1. 서론

2. API에서 jwt 토큰 유효성 검증

3. next/headers cookies함수

 

1. 서론

이 글은 JWT를 활용한 세션관리 중에서도 API에서의 토큰 유효성 검증 중심으로 작성하였습니다.

쿠키에 토큰을 저장해 세션관리를 하는 원리를 그려보았다

 

토큰 유효성검증을 나는 아래 세 가지 방법으로 나누었다.

1. 미들웨어

2. API 

3. 백그라운드에서 주기적 검증

 

API에서의 토큰 검증은 웹앱에서 가장 보편적으로 사용하는 방법이고,

(나의 경우 실시간 웹소켓통신을 해서 세 가지 모두 적용했다)

 

Next.js 에서 미들웨어 토큰 검증 및 토큰 재발급은 다음 글에 작성하였다.

 

2. API에서 jwt 토큰 유효성 검증

API요청,응답에서 토큰을 검증할때 서버와 클라이언트가 각각 할 일로 나눌 수 있다.

 

서버에서는 요청헤더를 확인해 토큰이 유효한지 확인하고, 유효하지 않을 경우 401 에러를 throw 한다.

 

클라이언트에서 Authorization헤더에 토큰을 추가해 전송했을 경우 Authorization에서 토큰을 추출하면 되고, 클라이언트에서 자동으로 쿠키로 토큰을 전송했을 경우 헤더의 쿠키필드에서 추출하면 된다.

나는 두 번째 방법을 사용했고, 이 경우 CSRF(Cross-Site Request Forgery) 공격 방어를 위해 따로 csrf토큰을 사용하는 게 좋다.(직접 구현할 수 있고, next-auth를 쓰면 자동으로 만들어준다)

 

클라이언트에서는

1. 요청 헤더에 토큰을 포함해 보낸다(Athorization에 보낼 경우)

2. 응답이 401 에러인 경우 토큰 재발급 요청을 하고 다시 시도한다

3. 토큰 재발급에 실패한 경우 토큰삭제 후 로그인 화면으로 이동한다.

의 작업이 필요하다.

 

이 중 1, 2번 작업을 가장 편하고 보편적으로 구현하는 것은

axios 라이브러리의 interceptors 기능을 쓰는 것이다.

 

예를 들어 아래처럼 설정해서 

request를 보내기 전 Authorization헤더에 토큰을 추가로 하고,

response를 받으면 응답 코드가 401인지 확인해서 토큰 재발급 요청을 하면 된다.

import axios from 'axios';

// axios 인스턴스 생성
const apiClient = axios.create({
  baseURL: '/api',
  withCredentials: true,
});

// 요청 전 Authorization 헤더에 accessToken 추가
apiClient.interceptors.request.use(
  (config) => {
    const accessToken = localStorage.getItem('accessToken'); 
    if (accessToken) {
      config.headers['Authorization'] = `Bearer ${accessToken}`;
    }
    return config;
  },
  (error) => {
    return Promise.reject(error);
  }
);

// 응답 인터셉터: 401이면 토큰 재발급 요청 후 재시도
apiClient.interceptors.response.use(
  (response) => {
    return response;
  },
  async (error) => {
    const originalRequest = error.config;

    // 401 에러 처리
    if (error.response.status === 401 && !originalRequest._retry) {
      originalRequest._retry = true;

      try {
        // refreshToken 가져오기 (로컬 스토리지 또는 쿠키에서)
        const refreshToken = localStorage.getItem('refreshToken');
        if (!refreshToken) {
          throw new Error('Refresh token not found');
        }

        // refreshToken을 Authorization 헤더에 추가하여 재발급 요청
        const { data } = await axios.post(
          '/api/auth/refresh', 
          {}, 
          {
            headers: {
              'Authorization': `Bearer ${refreshToken}`,
            },
            withCredentials: true,
          }
        );

        const newAccessToken = data.accessToken;

        // 새로 발급받은 accessToken을 로컬 스토리지에 저장
        localStorage.setItem('accessToken', newAccessToken);

        // 원래 요청에 새 accessToken 추가하여 재시도
        originalRequest.headers['Authorization'] = `Bearer ${newAccessToken}`;
        return apiClient(originalRequest);
      } catch (refreshError) {
        // 재발급 실패 시 토큰 삭제 및 로그인 페이지로 이동
        localStorage.removeItem('accessToken');
        localStorage.removeItem('refreshToken');
        window.location.href = '/signin'; 
      }
    }

    return Promise.reject(error);
  }
);

export default apiClient;

 

 

나는 이 방법을 쓰지 않았는데, 서버사이드렌더링인 Next js를 쓰면서

서버에서 토큰을 확인할 때, 브라우저에서 쿠키를 자동으로 전송한 쿠키를 next/haeders의 cookie함수로 확인하고 업데이트했기 때문이다.

 

3. next/headers cookies함수

next/headers의 cookies함수는 HTTP 요청의 쿠키를 읽거나, 서버 액션이나 API Route에서 쿠키를 업데이트(추가, 수정, 삭제) 할 수 있게 해 준다.

 

주의할 점은 cookies는 헤더에 접근해 값을 가져오기 때문에 dynamic 한 작업을 한다. 따라서 레이아웃이나 페이지에서 사용하면 다이나믹 렌더링이 된다. 

(동적 렌더링 시 서버가 요청 시마다 페이지를 렌더링 하기 때문에 사용자별로 다른 화면이나 데이터 보여줄 때 유리, 여러번 시행할 경우 비효율적 이므로 react cache로 per request 캐시를 하기도 한다.)

 

나는 로그인 성공 시 서버에서 cookies.set으로 accessToken과 refreshToken을 set 하고,

클라이언트에서 브라우저가 자동으로 보내주는 쿠키를 서버에서 cookies.get으로 확인하는 방법을 사용했다.

 

그래서 따로 토큰을 Authorization헤더에 추가할 필요가 없었고,

그래서 axios의 interceptors를 활용하지 않고 간단하게 customFertch를 만들어 response status가 401일 때

토큰을 재발급요청 후 다시 fetch 해줬다.

 

import { signoutUser } from "@/actions/auth";

async function fetchWithAuthRetry(url: string): Promise<Response> {
  const makeFetchRequest = async (url: string): Promise<Response> => {
    return await fetch(url);
  };

  let response = await makeFetchRequest(url);

  if (response.status === 401) {
    const refreshResponse = await fetch("/api/auth/refresh", {
      method: "POST",
    });

    if (refreshResponse.ok) {
      // 새로 발급한 토큰으로 다시 요청
      response = await makeFetchRequest(url);
    } else {
      // 재발급 실패시 로그아웃
      signoutUser();
    }
  }

  return response;
}

export default fetchWithAuthRetry;