본문으로 건너뛰기

안동민 개발노트

본문 시작

보호된 라우트

auth()와 Proxy matcher로 페이지·라우트 핸들러를 보호하고 인증 검사와 업무 권한을 구분합니다.

보호된 라우트는 인증된 사용자만 접근할 수 있는 페이지나 API입니다.

링크를 숨기거나 클라이언트에서 다른 페이지로 보내는 것만으로는 보호가 완성되지 않습니다.

공격자는 화면을 거치지 않고 URL을 직접 요청할 수 있기 때문입니다.

신뢰할 수 있는 서버 경계에서 auth()로 세션을 검사해야 합니다.

보호는 진입점부터 데이터까지 겹쳐 둔다

앞단 차단은 빠른 UX를 만들고 최종 서버 검사는 실제 데이터를 지킨다.

  1. Proxy 넓

    경로를 일찍 분기한다.

  2. Page · Action · Route

    기능 진입 시 인증과 권한을 검사한다.

  3. Data access 조회

    변경 직전에 소유권을 확정한다.


서버 컴포넌트 보호

페이지 하나를 보호할 때는 서버 컴포넌트에서 세션을 확인하는 방식이 가장 명확합니다.

src/app/account/page.tsx
import { auth } from '@/auth';
import { redirect } from 'next/navigation';

export default async function AccountPage() {
  const session = await auth();

  if (!session?.user) {
    redirect('/login');
  }

  return <h1>{session.user.name}님의 계정</h1>;
}

redirect() 이후에는 페이지 본문이 렌더링되지 않습니다.

이 검사는 요청마다 서버에서 수행됩니다.

여러 페이지가 같은 규칙을 사용하더라도 먼저 각 데이터 접근 지점이 세션을 검사하도록 만듭니다.

보호 페이지는 auth() 뒤에 렌더한다

서버에서 세션을 읽고 없으면 데이터 조회 전에 이동시킨다.

  1. Request 보호 페이지 요청

    들어온다.

  2. auth() 서버

    현재 세션을 읽는다.

  3. 세션 없음 로그인 페이지

    redirect한다.

  4. 세션 있음 필요한 데이터

    조회하고 렌더한다.


라우트 핸들러 보호

API는 화면과 별도로 호출될 수 있으므로 자체 검사가 반드시 필요합니다.

auth로 라우트 핸들러를 감싸면 요청의 auth 속성에서 세션을 읽을 수 있습니다.

src/app/api/posts/route.ts
import { auth } from '@/auth';

export const POST = auth(async (request) => {
  if (!request.auth?.user) {
    return Response.json({ message: '로그인이 필요합니다.' }, { status: 401 });
  }

  const body = await request.json();

  return Response.json({
    author: request.auth.user.email,
    title: body.title,
  });
});

로그인하지 않은 요청에는 401 Unauthorized를 반환합니다.

로그인은 했지만 해당 작업 권한이 없으면 403 Forbidden을 반환합니다.

두 상태를 구분하면 클라이언트와 운영 로그에서 원인을 정확히 판단할 수 있습니다.

Route Handler는 401과 403을 구분한다

로그인하지 않은 요청과 권한이 부족한 요청은 다른 실패다.

  1. HTTP 요청 auth()

    정책으로 호출자를 판정한다.

  2. 세션 없음 · 401 로그인

    필요함을 알린다.

  3. 권한 없음 · 403 신원

    있지만 행동을 허용하지 않는다.

  4. 허용 · 2xx 업무 로직

    실행한다.


여러 경로를 Proxy에서 보호

여러 페이지에 공통 로그인 경계를 적용할 때는 중앙 인증 모듈의 auth를 Proxy로 내보냅니다.

src/proxy.ts
export { auth as proxy } from '@/auth';

export const config = {
  matcher: ['/account/:path*', '/dashboard/:path*'],
};

matcher는 Proxy를 실행할 경로만 고릅니다.

인증 여부에 따른 허용 판단은 authorized 콜백에서 정의합니다.

src/auth.ts
import NextAuth from 'next-auth';
import GitHub from 'next-auth/providers/github';

export const { handlers, auth, signIn, signOut } = NextAuth({
  providers: [GitHub],
  callbacks: {
    authorized({ auth, request }) {
      const isProtected =
        request.nextUrl.pathname.startsWith('/account') ||
        request.nextUrl.pathname.startsWith('/dashboard');

      if (!isProtected) {
        return true;
      }

      return Boolean(auth?.user);
    },
  },
});

공개 경로는 true를 반환하고 보호 경로는 세션 존재 여부를 반환합니다.

정적 파일과 Auth.js 콜백 경로까지 무분별하게 가로채지 않도록 matcher 범위를 좁힙니다.

Proxy는 matcher와 authorized로 진입을 거른다

matcher가 실행 범위를 정하고 authorized가 세션 기반 진입 여부를 정한다.

  1. Request URL

    들어온다.

  2. matcher Proxy 실행 대상인지 판정한다
  3. authorized 세션

    경로로 진입을 판정한다.

  4. next · redirect 통과시키거나 로그인

    보낸다.


Proxy와 데이터 경계 구분

Proxy는 페이지 진입을 빠르게 차단하는 데 유용합니다.

하지만 중요한 데이터 변경은 라우트 핸들러나 서버 액션에서도 권한을 다시 확인해야 합니다.

Proxy 설정이 바뀌거나 내부 함수가 직접 호출되는 경우에도 데이터 규칙이 유지되어야 하기 때문입니다.

src/actions/delete-post.ts
'use server';

import { auth } from '@/auth';

export async function deletePost(postId: string) {
  const session = await auth();

  if (!session?.user) {
    throw new Error('로그인이 필요합니다.');
  }

  // 게시글 소유권 또는 관리자 권한을 확인한 뒤 삭제합니다.
}

인증 검사는 사용자를 확인합니다.

소유권과 역할 검사는 그 사용자가 해당 게시글을 변경할 수 있는지 확인합니다.

두 판단은 서로 대체할 수 없습니다.

Proxy 통과는 데이터 접근 허가가 아니다

경로 진입과 실제 자원 접근은 서로 다른 정보가 필요하다.

  1. Proxy 진입 검사 경로

    가벼운 세션 정보로 빠르게 분기한다.

  2. 데이터 최종 검사 최신 역할, 자원 존재, 소유권

    확인한다.

중요한 변경 작업은 세션 확인과 리소스 권한 확인을 같은 서버 경계에서 수행합니다.

Server Action은 인증 뒤 소유권을 확인한다

변경 대상 ID가 내 자원인지 확인한 뒤에만 저장한다.

  1. 입력 검증 형식

    허용 범위를 확인한다.

  2. auth() 호출자의 서버 세션

    읽는다.

  3. 소유권 조회 대상 자원의 ownerId

    비교한다.

  4. 변경 허용된 경우에만 저장한다

클라이언트 보호의 역할

클라이언트에서는 로그인하지 않은 사용자에게 작성 버튼을 숨길 수 있습니다.

이 처리는 사용자 경험을 개선하지만 보안 경계는 아닙니다.

실제 작성 API가 auth()를 검사하지 않으면 숨겨진 버튼과 관계없이 요청을 보낼 수 있습니다.

따라서 클라이언트 검사는 화면 표현에, 서버 검사는 데이터 보호에 사용합니다.

클라이언트 보호는 UX 보조선이다

빠른 화면 전환에는 유용하지만 보안 경계가 될 수 없다.

  1. Client UX 로딩 표시, 버튼

    숨김, 로그인 안내

  2. Server security 페이지, action

    route, 데이터 접근 차단


보호 기준

페이지 하나는 서버 컴포넌트에서 auth()로 보호합니다.

여러 경로의 공통 진입 규칙은 Proxy와 matcher로 표현합니다.

API와 서버 액션은 호출 지점에서 인증과 업무 권한을 다시 검사합니다.

보호 위치는 필요한 정보와 시점으로 고른다

빠른 분기와 정밀 판정을 한곳에 억지로 합치지 않는다.

  1. Proxy 넓

    경로를 요청 초기에 분기

  2. Server Page 렌더 전에 세션

    페이지 권한 확인

  3. Server Action 변경 직전 입력

    소유권 확인

  4. Route Handler HTTP 상태

    API 권한 확정

401403을 구분하고, 리다이렉트 경로는 내부 허용 목록으로 제한합니다.

다음 절에서는 세션에 역할을 포함하고 관리자와 일반 사용자의 권한을 나눕니다.