Dandy Now!
  • [Next.js] 인프런 강의 "Next.js 필수 개발 가이드 3시간 완성!" 정리(인증, 인가를 편리하게 NextAuth.js)
    2024년 02월 16일 15시 59분 37초에 업로드 된 글입니다.
    작성자: DandyNow
    728x90
    반응형

    1. NextAuth 설치

    npm install next-auth

    😉 공식 문서 : https://next-auth.js.org/getting-started/example

     

    2. OAuth 구글 계정 로그인 구현

    😉 공식 문서 :  https://next-auth.js.org/providers/google

    📌 구글 클라우드 설정

     

    1. 구글 클라우드 설정 URL로 이동(https://console.cloud.google.com/apis/credentials)
    2. OAuth consent screen : 구글 설정(프로젝트 생성 등)
    3. Credentials > CREATE CREDENTIALS > OAuth client ID : OAuth 클라이언트 아이디 생성
    4. Authorized redirect URIs에-공식 문서 Configuration의 내용인-"http://localhost:3000/api/auth/callback/google"을 붙여 넣기

    😉 위 설정 방법은 핵심만 간추려 둔 것이다. 따라서-친절한-그림이 있는 다른-블로그 등의-자료를 참조하는 것을 권한다.

     

    📌 Google provider 등록

    1. Openssl 키 생성

    다음 명령어로 Openssl을 이용해 키를 생성한 후 .env의 NEXTAUTH_SECRET에 입력한다.

    openssl rand -base64 32

    😉 Windows를 사용 중이고 Openssl이 설치되어 있지 않다면 여기를 참고하자! https://postforty.tistory.com/388

     

    2. Next.js 프로젝트에 환경변수 및 Provider를 적용

    // .env
    
    NEXTAUTH_URL="http://localhost:3000"
    NEXTAUTH_SECRET=<Openssl 키 생성 후 입력>
    GOOGLE_CLIENT_ID=<클라이언트 ID 입력>
    GOOGLE_CLIENT_SECRET=<클라이언트 보안 비밀번호 입력>
    // src/app/api/auth/[...nextauth]/route.ts
    
    import NextAuth from "next-auth/next";
    import GoogleProvider from "next-auth/providers/google";
    
    const handler = NextAuth({
      providers: [
        GoogleProvider({
          clientId: process.env.GOOGLE_CLIENT_ID!, // "!"는 undefined가 아닌 값이 확실히 있음을 명시함
          clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
        }),
      ],
    });
    
    export { handler as GET, handler as POST };
    // src/app/NavBar.tsx
    
    import Link from "next/link";
    import React from "react";
    
    const NavBar = () => {
      return (
        <div>
          {/* 생략 */}
          {/* Link를 통해 구글 로그인 화면으로 이동한다. */}
          <Link href="/api/auth/signin">Login</Link>
        </div>
      );
    };
    
    export default NavBar;

     

    📌 인증 세션

    NextAuth.js는 로그인 성공하면 인증 세션을 생성한다.

    로그인 테스트 진행 중에 [그림 1]과 같은 에러가 발생한다면 Credentials > CREATE CREDENTIALS에서 테스트 사용자에 로그인 시도에 사용한 사용자 이메일 주소를 추가해 주어야 한다.

    [그림 1] 로그인 시도 중 만난 에러

     

    로그인에 성공하면 [그림 2]와 같이 쿠키 확인이 가능하다.

    쿠키는 JWT형태이며 클라이언트와 서버 간 매 요청마다 교환되는 작은 정보 조각이고 신분증 역할을 한다.

    [그림 2] 로그인 성공하면 쿠키 확인 가능

     

    📌 JWT 디코딩

    JWT 디코딩은 실제 사용되지 않으므로 참고만 할 것!

    JWT는 기본 30일간 유효하다.

    // src/app/api/auth/token/route.tsx
    
    // JWT 디코딩
    import { getToken } from "next-auth/jwt";
    import { NextRequest, NextResponse } from "next/server";
    
    export async function GET(request: NextRequest) {
      const token = await getToken({ req: request });
      return NextResponse.json(token);
    }

     

    📌 Client Session Access

    SessionProvider를 이용해 세션 정보에 접근할 수 있다.

    SessionProvider는 클라이언트 컴포넌트에서만 작동한다. 따라서 별도의 CSR 컴포넌트를 만들어 사용한다.

    // src/app/auth/provider.tsx
    
    "use client";
    import React, { ReactNode } from "react";
    import { SessionProvider } from "next-auth/react";
    
    const AuthProvider = ({ children }: { children: ReactNode }) => {
      return <SessionProvider>{children}</SessionProvider>;
    };
    
    export default AuthProvider;
    // src/app/layout.tsx
    
    // 이 소스 코드에서는 AuthProvider 컴포넌트로 감싸는 것을 유의해서 보자!
    import type { Metadata } from "next";
    import { Inter } from "next/font/google";
    import "./globals.css";
    import NavBar from "./NavBar";
    import AuthProvider from "./auth/provider";
    
    const inter = Inter({ subsets: ["latin"] });
    
    export const metadata: Metadata = {
      title: "Create Next App",
      description: "Generated by create next app",
    };
    
    export default function RootLayout({
      children,
    }: Readonly<{
      children: React.ReactNode;
    }>) {
      return (
        <html lang="en" data-theme="dark">
          <body className={inter.className}>
            {/* AuthProvider 컴포넌트로 감싼다 */}
            <AuthProvider>
              <NavBar />
              <main>
                {children}
              </main>
            </AuthProvider>
          </body>
        </html>
      );
    }

     

    1. 클라이언트 컴포넌트에서 세션 정보 접근

    NavBar 컴포넌트를 세션 정보에 접근하여 사용하는 코드로 수정하였다.

    // src/app/NavBar.tsx
    
    "use client"; // 클라이언트 컴포넌트로 지정
    import { useSession } from "next-auth/react"; // 추가
    import Link from "next/link";
    import React from "react";
    
    const NavBar = () => {
      const { status, data: session } = useSession(); // 세션 정보
      // status 값은 authenticated(인증됨), unauthenticated(비인증됨), loading(로딩증) 중 하나이다.
      console.log(session); // 세션 정보는 객체 형태임 → {expires: "", user:{email: "", image: "", name: ""}}
      return (
        <div>
          <Link className="mr-5" href="/">
            Next.js
          </Link>
          <Link href="/users">Users</Link>
          {/* 세션 정보 사용 : 세션 인증되지 않은 경우에는 Login, 인증된 경우에는 유저 이름 렌더링 */}
          {status === "authenticated" && <div>{session.user!.name}</div>}
          {status === "unauthenticated" && (
            <Link href="/api/auth/signin">Login</Link>
          )}
        </div>
      );
    };
    
    export default NavBar;

     

    2. 서버 컴포넌트에서 세션 정보 접근

    // src/app/api/auth/[...nextauth]/route.ts
    
    import NextAuth from "next-auth/next";
    import GoogleProvider from "next-auth/providers/google";
    
    // 서버, 클라이언트 컴포넌트 모두에서 사용할 수 있게 코드 수정
    export const authOptions = {
      providers: [
        GoogleProvider({
          clientId: process.env.GOOGLE_CLIENT_ID!,
          clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
        }),
      ],
    };
    
    const handler = NextAuth(authOptions);
    
    export { handler as GET, handler as POST };
    // src/app/page.tsx
    
    import Link from "next/link";
    import { getServerSession } from "next-auth"; // 추가
    import { authOptions } from "./api/auth/[...nextauth]/route"; // 추가
    
    export default async function Home() {
      const session = await getServerSession(authOption); // 추가
      return (
        <main>
          {/* 세션이 존재하는 경우 사용자 이름 렌더링 */}
          <h1>안녕하세요 - {session && <span>{session.user!.name}</span>}</h1>
          <Link href="/users">Users</Link>
        </main>
      );
    }

     

    📌 로그아웃 기능

    // src/app/api/auth/[...nextauth]/route.ts
    
    "use client";
    import { useSession } from "next-auth/react";
    import Link from "next/link";
    import React from "react";
    
    const NavBar = () => {
      // 생략
      return (
        <div>
          {/* 생략 */}
          {status === "authenticated" && (
            <div>
              {session.user!.name}
              {/* 로그 아웃 기능 추가 */}
              <Link href="/api/auth/signout" className="ml-3">
                SignOut
              </Link>
            </div>
          )}
          {/* 생략 */}
        </div>
      );
    };
    
    export default NavBar;

     

    📌 로그인 사용자만 접근 가능한 컴포넌트

    미들웨어(Middleware)

    • 클라이언트와 서버 사이에서 동작하는 소프트웨어(또는 코드 집합)
    • 클라이언트의 요청과 서버의 응답 사이에서 중간 처리 역할
    • 데이터 변환, 메시지 관리, 인증, 로깅 등 다양한 기능 수행
    // middleware.ts
    
    // package.json이 있는 최상단 경로에 작성해야 한다!
    // https://nextjs.org/docs/app/building-your-application/routing/middleware#example
    import { NextRequest, NextResponse } from "next/server";
    export { default } from "next-auth/middleware";
    
    // matcher에 해당하는 경우에만 미들웨어 작동
    /*
    *: 0개 이상
    +: 1개 이상
    ?: 0 또는 1개
    */
    export const config = {
      matcher: ["/users/:id*"], // Protected Routes라고 부른다.
    };

     

    📌 데이터베이스 어댑터

    OAuth 로그인하더라도 사용자 로그인 정보를 DB에 저장할 수 있다.

    😉 한번 저장된 사용자 정보는 중복해서 저장되지 않는 것을 확인했다.

     

    1. 설치

    npm install @prisma/client @auth/prisma-adapter
    npm install prisma --save-dev // prisma가 설치 안된 경우 설치할 것

    😉 공식 문서 : https://authjs.dev/reference/adapter/prisma?_gl=1*174wz90*_gcl_au*NTQzMjk4MzM4LjE3MDgwNDQ1MTQuMzk0ODUwODEzLjE3MDgwNTA5NjMuMTcwODA1MDk2OA..#installation

     

    2. User 테이블을 모델에 추가

    1) 충돌 방지하기 위해 기존 모델 삭제 후 마이그레이션

    npx prisma migrate dev // 마이그레이션 명령어

    2) 공식 문서의 모델 copy/paste

    😉 공식 문서 :  https://authjs.dev/reference/adapter/prisma?_gl=1*174wz90*_gcl_au*NTQzMjk4MzM4LjE3MDgwNDQ1MTQuMzk0ODUwODEzLjE3MDgwNTA5NjMuMTcwODA1MDk2OA..#create-the-prisma-schema-from-scratch

    3) 프리즈마 스키마의 모델에서 User(테이블)에 로그인 사용자 정보가 저장된다. 공식 문서의 경우 id가 문자열로 되어 있다.

    3. Provider에 설치한 어댑터 적용

    // src/app/api/auth/[...nextauth]/route.ts
    
    import NextAuth from "next-auth/next";
    import GoogleProvider from "next-auth/providers/google";
    import { PrismaAdapter } from "@auth/prisma-adapter"; // 추가
    import { PrismaClient } from "@prisma/client"; // 추가
    
    const prisma = new PrismaClient(); // 추가
    
    export const authOptions = {
      adapter: PrismaAdapter(prisma), // 추가
      providers: [
        GoogleProvider({
          clientId: process.env.GOOGLE_CLIENT_ID!,
          clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
        }),
      ],
      // 강의에서 추가한 코드인데 https://next-auth.js.org/errors#jwt_session_error Invalid Compact JWE 에러 발생
      // 하지만 동작은 하고 DB에 사용자 정보도 잘 저장됨
      // 이 코드 없이 진행해도 DB에 사용자 정보가 잘 저장됨
      // session: {
      //   strategy: "jwt",
      // },
    };
    // @ts-ignore
    const handler = NextAuth(authOptions);
    
    export { handler as GET, handler as POST };

    🤔 authOptions의 adapter의 타입을 무시하였다. 추후 타입을 확인하게 되면 수정하겠다.


     🤔 실습 진행 중 이슈 : 깃허브 커밋시 .env를 gitignore에 추가하였음에도 불구하고 무시되지 않았다. 한번 커밋된 경우에는 gitignore에 추가해도 무시되지 않는다고 한다. 프로젝트를 로컬에 백업해 두고, 깃허브에서 해당 프로젝트를 delete 하고 새롭게 프로젝트를 생성한 후 백업해 둔 프로젝트를 복붙 한 뒤 최초 커밋시 .env를 gitignore에 추가한 후 커밋, 푸시하여 해결했다.

    728x90
    반응형
    댓글