Basic Auth 미들웨어로 홈페이지 잠그기 (Feat. Next.js Middleware)

`middleware.ts`를 사용하여 특정 경로에 대해 요청을 가로채고 처리할 수 있는, 아주 간단한 Basic 인증 미들웨어를 만들어 잠금 기능을 추가하는 법

Basic Auth 미들웨어로 홈페이지 잠그기 (Feat. Next.js Middleware)
Photo by Muhammad Zaqy Al Fattah / Unsplash

사내 데모나 개인 프로젝트를 프로토타입 단계에서 배포 해놓고, (아무도 들어오진 않겠지만) 외부에 노출시키고 싶지 않을 수 있다.

패스워드로 접근을 제한하는 기능은 일반적인 CMS에서는 무료로 제공된다. 하지만 직접 만든 Next.js 기반 블로그에 적용하려면 구현이 필요하다. (지금 그걸 내가 하는 중이다. 아예 처음부터 에디터, 데이터베이스까지 연동한 테크 블로그를 만들고 있다. 이것은 다음 번에 다루도록 하겠다.) 배포 플랫폼에서도 제공을 하지만, 내가 사용한 Netlify에서는 유료다. 돈을 아끼면 좋고, 또 직접 구현해보고 싶었다.

이 글에서는 Next.js에서 middleware.ts를 사용하여 특정 경로에 대해 요청을 가로채고 처리할 수 있는, 아주 간단한 Basic 인증 미들웨어를 만들어 잠금 기능을 추가하는 방법을 다뤄보겠다. 복잡한 OAuth나 JWT 없이, 정해진 사용자/비밀번호로 인증만 통과하면 접근을 허용하는 방식이다.


구현 방법 (코드 예시: middleware.ts)

// Next.js의 Response 객체와 타입을 불러옴
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';

// 간이 인증용 유저명과 비밀번호 (실제로는 환경변수로 관리해야 함)
const USERNAME = 'ADMIN';
const PASSWORD = 'PASSWORD123';

// 모든 요청마다 실행되는 middleware 함수 정의
export function middleware(req: NextRequest) {
  // 요청 헤더에서 Authorization 정보를 가져옴
  const auth = req.headers.get('authorization');

  // Authorization 헤더가 존재하면
  if (auth) {
    // "Basic base64encoded" 형식으로 되어 있으므로, 공백 기준으로 분리
    const [scheme, encoded] = auth.split(' ');

    // 인증 스킴이 'Basic'일 때만 처리
    if (scheme === 'Basic') {
      // base64 인코딩을 디코딩한 후, 콜론(:) 기준으로 유저명과 비밀번호 분리
      const [user, pass] = atob(encoded).split(':');

      // 유저명과 비밀번호가 일치하면 요청을 허용 (다음 처리로 넘어감)
      if (user === USERNAME && pass === PASSWORD) {
        return NextResponse.next(); // 정상 처리
      }
    }
  }

  // 인증 실패 시, 브라우저에서 로그인 팝업을 띄우도록 401 응답 반환
  return new Response('Authentication required', {
    status: 401,
    headers: {
      'WWW-Authenticate': 'Basic realm="Secure Area"', // 로그인 팝업용 헤더
    },
  });
}

// middleware가 적용될 경로 설정
export const config = {
  // 정적 파일(_next/static 등)은 제외하고 모든 경로에 대해 적용
  matcher: ['/', '/((?!_next/static|_next/image|favicon.ico).*)'],
};

인증 방식: Basic Auth란?

Basic 인증은 HTTP 프로토콜 수준에서 제공하는 간단한 인증 방식이다. 클라이언트가 요청 시 Authorization 헤더에 아래와 같은 형태로 아이디/비밀번호를 담아 보낸다:

Authorization: Basic base64(username:password)

예를 들어 ADMIN:PASSWORD123이라면 아래처럼 base64로 인코딩된다:

Authorization: Basic QURNSU46UEFTU1dPUkQxMjM=

서버는 이 값을 다시 base64 디코딩해서 유저명과 비밀번호를 식별한다.

​환경 변수 분리

해당 프로젝트의 경우 기밀 정보 같은 것이 전혀 없는 껍데기 수준이라 코드에 USERNAME과 PASSWORD를 하드코딩했지만, 중요한 정보가 있다면 환경 변수를 만들어야 한다. 루트 디렉토리에 .env.local 파일을 생성한다.

BASIC_AUTH_USER=ADMIN
BASIC_AUTH_PASS=PASSWORD123

.env.local

그리고 자동으로 인식될 수 있도록 next.config.js 파일에 추가한다.

module.exports = {
  env: {
    BASIC_AUTH_USER: process.env.BASIC_AUTH_USER,
    BASIC_AUTH_PASS: process.env.BASIC_AUTH_PASS,
  },
};

next.config.js

HTTPS가 반드시 필요

Basic Auth는 비밀번호를 암호화하지 않고, 단순히 Base64로 인코딩해서 전송한다.
Base64는 그냥 “문자열을 다른 형태로 바꾼 것”일 뿐이다. 즉, HTTP로 통신하면 누구든지 중간에서 패킷을 가로채서 계정 정보가 그대로 노출될 수 있다. 그래서 HTTPS가 필요하다. HTTPS는 네트워크 구간 전체를 암호화해서 중간자 공격(MITM)을 막아준다. 따라서 Base64 인코딩만 하는 Basic Auth도 HTTPS 위에서 써야 어느 정도 안전하게 작동한다. 참고로 Netlify, Vercel, GitHub Pages 같은 플랫폼은 배포하면 자동으로 HTTPS를 기본 적용해준다.

특정 페이지 막기

위 코드에서는 페이지 전체를 막았지만, 특정 페이지만 보호하고 싶을 때는, middleware.ts에서 matcher에 경로를 써주면 된다.

export const config = {
  matcher: ['/top-secret'], // 이 페이지에만 middleware 실행
};

한계 및 주의사항

1. 진짜 인증이 아님

사용자 관리, 세션, 로그아웃, 비밀번호 리셋 등 모든 기능이 없다. 말 그대로 '들어오는 사람만 걸러내자' 수준의 인증.​

2. UI가 없음

브라우저 기본 팝업 UI가 사용된다. 커스터마이징을 할 수 없다.


마무리

이 방식은 어디까지나 임시용, 비공개용이다. 사용자 인증이 필요한 서비스에서는 OAuth, Firebase Auth, Supabase Auth 등 정식 인증 도구를 써야 한다. 하지만, 빠르게 접근 제어를 걸고 싶을 땐 middleware.ts 하나로도 충분히 막을 수 있다. 직접 쓰기에도 좋고, 개발팀 내부에서 공유할 데모 환경 잠금용으로도 유용하게 쓸 수 있다.