개요

외부로 노출된 관리 페이지를 별도의 서브도메인으로만 접속가능하게 바꾸는 작업입니다.

서비스 사용은 www.doeafit.org, 관리 페이지는 vpn을 이용해 서버가 위치한 네트워크크에 접속한 뒤 cms.local.doeafit.org 로 접속 할 수 있도록 하는 것이 목표입니다.


middleware

next.js 에서 middleware는 요청이 완료되기 전에 코드를 실행할 수 있게 하는 기능입니다. 이를 통해 reroute, redirect 같은 작업을 수행할 수 있습니다.

middleware 작성성

이 작업에서는 서브도메인 기반 라우팅을 구현합니다. 이를 위해 호스트 이름을 확인 하여 환경 변수에 설정된 관리자, 사용자용 서브 도메인을 확인합니다.

 
import { NextRequest, NextResponse } from 'next/server';  
  
/**  
 * @file 서브도메인 기반 라우팅을 위한 Next.js 미들웨어입니다.  
 * @see https://nextjs.org/docs/advanced-features/middleware  
 */  
/**  
 * 미들웨어 설정입니다.  
 * 페이지 라우팅과 관련 없는 모든 요청을 제외하여 불필요한 URL 재작성을 방지합니다.  
 * @property {string[]} matcher - 일치시킬 경로 패턴의 배열입니다.  
 */
 export const config = {  
    matcher: [  
        /*  
         * 다음으로 시작하거나 끝나는 경로는 미들웨어에서 제외합니다:  
         * - api (API 라우트)  
         * - _next/static (정적 파일)  
         * - _next/image (이미지 최적화 파일)  
         * - _next/data (Next.js 내부 데이터 요청)  
         * - favicon.ico (파비콘 파일)  
         * - 일반적인 정적 파일 확장자로 끝나는 모든 경로  
         */        '/((?!api|_next/static|_next/image|_next/data|favicon.ico|.*\\.(?:svg|png|jpg|jpeg|gif|webp|js|css|json|woff|woff2|ttf|eot)$).*)',  
    ],  
};  
  
/**  
 * 요청을 처리하고 호스트 이름에 따라 라우팅하는 미들웨어 함수입니다.  
 * * @param {NextRequest} req - 들어오는 요청 객체입니다.  
 * @returns {NextResponse} 재작성, 리디렉션 또는 다음 응답이 될 수 있는 응답 객체입니다.  
 */
 export function middleware(req: NextRequest): NextResponse {  
    console.log(req.url);  
    const url = req.nextUrl.clone();  
    const host = req.headers.get('host');  
  
    const baseDomain = process.env.NEXT_PUBLIC_DOMAIN;  
    const adminSubdomain = process.env.NEXT_PUBLIC_ADMIN_SUBDOMAIN || 'cms';  
    const siteDomain = 'www.' + process.env.NEXT_PUBLIC_DOMAIN;  
  
    if (!host) {  
        return NextResponse.next();  
    }  
  
    const hostname = host.split(':')[0];  
    const adminFullDomain = `${adminSubdomain}.${baseDomain}`;  
  
    if (hostname === adminFullDomain) {  
        url.pathname = `/cms${url.pathname}`;  
        return NextResponse.rewrite(url);  
    }  
  
    if (hostname === baseDomain || hostname === siteDomain) {  
        url.pathname = `/site${url.pathname}`;  
        return NextResponse.rewrite(url);  
    }  
  
    return NextResponse.next();  
}
 

이를 통해 www.doeafit.org 로 접속하면 pages/site로 ,cms.doeafit.org 로 접속하면 pages/cms로 라우팅 합니다.

pages 디렉토리 정리

기존에 pages 디렉토리에 있던 페이지들을 site, cms에 맞게 정리를 합니다.

이 때 login, robot-check 같이 관리자, 사용자 페이지에 공통적으로 쓰이는 페이지는 컴포넌트로 바꿔 재사용할 수 있도록 했습니다. ( 나중에 pages 디렉토리 정리 시 공통 페이지를 모아 middleware에서 라우팅하도록 해도 되겠다는 생각이 들었습니다. )

로컬 dns 설정

이제 서브도메인 분리는 마쳤지만 실제로 cms.local.doeatfit.org 로 접속하기 위해서는 dns 설정이 필요합니다.

현재 프로젝트에서는 로컬 dns 서버로 unbound dns를 이용하고 있고 *.local.doeatfit.org 에 대한 DNS 조회는 Nginx Proxy Manager 가 설치된 서버로 재정의 하여 doeatfit 컨테이너를 실행중인 서버의 IP로 보내도록 합니다. ( Nginx Proxy Manager WebUI 에서 cms.local.doeatfit.org 설정이 됐다는 가정 하에 )

결과

wireguard vpn을 쓰고 접속 시도 시

vpn 없이 접속 시도 시


회고

  • 관리자 페이지의 존재 자체를 인터넷에서 숨기는 효과를 얻었습니다. VPN에 연결하지 않으면 cms.local.doeafit.org라는 도메인 자체가 존재하지 않으므로, 봇이나 해커의 무차별 대입 공격 시도 자체를 차단할 수 있었습니다.
  • 공통 페이지( login 등)를 컴포넌트로 분리한 것은 좋았으나, 작업 후에 이 페이지들을 별도 디렉토리로 모아 미들웨어에서 직접 라우팅하는 것이 더 효율적일 수 있겠다는 아이디어를 얻었습니다.

참고

https://youtu.be/mpQZVYPuDGU?si=shu4zFd_BhYHdDSl Middleware – Nextjs 한글 문서