[ Next Auth ] token으로 로그인 작동 프로세스

루비·2024년 6월 4일
0

NextJS

목록 보기
7/7

[ 개발환경 ] typescript + nextjs (app router) + tailwindcss ...

서론

기존 admin 페이지에서 token이 존재하지 않으면, 로그인이 안되게 프로세스를 바꾸었습니다.

[ 이유 ]
1. admin 기능들이 복잡해짐.
2. admin 관리자가 늘어날 예정.

보안의 필요성이 느껴져, 토큰을 관리해주는 로직을 추가하게 되었습니다.

// _App.tsx 폴더에 sessionProvieder을 추가한다. 
...

export default function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <html lang="en">
      <body className={inter.className}>
        <AuthContext>
        ...
        	{children}
        ...
        </AuthContext>
      </body>
    </html>
  );
}
  • 아래에 하위 Provider이 많이 존재하므로, AuthContext의 폴더를 따로 만들어서 추가하였습니다.
  • 우리는 layout 폴더가, _app.tsx의 기능을 수행함으로, layout 페이지에서 작업을 하였습니다.
// AuthContext.tsx
"use client";
import { SessionProvider } from "next-auth/react";
import type { ReactNode } from "react";

type Props = {
  children: ReactNode;
};

export default function AuthContext({ children }: Props) {
  return <SessionProvider>{children}</SessionProvider>;
}
  • AuthContext의 파일 안에서, SessionProvider를 사용하였습니다. 이를 통해 애플리케이션 전체에서 NextAuth 세션 관리를 일관되게 처리할 수 있습니다. 이 방식은 클라이언트 측에서 세션을 제공하는 역할을 합니다.

  • SessionProvider는 NextAuth에서 제공하는 컴포넌트로, 클라이언트 측에서 세션 상태를 관리하고 제공하는 역할을 합니다. SessionProvider는 애플리케이션의 모든 컴포넌트가 세션 상태에 접근할 수 있도록 React Context를 사용하여 세션 정보를 공유합니다.

SessionProvider의 주요 역할

  1. 세션 관리: 세션 상태를 관리하고, 로그인, 로그아웃, 세션 갱신 등의 작업을 처리합니다.
  2. 세션 정보 제공: 애플리케이션 내의 모든 컴포넌트가 세션 정보에 접근할 수 있도록 Context API를 사용하여 세션 데이터를 제공합니다.
  3. 자동 세션 갱신: 세션이 만료되기 전에 자동으로 갱신하여 사용자가 계속 로그인 상태를 유지할 수 있도록 합니다.
// middleware.ts
import { withAuth } from "next-auth/middleware";

export default withAuth({
  callbacks: {
    authorized: ({ token, req }) => {
      if (token) return true;
      return false;
    },
  },
  pages: {
    signIn: "/sign-in",
  },
});

export const config = {
  matcher: [
    /*
     * Match all request paths except for the ones starting with:
     * - api (API routes)
     * - _next/static (static files)
     * - _next/image (image optimization files)
     * - favicon.ico (favicon file)
     */
    "/((?!api|_next/static|_next/image|favicon.ico).*)",  // 특정 경로를 제외한 모든 경로에 대해 인증을 강제하려면
  ],
};
  • middleware에서 token이 있으면 true를, 없으면 false를 반환하게 작성하였습니다.

withAuth는 NextAuth에서 제공하는 미들웨어로, Next.js 애플리케이션에서 특정 경로에 대한 인증을 강제하는 데 사용됩니다. 이 미들웨어를 사용하면 인증되지 않은 사용자가 보호된 페이지에 접근하려고 할 때 로그인 페이지로 리디렉션됩니다.

[ 주요 기능 ]
1. 인증 강제: 특정 경로에 대해 인증을 요구할 수 있습니다.
2. 리디렉션: 인증되지 않은 사용자가 보호된 경로에 접근하려고 하면 로그인 페이지로 리디렉션합니다.
3. 유연한 설정: 보호할 경로를 설정 파일이나 코딩을 통해 유연하게 정의할 수 있습니다.

// next-auth.d.ts
import NextAuth, { DefaultSession, DefaultUser } from "next-auth";

declare module "next-auth" {
  interface Session {
    accessToken?: string;
  }

  interface User {
    token?: string;
  }
}

NextAuth에서 제공하는 기본 타입들을 확장하는 역할을 합니다. 이를 통해 프로젝트의 타입 정의를 커스터마이징하여, 특정 요구 사항에 맞는 타입 정보를 추가할 수 있습니다.

[ 주요 목적 ]

  • 타입 확장: NextAuth에서 기본적으로 제공하는 Session 및 User 인터페이스를 확장하여 커스텀 속성을 추가할 수 있습니다.
  • 타입 안전성: 프로젝트 전반에 걸쳐 타입 안전성을 보장합니다. TypeScript를 사용하면 코드에서 잠재적인 타입 오류를 컴파일 단계에서 잡을 수 있습니다.
  • 개발자 경험 향상: 확장된 타입 정의를 통해 코드 작성 시 보다 구체적인 타입 정보를 제공하여, 코드 자동 완성 및 타입 검사를 보다 효율적으로 할 수 있습니다.
// [...nextauth]/route.ts
import NextAuth, { NextAuthOptions } from "next-auth";
import CredentialsProvider from "next-auth/providers/credentials";

export const authOptions: NextAuthOptions = {
  providers: [
    CredentialsProvider({
      name: "Credentials",
      credentials: {
        email: { label: "email", type: "text" },
        password: { label: "password", type: "password" },
      },
      authorize: async (credentials) => {
        const res = await fetch(
          `${process.env.NEXT_PUBLIC_SERVER_URL}/sign-in`,
          {
            method: "POST",
            body: JSON.stringify(credentials),
            headers: { "Content-Type": "application/json" },
          }
        );

        const result = await res.json();

        if (result.success && result.data) {
          return {
            ...result.data,
            token: result.data.token,
          };
        }

        return null;
      },
    }),
  ],
  callbacks: {
    async jwt({ token, user }) {
      if (user) {
        token.accessToken = user.token;
      }
      return token;
    },
    async session({ session, token }) {
      if (token) {
        session.accessToken = token.accessToken as string;
      }
      return session;
    },
  },

  pages: {
    signIn: "/sign-in",
  },
};

const handler = NextAuth(authOptions);

export { handler as GET, handler as POST };

[ 역할 ]

  • providers: 인증 제공자를 정의합니다. 여기서는 CredentialsProvider를 사용하여 이메일과 비밀번호로 인증합니다.
  • authorize 함수: 사용자가 제출한 자격 증명을 검증하고, 성공하면 사용자 객체를 반환합니다.
  • callbacks:
    1. jwt: JWT 토큰을 생성하거나 갱신할 때 호출됩니다. 사용자 객체가 있으면 토큰에 accessToken을 추가합니다.
    2. session: 세션이 생성될 때 호출됩니다. 세션 객체에 accessToken을 추가합니다.
  • pages: 커스텀 로그인 페이지 경로를 지정합니다.
// 서버사이드로 세션 확인 및 상태 관리 
import { getServerSession } from "next-auth";
import "../../globals.css";
import { authOptions } from "@/app/api/auth/[...nextauth]/route";
import { redirect } from "next/navigation";
import SignInForm from "./_components/SignInForm";

export default async function SignIn() {
  const session = await getServerSession(authOptions);

  if (session?.user) {
    return redirect("/");
  }

  return (
    <article className="w-full h-screen flex items-center justify-center bg-bg-1 px-4">
      <SignInForm />
    </article>
  );
}

서버사이드 세션 확인의 장점과 단점

[ 장점 ]

  • 보안: 서버사이드에서 세션을 확인하면 클라이언트로 민감한 데이터가 전달되기 전에 유효성을 검증할 수 있습니다. 이는 클라이언트가 데이터를 조작할 수 없도록 하여 보안을 강화합니다.
  • SEO: 서버사이드에서 세션을 확인하면 검색 엔진 크롤러가 페이지를 올바르게 인덱싱할 수 있습니다. 이는 특히 콘텐츠가 인증된 사용자에게만 표시되어야 하는 경우에 유용합니다.
  • 빠른 리다이렉션: 서버사이드에서 세션을 확인하면 클라이언트가 페이지를 로드하기 전에 적절한 리다이렉션을 수행할 수 있습니다. 이는 불필요한 페이지 로딩을 방지하고 사용자 경험을 향상시킵니다.

[ 단점 ]

  • 복잡성: 서버사이드 렌더링과 관련된 로직은 클라이언트사이드 렌더링보다 더 복잡할 수 있습니다.
  • 성능: 서버사이드에서 세션을 확인하는 것은 서버 리소스를 사용하므로, 클라이언트사이드에서 확인하는 것보다 성능이 떨어질 수 있습니다.
// 클라이언트 사이드로 세션 확인 및 상태 관리 
"use client";
import { useSession, signIn } from "next-auth/react";
import { useRouter } from "next/router";
import { useState, useEffect } from "react";

export default function SignInForm() {
  const [email, setEmail] = useState("");
  const [password, setPassword] = useState("");
  const router = useRouter();
  const { data: session, status } = useSession();

  useEffect(() => {
    if (status === "authenticated") {
      router.push("/dashboard");
    }
  }, [status, router]);

  const handleSubmit = async (e: React.FormEvent) => {
    e.preventDefault();

    const result = await signIn("credentials", {
      redirect: false,
      email,
      password,
    });

    if (result?.error) {
      console.error("Login error:", result.error);
      alert("로그인에 실패하였습니다.");
    } else {
      router.push("/dashboard");
      alert("로그인에 성공하였습니다.");
    }
  };

  if (status === "loading") {
    return <div>Loading...</div>;
  }

  return (
    <form onSubmit={handleSubmit}>
      <div>
        <label>
          Email
          <input
            type="email"
            name="email"
            value={email}
            onChange={(e) => setEmail(e.target.value)}
          />
        </label>
      </div>
      <div>
        <label>
          Password
          <input
            type="password"
            name="password"
            value={password}
            onChange={(e) => setPassword(e.target.value)}
          />
        </label>
      </div>
      <button type="submit">Sign in</button>
    </form>
  );
}

클라이언트사이드 세션 확인의 장점과 단점

[ 장점 ]

  • 단순성: 클라이언트사이드에서 세션을 확인하는 로직은 더 간단하고 구현하기 쉽습니다.
  • 성능: 클라이언트사이드에서 세션을 확인하면 서버의 부하를 줄이고,사용자가 페이지를 더 빠르게 로드할 수 있습니다.
  • 반응성: 클라이언트사이드에서 세션을 확인하면 상태 변화에 대해 더 빠르게 반응할 수 있습니다. 예를 들어, 사용자가 로그아웃하거나 세션이 만료되었을 때 즉시 반응할 수 있습니다.

[ 단점 ]

  • 보안: 클라이언트사이드에서 세션을 확인하면 클라이언트가 데이터를 조작할 수 있는 가능성이 있습니다. 따라서 중요한 검증은 서버사이드에서 수행해야 합니다.
  • SEO: 클라이언트사이드에서 세션을 확인하면 검색 엔진 크롤러가 페이지를 올바르게 인덱싱하지 못할 수 있습니다. 이는 특히 콘텐츠가 인증된 사용자에게만 표시되어야 하는 경우에 문제가 될 수 있습니다.
profile
개발훠훠

0개의 댓글