Next.js

Next Auth(1)

minsun309 2024. 8. 26. 08:54

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

 

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

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

Next-Auth npm

 

next-auth

Authentication for Next.js. Latest version: 4.24.7, last published: 6 months ago. Start using next-auth in your project by running `npm i next-auth`. There are 324 other projects in the npm registry using next-auth.

www.npmjs.com

 

Next-Auth Homepage

 

NextAuth.js

Authentication for Next.js

next-auth.js.org

 

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 정보 가져오기 & 로그아웃은 다음 글 이어서…