← 전체 글로 돌아가기

Next.js

Next.js 미들웨어 인증에서 리다이렉트 루프를 만든 실수

로그인 보호 미들웨어를 추가한 뒤 로그인 페이지까지 보호해 버려 무한 리다이렉트가 난 상황을 정리했다.

시작은 너무 단순한 보호 코드였다

관리자 화면을 막으려고 Next.js 미들웨어에서 쿠키를 검사했다. 쿠키가 없으면 로그인 페이지로 보내는 흔한 코드였는데, 배포 후 브라우저가 "too many redirects"를 보여 줬다. 서버가 죽은 것도 아니고 라우트도 존재했는데 로그인 화면에 도착하지 못했다.

문제는 로그인 페이지도 보호 대상에 포함해 버린 것이었다.

import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'

export function middleware(request: NextRequest) {
  const token = request.cookies.get('admin_session')?.value

  if (!token) {
    return NextResponse.redirect(new URL('/admin/login', request.url))
  }

  return NextResponse.next()
}

export const config = {
  matcher: ['/admin/:path*'],
}

/admin/login/admin/:path*에 걸리기 때문에, 로그인 페이지 요청이 다시 로그인 페이지로 리다이렉트됐다.

로그에서 본 단서

처음에는 미들웨어 안에 임시 로그를 넣고 경로를 찍었다.

console.log('middleware path:', request.nextUrl.pathname)

배포 로그에 /admin/login이 반복해서 찍히는 것을 보고 범위를 잘못 잡았다고 확신했다. 브라우저 개발자 도구의 Network 탭에서도 307 응답이 같은 주소로 계속 이어졌다.

고친 방식

나는 로그인, 정적 파일, 공개 API를 먼저 제외하는 쪽으로 수정했다. 조건을 길게 늘어놓기보다 "보호할 경로"와 "예외 경로"를 분리해서 읽히게 만들었다.

const publicAdminPaths = ['/admin/login']

export function middleware(request: NextRequest) {
  const { pathname } = request.nextUrl

  if (publicAdminPaths.includes(pathname)) {
    return NextResponse.next()
  }

  const token = request.cookies.get('admin_session')?.value

  if (!token) {
    return NextResponse.redirect(new URL('/admin/login', request.url))
  }

  return NextResponse.next()
}

export const config = {
  matcher: ['/admin/:path*'],
}

점검표

비슷한 인증 미들웨어를 넣을 때는 아래 순서로 확인한다.

  • 로그인 페이지 자체가 보호 대상에 들어가지 않았는가
  • 로그아웃 API가 인증 실패 리다이렉트에 막히지 않는가
  • 이미지, CSS, Next.js 내부 파일까지 matcher에 들어가지 않는가
  • 리다이렉트 대상 URL이 현재 URL과 같지 않은가
  • 배포 환경의 쿠키 이름과 로컬 쿠키 이름이 같은가

남은 기준

미들웨어는 짧아 보여도 모든 요청 앞에 선다. 그래서 조건 하나가 틀리면 특정 페이지 하나가 아니라 서비스 흐름 전체가 꼬인다. 지금은 인증 코드를 추가하면 로그인 페이지를 시크릿 창에서 직접 여는 테스트를 먼저 한다.