[Nextjs] Next.js Middleware로 JWT Access Token 갱신하기

MinJae·어제
0

jwt

이미지출처

JWT(JSON Web Token)는 사용자 인증을 관리하기 위한 강력한 도구입니다. 하지만, JWT의 짧은 유효 기간 때문에 사용자가 인증 상태를 유지하려면 Access Token을 주기적으로 갱신해야 합니다. 이번 글에서는 Next.js의 Middleware를 활용하여 Access Token을 자동으로 갱신하는 방법을 살펴보겠습니다.


1. 문제 정의: Access Token의 유효 기간

Access Token은 클라이언트가 인증된 상태를 증명하기 위해 서버에 전송하는 JSON Web Token입니다. 보안상의 이유로 Access Token은 유효 기간(exp)을 짧게 설정하는 경우가 많습니다.

이로 인해 사용자는 토큰이 만료될 때마다 새로 로그인해야 하는 불편함을 겪을 수 있습니다. 이를 해결하기 위해 Refresh Token을 활용하여 Access Token을 갱신하는 방식이 일반적입니다.

현재 진행하는 프로젝트에서는 Access Token은 30분, Refresh Token은 7일의 유효 기간이고 토큰 갱신 코드가 완성되면 Access Token의 유효 기간을 더 줄여서 보안 강화할 계획입니다.


2. Next.js Middleware의 역할

Next.js Middleware는 서버와 클라이언트 사이의 요청을 가로채어 필요한 전처리를 수행할 수 있는 강력한 기능을 제공합니다. 이를 활용해 보호된 경로에 접근하기 전에 JWT의 유효성을 검증하고, 필요할 경우 Access Token을 갱신할 수 있습니다.


3. 구현: Middleware 코드

// middleware.ts
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
import { sendRefreshTokenRequest } from '@/api/auth';
import { jwtDecode } from 'jwt-decode';

const PROTECTED_ROUTES = ['/create', '/chat', '/mypage'];
const AUTH_ROUTES = ['/auth/signIn', '/auth/signUp'];

export const middleware = async (req: NextRequest) => {
  const { pathname } = new URL(req.url);

  if (AUTH_ROUTES.some(route => pathname.startsWith(route))) {
    const refreshToken = req.cookies.get('refreshToken')?.value;

    if (refreshToken) {
      return NextResponse.redirect(new URL('/mypage', req.url));
    }
    return NextResponse.next();
  }

  if (!PROTECTED_ROUTES.some(route => pathname.startsWith(route))) {
    return NextResponse.next();
  }

  const accessToken = req.cookies.get('accessToken')?.value;
  const refreshToken = req.cookies.get('refreshToken')?.value;

  if (!accessToken && !refreshToken) {
    return NextResponse.redirect(new URL('/auth/signIn', req.url));
  }

  try {
    if (accessToken) {
      const decodedToken: { exp: number } = jwtDecode(accessToken);
      const currentTime = Math.floor(Date.now() / 1000);

      if (decodedToken.exp > currentTime) {
        return NextResponse.next();
      }
    }

    if (refreshToken) {
      const refreshResponse = await sendRefreshTokenRequest(refreshToken);

      if (refreshResponse.success && refreshResponse.tokens) {
        const response = NextResponse.next();

        response.cookies.set('accessToken', refreshResponse.tokens.access, {
          httpOnly: true,
          secure: process.env.NODE_ENV === 'production',
          path: '/',
        });
        response.cookies.set('refreshToken', refreshResponse.tokens.refresh, {
          httpOnly: true,
          secure: process.env.NODE_ENV === 'production',
          path: '/',
        });

        return response;
      } else {
        console.error('Refresh token 갱신 실패:', refreshResponse.message);
        const response = NextResponse.redirect(new URL('/auth/signIn', req.url));
        response.cookies.delete('refreshToken');
        return response;
      }
    }

    return NextResponse.redirect(new URL('/auth/signIn', req.url));
  } catch (error) {
    console.error('Token verification error:', error);
    const response = NextResponse.redirect(new URL('/auth/signIn', req.url));
    response.cookies.delete('refreshToken');
    return response;
  }
};

export const config = {
  matcher: ['/create/:path*', '/chat/:path*', '/mypage/:path*', '/auth/signIn', '/auth/signUp'],
};

4. Access Token 갱신 요청

sendRefreshTokenRequest 함수는 서버로 Refresh Token을 전송하여 Access Token을 갱신합니다.

export const sendRefreshTokenRequest = async (refresh: string): Promise<RefreshResponse> => {
  if (!refresh) {
    return {
      success: false,
      message: 'Refresh token이 없습니다.',
    };
  }

  try {
    const response = await fetch(`${process.env.NEXT_PUBLIC_BASE_URL}/api/users/refresh`, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        Authorization: `Bearer ${refresh}`,
      },
      body: JSON.stringify({ refresh }),
      credentials: 'include',
    });

    if (!response.ok) {
      const errorData = await response.json().catch(() => ({}));
      console.error('Refresh API Error:', {
        status: response.status,
        statusText: response.statusText,
        error: errorData,
      });

      return {
        success: false,
        message: `토큰 갱신 실패 (${response.status}): ${errorData.message || response.statusText}`,
      };
    }

    const data: RefreshResponse = await response.json();
    return data;
  } catch (error) {
    console.error('토큰 갱신 요청 오류:', error);
    return {
      success: false,
      message: error instanceof Error ? error.message : '토큰 갱신 요청에 실패했습니다.',
    };
  }
};

마무리

Next.js의 Middleware는 클라이언트와 서버 간의 인증 로직을 관리하는 데 매우 유용합니다. 위 코드를 참고하여 여러분의 프로젝트에 Access Token 갱신 기능을 통합해 보세요. 이 과정에서 보안 및 사용자 경험을 모두 만족시키는 인증 시스템을 구현할 수 있을 것입니다.


✅ 참고

profile
고양이 간식 사줄려고 개발하는 사람

0개의 댓글