image

Next Auth(1)

태그
Next.js
상세설명Next Auth 사용법
작성일자2024.01.02

Next.js 기반으로 관리자 페이지, 관제 시스템 등 권한이 필요한 프로젝트 진행 시 Next-Auth 라는 npm 을 자주 사용하였다.

여러 프로젝트 진행하면서 고려해야 했던 포인트

  • 소수 계정일 경우
  • session 기간
  • 권한 여부에 대한 스타일, 페이지 차이
  • 비 로그인 시 홈 접근 방지 / 로그인 상태 일시 로그인 페이지 접근 방지
  • user 정보 가져오기 & 로그아웃
  • Next-Auth npm

    Next-Auth Homepage

    Next-Auth 설치

    npm i next-auth
    // or
    yarn add next-auth

    Next-Auth 사용법

    pages/api /auth 폴더를 만들고 […nextauth].js 파일을 만들면 된다. ( 타입 스크립트이면 ts)

    providers에 이메일을 사용하여 로그인 할 수 있게 했는데 공식 홈에 Google, GitHub 등 원하는 providers를 넣으면 계정으로 로그인 할 수 있다.

    providers 예시

    import NextAuth from "next-auth"
    import GitHubProvider from "next-auth/providers/github";
    import NaverProvider from "next-auth/providers/naver"
    import KakaoProvider from "next-auth/providers/kakao"
    
    export const authOptions = {
      ...
      providers: [
        GitHubProvider({
          clientId: process.env.GITHUB_ID,
          clientSecret: process.env.GITHUB_SECRET
        }),
        NaverProvider({
          clientId: process.env.NAVER_CLIENT_ID,
          clientSecret: process.env.NAVER_SECRET,
        }),
    	  KakaoProvider({
          clientId: process.env.KAKAO_CLIENT_ID,
          clientSecret: process.env.KAKAO_SECRET,
        }),
      ],
    }
    export default NextAuth(authOptions)

    이메일 providers & 소수 계정일 경우

    아래 코드는 이메일 providers로 소수의 계정이 필요 한 경우 find() 함수를 활용해 일치하면 로그인 가능하게 하였다.

    // pages/api/auth/[...nextauth].js
    
    import NextAuth from "next-auth";
    import CredentialsProvider from "next-auth/providers/credentials";
    
    export default NextAuth({
      providers: [
        CredentialsProvider({
          name: "유저 이메일,페스워드 방식",
          credentials: {
            email: {
              label: "유저 이메일",
              type: "email",
              placeholder: "user@email.com",
            },
            password: { label: "패스워드", type: "password" },
          },
          async authorize(credentials, req) {
            const accountArray = [
              {
                id: "test1",
                password: "test1234!",
              },
              {
                id: "test2",
                password: "test1234!",
              }
            ];
    
            const matchedAccount = accountArray.find(
              (account) =>
                account.id === credentials.email &&
                account.password === credentials.password
            );
    
            if (matchedAccount) {
              const user = [
                {
                  id: 1,
                  name: "테스터1",
                  email: "test1",
                },
                {
                  id: 2,
                  name: "테스터2",
                  email: "test2",
                }
              ];
    
              const matchedUser = user.find((x) => x.email === matchedAccount.id);
              if (matchedUser) {
                return Promise.resolve(matchedUser);
              }
            }
            return Promise.resolve(null);
          },
        }),
      ],
      secret: process.env.NEXTAUTH_SECRET,
    });

    session 기간

    secret 다음에 maxAge를 설정하면 된다. 기본 값은 30일 이다.

    session: {
        maxAge: 24 * 60 * 60, // 24시간
    },

    NEXTAUTH_SECRET / NEXTAUTH_URL

    ⭐ 두 개 다 필수로 있어야 함.

    NEXTAUTH_SECRET은 무작위 문자열을 넣어주면 된다.

    OpenSSL 설치되어 있는 경우

    $ openssl rand -base64 32

    Node.js를 사용하는 경우

    node -e "console.log(require('crypto').randomBytes(32).toString('base64'))"

    명령 프롬프트(CMD)를 열고 명령어를 입력하면 무작위 문자열이 나온다.

    Get-Random을 사용하여 PowerShell에서 무작위 바이트를 생성

    [Convert]::ToBase64String((Get-Random -Count 32 -InputObject ([byte[]]@(0..255))))

    NEXTAUTH_URL은 development, production 분리해서 url를 다르게 설정해줘야 한다.

    .env.development

    NEXTAUTH_SECRET=...
    NEXTAUTH_URL=http://localhost:3000 

    .env.production

    NEXTAUTH_SECRET=...
    NEXTAUTH_URL=https://.... (배포 주소)

    next.js에 nextauth 연결

    client에서 session 정보를 불러올 수 있는 useSession 훅을 사용하기 위해 앱의 가장 최 상단(_app.tsx)에 SessionProvider를 넣어준다.

    기본형

    // pages/_app.jsx
    
    import { SessionProvider } from "next-auth/react"
    
    export default function App({
      Component,
      pageProps: { session, ...pageProps },
    }) {
      return (
        <SessionProvider session={session}>
          <Component {...pageProps} />
        </SessionProvider>
      )
    }

    비 로그인 시 홈 접근 방지

    권한 여부에 대한 스타일, 페이지 차이

    auth 여부에 따라 보여줄 수 있는 페이지 와 스타일 차이를 Component.auth 를 통해 처리 했다.

    그래서 각 페이지마다 login.tsx 하단에 Login.auth = false; index.tsx 하단에 Home.auth = true; 와 같이 넣어 해당 조건에 맞는 스타일과 접근 권한 여부를 부여한다.

    Auth

    Component.auth true인 페이지를 불러와서 조건을 통과하면 status에 따른 스타일, 행동을 줄 수 있다. useSession 훅에서 required: true를 설정하는 것은 세션이 반드시 필요한지 여부를 나타냅니다. 이 옵션을 사용하면 세션이 없는 경우에 대한 처리를 지정할 수 있습니다.

    하위 코드에서는 useSession 훅을 사용하여 세션 상태를 확인하고, onUnauthenticated 이면 로그인 페이지로 이동 시키고 세션이 로딩 상태이면 컴포넌트를 렌더링한다.

    function Auth({ children }: PropsWithChildren) {
      const router = useRouter();
      const { status } = useSession({
        required: true,
        onUnauthenticated() {
          router.push(`/login`);
        },
      });
    
      if (status === "loading") {
        return (
          <>
            <Global styles={loadingReset} />
            <Loading />
          </>
        );
      }
    
      return <>{children}</>;
    }

    Login.auth = false 예시

    export default function Login() {
     ...
     return (
        <>
    			...
        </>
      );
    }
    
    Login.auth = false;

    타입 스크립트 사용 시

    auth 에 대한 타입을 추가해줘야 한다.

    type CustomAppProps = AppProps & {
      Component: NextComponentType & { auth?: boolean }; // add auth type
    };
    
    export default function App({
      Component,
      pageProps: { session, ...pageProps },
      ...appProps
    }: CustomAppProps) {
    	...
    }

    특정 페이지 레이아웃 스타일 분리

    appProps 를 통해 pathname이 /login 이면 LayoutComponent 가 스타일이 없는 React.Fragment가 되게 했다.

    export default function App({
      Component,
      pageProps: { session, ...pageProps },
      ...appProps
    }: CustomAppProps) {
    
    	const withoutLayout = appProps.router.pathname === "/login";
      const LayoutComponent = withoutLayout ? React.Fragment : Layout;
    
      return (
          <SessionProvider session={session}>
            {Component.auth ? (
              <Auth>
                <LayoutComponent>
                  <Global styles={[reset, SCoreDreamFont]} />
                  <Component {...pageProps} />
                </LayoutComponent>
              </Auth>
            ) : (
              <LayoutComponent>
                <Global styles={[loginReset, SCoreDreamFont]} />
                <Component {...pageProps} />
              </LayoutComponent>
            )}
          </SessionProvider>
      );
    }

    전체 코드

    // _app.tsx
    
    import React, { useEffect } from "react";
    import { SessionProvider, useSession } from "next-auth/react";
    import Layout from "@/components/Layout";
    import type { AppProps } from "next/app";
    import { useRouter } from "next/router";
    import { PropsWithChildren } from "react";
    import { Global } from "@emotion/react";
    import { loadingReset, loginReset, reset } from "@/styles/Global";
    import Loading from "@/components/ScreenStructure/Loading";
    import type { NextComponentType } from "next";
    import {
      Hydrate,
      QueryClient,
      QueryClientProvider,
    } from "@tanstack/react-query";
    import { SCoreDreamFont } from "@/styles/Fonts";
    
    //Add custom appProp type then use union to add it
    type CustomAppProps = AppProps & {
      Component: NextComponentType & { auth?: boolean }; // add auth type
    };
    
    export default function App({
      Component,
      pageProps: { session, ...pageProps },
      ...appProps
    }: CustomAppProps) {
    
      const withoutLayout = appProps.router.pathname === "/login";
      const LayoutComponent = withoutLayout ? React.Fragment : Layout;
    
      const [queryClient] = React.useState(() => new QueryClient());
    
      return (
        <QueryClientProvider client={queryClient}>
          <SessionProvider session={session}>
            {Component.auth ? (
    					// 권한이 필요한 경우
              <Auth>
                <LayoutComponent>
                  <Hydrate state={pageProps.dehydratedState}>
                    <Global styles={[reset, SCoreDreamFont]} />
                    <Component {...pageProps} />
                  </Hydrate>
                </LayoutComponent>
              </Auth>
            ) : (
    					// 권한이 필요하지 않은 경우
              <LayoutComponent>
                <Global styles={[loginReset, SCoreDreamFont]} />
                <Component {...pageProps} />
              </LayoutComponent>
            )}
          </SessionProvider>
        </QueryClientProvider>
      );
    }
    
    function Auth({ children }: PropsWithChildren) {
      const router = useRouter();
      const { status } = useSession({
        required: true,
        onUnauthenticated() {
          router.push(`/login`);
        },
      });
    
      if (status === "loading") {
        return (
          <>
            <Global styles={loadingReset} />
            <Loading />
          </>
        );
      }
    
      return <>{children}</>;
    }

    로그인 되어 있을 때 로그인 페이지 접근 방지

    next.config.js

    로그인 되어 있는 상태에서 url에 "/login" 입력 시 로그인 페이지에 진입을 방지하기 위해 "/login" 입력하면 “/” 으로 가는 redirects를 추가한다.

    // next.config.js
    
    /** @type {import('next').NextConfig} */
    const nextConfig = {
      reactStrictMode: true,
      swcMinify: true,
      async redirects() {
        return [
          {
            source: "/login",
            destination: "/",
            permanent: true,
          },
          {
            source: "/error",
            destination: "/",
            permanent: true,
          },
        ];
      },
    };
    
    module.exports = nextConfig;

    user 정보 가져오기 & 로그아웃은 다음 글 이어서…