안동민 개발노트

안동민 개발노트

Auth.js 설정로그인과 로그아웃보호된 라우트역할 기반 접근 제어
본문 시작
  1. 홈
  2. 문서
  3. Next.js
  4. 10장 : 인증 및 권한 관리
  5. 보호된 라우트
  1. Next.js
  2. 보호된 라우트

보호된 라우트

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

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

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

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

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


서버 컴포넌트 보호

페이지 하나를 보호할 때는 서버 컴포넌트에서 세션을 확인합니다. 아래 /login은 직접 만든 로그인 페이지를 전제로 합니다.

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() 이후에는 페이지 본문이 렌더링되지 않습니다.

서버가 이 페이지를 렌더링할 때 검사를 수행합니다. 이미 받은 UI의 클라이언트 재사용이 새 권한 검사를 대신하지는 않습니다.

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


라우트 핸들러 보호

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를 반환합니다. 로그인 뒤에는 JSON을 읽어 일부 값을 되돌려 줄 뿐, 본문 스키마 검증이나 게시글 저장을 구현하지 않았습니다. 잘못된 JSON의 처리도 별도로 필요합니다.

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

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


여러 경로를 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와 데이터 경계 구분

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('로그인이 필요합니다.');
  }

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

위 deletePost는 세션 검사까지만 구현한 골격입니다. 주석 위치에서 게시글 조회, 소유권·역할 판정, 삭제를 구현해야 합니다. 인증은 사용자 확인이고, 인가는 해당 사용자의 작업 허용 여부이므로 서로 대체할 수 없습니다.


클라이언트 보호의 역할

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

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

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

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


보호 기준

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

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

로그인과 로그아웃

이전 페이지

역할 기반 접근 제어

다음 페이지

이 페이지의 목차

서버 컴포넌트 보호라우트 핸들러 보호여러 경로를 Proxy에서 보호Proxy와 데이터 경계 구분클라이언트 보호의 역할보호 기준