๐Ÿ›ก๏ธ Nextโ€Auth๋กœ ๋กœ๊ทธ์ธ ๋กœ์ง ๊ตฌํ˜„ํ•˜๊ธฐ (with MySQL, Prisma) - JeongwooHam/How-Can-I-Use-Market GitHub Wiki

NextAuth๋ž€?

  • Next.js ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์„ ์œ„ํ•œ ์˜คํ”ˆ ์†Œ์Šค ์ธ์ฆ ์†”๋ฃจ์…˜
  • Next.js์™€ Serverless ํ™˜๊ฒฝ์„ ์ง€์›ํ•˜๋„๋ก ์„ค๊ณ„๋˜์—ˆ๋‹ค.
  • ๊ฐ„ํŽธ ๋กœ๊ทธ์ธ์„ ์‰ฝ๊ฒŒ ๊ตฌํ˜„ํ•  ์ˆ˜ ์žˆ์œผ๋ฉฐ JWT์™€ DB ์„ธ์…˜์„ ๋ชจ๋‘ ์ง€์›ํ•œ๋‹ค.

Next.js์˜ App Router์—์„œ NextAuth ์‹œ์ž‘ํ•˜๊ธฐ

NextAuth.js ์„ค์น˜ํ•˜๊ธฐ

npm i next-auth

Next Auth ๊ธฐ๋ณธ ํŒŒ์ผ ์ž‘์„ฑํ•˜๊ธฐ

  • app/api/auth/[...nextauth] ํด๋”์— ์ž‘์„ฑํ•œ๋‹ค.
  • Next.js์˜ Server Side API๋ฅผ ์‚ฌ์šฉํ•˜๊ธฐ ์œ„ํ•ด route.js ํŒŒ์ผ ์•ˆ์— ์ž‘์„ฑํ•ด์ฃผ์–ด์•ผ ํ•œ๋‹ค.
import NextAuth from 'next-auth/next'

const handler = NextAuth({})

export { handler as GET, handler as POST }

Credentials ์ ์šฉํ•˜๊ธฐ

  • CredentialsProvider
    • ์‚ฌ์šฉ์ž๋ช…, ๋น„๋ฐ€๋ฒˆํ˜ธ์™€ ๊ฐ™์€ ์ธ์ฆ์„ ์ œ๊ณตํ•˜๋Š” NextAuth์˜ Provider
// ์ธ์ฆ ์ œ๊ณต์ž ์„ค์ •: CredentialsProvider ์™ธ์—๋„ google, github ๋“ฑ ๊ฐ„ํŽธ ๋กœ๊ทธ์ธ Provider๋„ ์„ค์ • ๊ฐ€๋Šฅ
providers: [
    CredentialsProvider({
      // ๋กœ๊ทธ์ธ ํผ์— ๋ณด์—ฌ์ค„ ์ด๋ฆ„
      name: "Credentials",
      // ๋กœ๊ทธ์ธ ํŽ˜์ด์ง€์—์„œ ํผ์„ ์ƒ์„ฑํ•˜๊ธฐ ์œ„ํ•ด ์‚ฌ์šฉํ•œ๋‹ค.
      // credentials ๊ฐ์ฒด์— ์–ด๋–ค ํ•„๋“œ๊ฐ€ ์ œ์ถœ๋˜์–ด์•ผ ํ•  ์ง€ ์ง€์ •ํ•  ์ˆ˜ ์žˆ๋‹ค.
      // <input> ํƒœ๊ทธ์— ์ „๋‹ฌํ•  HTML ์†์„ฑ์„ ์ถ”๊ฐ€ํ•  ์ˆ˜ ์žˆ๋‹ค.
      credentials: {
        email: {
          label: "email",
          type: "text",
          placeholder: "์ด๋ฉ”์ผ ์ฃผ์†Œ๋ฅผ ์ž…๋ ฅํ•ด์ฃผ์„ธ์š”.",
        },
        password: {
          label: "Password",
          type: "password",
          placeholder: "๋น„๋ฐ€๋ฒˆํ˜ธ๋ฅผ ์ž…๋ ฅํ•ด์ฃผ์„ธ์š”.",
        },
      },
      // ์‚ฌ์šฉ์ž๋ฅผ ์ธ์ฆํ•˜๊ธฐ ์œ„ํ•œ ์ฝœ๋ฐฑ ํ•จ์ˆ˜
      // ์‚ฌ์šฉ์ž๊ฐ€ ์ œ์ถœํ•œ ์ž๊ฒฉ ์ฆ๋ช…์„ ๋ฐ›์•„ ์‚ฌ์šฉ์ž๋ฅผ ์ฐพ๊ณ  ์ธ์ฆ์„ ์ˆ˜ํ–‰ํ•œ๋‹ค.
      // DB๋‚˜ ์™ธ๋ถ€ ์„œ๋น„์Šค๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ์‚ฌ์šฉ์ž๋ฅผ ์ฐพ๋Š” ๋กœ์ง์ด ๋“ค์–ด๊ฐ„๋‹ค.
      async authorize(credentials, req) {
        // ์ž„์˜์˜ ์‚ฌ์šฉ์ž ์ธ์ฆ ๋กœ์ง ์ž‘์„ฑ
        // ...
        if (user) {
          return user;
        } else {
          return null;
        }
      },
    }),
  ],

ํ™˜๊ฒฝ๋ณ€์ˆ˜ ์„ค์ •ํ•˜๊ธฐ

NEXTAUTH_SECRET= ์ž„์˜๋กœ ์•„๋ฌด string์„ ๋„ฃ์–ด secret key๋ฅผ ์„ค์ •ํ•œ๋‹ค.
NEXTAUTH_URL=http://localhost:3000 // ๊ฐœ๋ฐœ ์„œ๋ฒ„ ํฌํŠธ ์ง€์ •

Prisma์™€ MySQL ์‹œ์ž‘ํ•˜๊ธฐ

prisma์™€ adapter ์„ค์น˜ํ•˜๊ธฐ

npm install @prisma/client @auth/prisma-adapter
npm install prisma --save-dev

prisma adapter ์„ค์ •ํ•˜๊ธฐ

  • ์•ž์„œ Next Auth์— ๋Œ€ํ•œ ๊ธฐ๋ณธ ์„ค์ •์„ ํ•ด์ฃผ์—ˆ๋˜ app/api/auth/[...nextauth]์˜ route.ts์— ์ž‘์„ฑํ•œ๋‹ค.
import { PrismaAdapter } from "@next-auth/prisma-adapter";

const handler = NextAuth({
  // ์‚ฌ์šฉ์ž ์ธ์ฆ handler, Next.js API ๋ผ์šฐํŠธ์—์„œ ์‚ฌ์šฉ๋œ๋‹ค.
  adapter: PrismaAdapter(prisma),
  ...
})

prisma์— NextAuth ๊ธฐ๋ณธ Schema ์ž‘์„ฑํ•˜๊ธฐ

  • Next Auth์—์„œ๋Š” prisma ์‚ฌ์šฉ ์‹œ ๊ธฐ๋ณธ์œผ๋กœ ์„ค์ • ๊ฐ€๋Šฅํ•œ schema๋ฅผ ์ œ๊ณตํ•œ๋‹ค. (๊ณต์‹ ๋ฌธ์„œ)
  • database๋กœ MySQL์„ ์‚ฌ์šฉํ•˜์˜€์œผ๋ฏ€๋กœ ์•„๋ž˜ ๋ถ€๋ถ„์„ ์ˆ˜์ •ํ•ด์ค€๋‹ค.
datasource db {
  provider = "mysql"
  url      = env("DATABASE_URL")
}

env ํŒŒ์ผ์— DATABASE_URL ์„ค์ •ํ•˜๊ธฐ

DATABASE_URL="mysql://์•„์ด๋””:๋น„๋ฐ€๋ฒˆํ˜ธ@localhost:ํฌํŠธ๋ฒˆํ˜ธ/database์ด๋ฆ„"

์‚ฌ์šฉ์ž ๋ชจ๋ธ ์ž‘์„ฑํ•˜๊ธฐ

  • NextAuth์—์„œ ์ œ๊ณตํ•ด์ค€ ๊ธฐ๋ณธ ์‚ฌ์šฉ์ž ๋ชจ๋ธ์—์„œ ์ถ”ํ›„ ์‚ฌ์šฉ๋  Post ๋ฐ ๊ถŒํ•œ ์ •๋ณด๋ฅผ ์ถ”๊ฐ€ํ–ˆ๋‹ค.
enum UserType {
  User
  Admin
}

model User {
  id            String    @id @default(cuid())
  hashedPassword String?
  name          String? @unique
  email         String?   @unique
  emailVerified DateTime?
  image         String?
  createdAt     DateTime  @default(now())
  updatedAt     DateTime  @updatedAt
  accounts      Account[]
  sessions      Session[]
  posts         Post[]
  userType UserType @default(User)
}

์‹ค์ œ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์™€ ์—ฐ๊ฒฐํ•˜๊ธฐ

  • Prisma์—์„œ ์ˆ˜์ •ํ•œ ๋‚ด์šฉ ๋ฐ˜์˜ํ•˜๊ธฐ
npx prisma db push
  • ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์—์„œ ์ˆ˜์ •๋œ ๋‚ด์šฉ ๋ฐ›์•„์˜ค๊ธฐ
npx prisma db pull
  • ์›น์ƒ์—์„œ ๋ฐ์ดํ„ฐ ์ˆ˜๋™์œผ๋กœ ์กฐ์ž‘ํ•˜๊ธฐ
npx prisma studio

Prisma Client ์„ค์ •ํ•˜๊ธฐ

  • Next.js์—์„œ Prisma Client๋ฅผ ํ˜ธ์ถœํ•˜๋Š” ๋ชจ๋“ˆ์„ ์ž‘์„ฑํ•ด์ฃผ์–ด์•ผ ํ•œ๋‹ค.
  • app/libs ํด๋” ํ•˜์œ„์˜ prisma.ts ํŒŒ์ผ์— ์ž‘์„ฑํ•ด์ฃผ์—ˆ๋‹ค.
import { PrismaClient } from "@prisma/client";

const globalForPrisma = global as unknown as { prisma: PrismaClient };

// ๋ฉ”๋ชจ๋ฆฌ์— ํ•˜๋‚˜์˜ Prisma Client๋งŒ ๋ถˆ๋Ÿฌ์˜ค๋„๋ก ์„ค์ •ํ•ด์ค€๋‹ค.
export const prisma = globalForPrisma.prisma || new PrismaClient();

if (process.env.NODE_ENV !== "production") globalForPrisma.prisma = prisma;

export default prisma;

Next.js์˜ API Routes๋กœ ํšŒ์›๊ฐ€์ž…, ๋กœ๊ทธ์ธ ๋กœ์ง ๊ตฌํ˜„ํ•˜๊ธฐ

  • Next.js 13 ๋ฒ„์ „์—์„œ API ๋กœ์ง์€ route.ts์— ์œ„์น˜ํ•ด์•ผ ํ•œ๋‹ค.
  • ์˜ˆ๋ฅผ ๋“ค์–ด, app/api/user ํด๋” ํ•˜์œ„์˜ route.ts ํŒŒ์ผ์— API ๋กœ์ง์„ ์ž‘์„ฑํ–ˆ๋‹ค๋ฉด ํ•ด๋‹น API routes๋Š” https://localhost:3000/api/user๊ฐ€ ๋œ๋‹ค.

ํšŒ์›๊ฐ€์ž… ๋กœ์ง ๊ตฌํ˜„ํ•˜๊ธฐ

import prisma from "@/app/libs/prisma";
import * as bcrypt from "bcrypt";

interface RequestBody {
  name: string;
  email: string;
  password: string;
}

// ํšŒ์›๊ฐ€์ž…
export async function POST(request: Request) {
  const body: RequestBody = await request.json();

  // body๋กœ ๋ฐ›์•„์˜จ ์ •๋ณด๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ prisma๋กœ ๋ฐ์ดํ„ฐ๋ฅผ ์ƒ์„ฑํ•œ๋‹ค.
  const user = await prisma.user.create({
    data: {
      name: body.name,
      email: body.email,
      hashedPassword: await bcrypt.hash(body.password, 10),
    },
  });

  // ๋น„๋ฐ€๋ฒˆํ˜ธ๋ฅผ ์ œ์™ธํ•œ ๋ถ€๋ถ„์„ Response๋กœ ๋ณด๋‚ด์ค€๋‹ค.
  const { hashedPassword, ...result } = user;
  return new Response(JSON.stringify(result));
}

Bcrypt

npm i -D bcrypt
npm i -D @types/bcrypt
  • ์‚ฌ์šฉ์ž์˜ ๋น„๋ฐ€๋ฒˆํ˜ธ๊ฐ€ ๊ทธ๋Œ€๋กœ ์ €์žฅ๋˜์ง€ ์•Š๋„๋ก hash๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ์•”ํ˜ธํ™”ํ•ด์ค€๋‹ค.

Pasted image 20240209173710

๋กœ๊ทธ์ธ/๋กœ๊ทธ์•„์›ƒ ๋กœ์ง ๊ตฌํ˜„ํ•˜๊ธฐ

  • DB์˜ email๊ณผ password๋ฅผ HTML Form์— ์ž…๋ ฅ๋œ ๊ฐ’๊ณผ ๋น„๊ตํ•˜์—ฌ ์ผ์น˜ํ•˜๋ฉด ์‚ฌ์šฉ์ž ์ •๋ณด๋ฅผ, ํ‹€๋ฆฌ๋ฉด null์„ ๋ฐ˜ํ™˜ํ•œ๋‹ค.
  • bcrypt.compare๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ์•”ํ˜ธํ™”๋œ ๋น„๋ฐ€๋ฒˆํ˜ธ๋ฅผ ๋น„๊ตํ•œ๋‹ค.
import { signJwtAccessToken } from "@/app/libs/jwt";
import prisma from "@/app/libs/prisma";
import * as bcrypt from "bcrypt";

interface RequestBody {
  email: string;
  password: string;
}

export async function POST(req: Request) {
  const body: RequestBody = await req.json();

  const user = await prisma.user.findFirst({
    where: {
      email: body.email,
    },
  });

  if (
    user &&
    user.hashedPassword &&
    (await bcrypt.compare(body.password, user.hashedPassword))
  ) {
    const { hashedPassword, ...userWithoutPassword } = user;
    return new Response(JSON.stringify(userWithoutPassword ));
  } else return new Response(JSON.stringify(null));
}
  • ๋กœ๊ทธ์ธ ์„ฑ๊ณต Pasted image 20240210220632

  • ๋กœ๊ทธ์ธ ์‹คํŒจ Pasted image 20240210220609

Next Auth์— ๋กœ๊ทธ์ธ ๋กœ์ง ์—ฐ๊ฒฐํ•˜๊ธฐ

  • authorize ํ•จ์ˆ˜์—์„œ ์ƒ์„ฑํ•œ ๋กœ๊ทธ์ธ API์— ๋Œ€ํ•œ fetch๋ฅผ ์‹คํ–‰ํ•œ๋‹ค.
  • ์‹คํ–‰ ๊ฒฐ๊ณผ๋Š” user ๊ฐ์ฒด๋กœ ์ €์žฅ๋œ ๋’ค ๋ฐ˜ํ™˜๋œ๋‹ค. Next Auth์—์„œ๋Š” null์ด ์•„๋‹Œ ๊ฐ’์ด ๋ฐ˜ํ™˜๋˜๋ฉด ๋กœ๊ทธ์ธ์ด ๋˜์—ˆ๋‹ค๊ณ  ํŒ๋‹จํ•œ๋‹ค.
async authorize(credentials, req) {
        const res = await fetch(`${process.env.NEXTAUTH_URL}/api/signin`, {
          method: "POST",
          headers: {
            "Content-Type": "application/json",
          },
          body: JSON.stringify({
            email: credentials?.email,
            password: credentials?.password,
          }),
        });

        const user = await res.json();

        if (user) {
          return user;
        } else {
          return null;
        }
      },

๋กœ๊ทธ์ธ/๋กœ๊ทธ์•„์›ƒ ๋ฒ„ํŠผ ๊ตฌํ˜„ํ•˜๊ธฐ

  • Next Auth์—์„œ๋Š” ๊ธฐ๋ณธ์ ์œผ๋กœ signIn ๋ฒ„ํŠผ๊ณผ signOut ํ•จ์ˆ˜๋ฅผ ์ œ๊ณตํ•ด์ค€๋‹ค.
  • signIn ํ•จ์ˆ˜ ํ˜ธ์ถœ ์‹œ ์•ž์„œ ๊ตฌํ˜„ํ•œ authorize ํ•จ์ˆ˜๊ฐ€ ์ž‘๋™ํ•ด ๋กœ๊ทธ์ธ ๋กœ์ง์„ ์‹คํ–‰ํ•œ๋‹ค.
<button onClick={() => signIn()}>{ButtonText.login}</button>
<button onClick={() => signOut()}>{ButtonText.logout}</button>
  • ๋กœ๊ทธ์ธ ์„ฑ๊ณต ์‹œ Next Auth๋Š” ์„ฑ๊ณต ์—ฌ๋ถ€๋ฅผ session ๋ฐฉ์‹์œผ๋กœ ์ œ๊ณตํ•ด์ค€๋‹ค.
    • session์€ ๋ธŒ๋ผ์šฐ์ €์˜ ์ฟ ํ‚ค์— ์ €์žฅ๋œ๋‹ค. โžก๏ธ Next Auth ์ž์ฒด๊ฐ€ CSR!

SessionProvider๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ Next.js ์•ฑ ์ „์ฒด์— NextAuth Session ์ ์šฉํ•˜๊ธฐ

  • NextAuth์—์„œ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋Š” HOC๋ฅผ SessionProvider๋ผ๊ณ  ํ•œ๋‹ค.
  • components/providers ํด๋”์— SessionProviders.tsx ํŒŒ์ผ์„ ์ƒ์„ฑํ•˜์—ฌ ๊ตฌํ˜„ํ•ด์ฃผ์—ˆ๋‹ค.
    • SessionProvider ์ ์šฉ์„ ์œ„ํ•ด ์ „์—ญ layout์„ ํด๋ผ์ด์–ธํŠธ ์ปดํฌ๋„ŒํŠธ๋กœ ๋งŒ๋“ค์–ด์•ผ ํ•˜๋Š” ์ƒํ™ฉ์„ ๋ง‰๊ธฐ ์œ„ํ•ด ๋ณ„๋„์˜ ํด๋ผ์ด์–ธํŠธ ์ปดํฌ๋„ŒํŠธ๋ฅผ ์ƒ์„ฑํ•œ ๋’ค import ํ•ด์ฃผ์—ˆ๋‹ค.
"use client";

import { SessionProvider } from "next-auth/react";
import React, { ReactNode } from "react";

interface Props {
  children: ReactNode;
}
function SessionProviders({ children }: Props) {
  return <SessionProvider>{children}</SessionProvider>;
}

export default SessionProviders;

์ „์—ญ layout.tsx์— ์ ์šฉํ•˜๊ธฐ

export default function RootLayout({
  children,
}: Readonly<{
  children: React.ReactNode;
}>) {
  return (
    <html lang='en'>
      <body className={inter.className}>
        <SessionProviders>
          <NavBar />
          {children}
        </SessionProviders>
      </body>
    </html>
  );
}

๋กœ๊ทธ์ธ ์—ฌ๋ถ€์— ๋”ฐ๋ผ ๋ฒ„ํŠผ ํ…์ŠคํŠธ ๋‹ค๋ฅด๊ฒŒ ๋ณด์ด๊ฒŒ ํ•˜๊ธฐ

  • NextAuth์—์„œ ํด๋ผ์ด์–ธํŠธ์—์„œ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋„๋ก ์ œ๊ณตํ•ด์ฃผ๋Š” ํ›…ํ•จ์ˆ˜์ธ useSession์„ ์‚ฌ์šฉํ•˜์—ฌ session์„ ๊ฒ€์‚ฌํ•˜์˜€๋‹ค.
  • ๋ฐ˜ํ™˜๊ฐ’์ธ data๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ session์ด ์žˆ์„ ๋•Œ๋Š” ๋กœ๊ทธ์•„์›ƒ ๋ฒ„ํŠผ, ์—†์„ ๋•Œ๋Š” ๋กœ๊ทธ์ธ ๋ฒ„ํŠผ์ด ๋ณด์ด๋„๋ก ํ•˜์˜€๋‹ค.
const { data: session, status } = useSession();

  if (!session?.user)
    return (
      <ButtonWrapper>
        <button onClick={() => signIn()}>{ButtonText.login}</button>
      </ButtonWrapper>
    );

  return (
    <ButtonWrapper>
      <button onClick={() => signOut()}>{ButtonText.logout}</button>
    </ButtonWrapper>
  );

ํ† ํฐ์„ ์ด์šฉํ•ด ์„ธ์…˜ ๋ณดํ˜ธํ•˜๊ธฐ

  • NextAuth์—์„œ๋Š” ์ธ์ฆ๋˜์ง€ ์•Š์€ ์‚ฌ์šฉ์ž์˜ ์ ‘๊ทผ ์ œํ•œ์„ ์œ„ํ•ด JWT ํ† ํฐ์„ ๋‹ค๋ฃจ๋Š” ๊ธฐ๋Šฅ์œผ ์ œ๊ณตํ•œ๋‹ค.
  • ๋กœ๊ทธ์ธ ์‹œ ๋ฐ˜ํ™˜๋˜๋Š” user ๊ฐ์ฒด์— AccessToken์ด ํ•จ๊ป˜ ์‹ค๋ฆฌ๋„๋ก ํ•œ ๋’ค ํ•ด๋‹น ํ† ํฐ์„ session์— ์ €์žฅํ•˜์—ฌ ์›ํ•˜๋Š” ๊ธฐ๋Šฅ์„ ๊ตฌํ˜„ํ•  ์ˆ˜ ์žˆ๋‹ค.

jsonwebtoken ์„ค์น˜ํ•˜๊ธฐ

  • JWT ํ‘œ์ค€ ๋ช…์„ธ์„œ๋ฅผ JS ์–ธ์–ด๋กœ ๊ตฌํ˜„ํ•˜๊ณ  ์žˆ๋Š” ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ
  • JWT ๊ธฐ๋ฐ˜์œผ๋กœ ์‚ฌ์šฉ์ž ์ธ์ฆ/์ธ๊ฐ€๋ฅผ ํ•˜๋Š” JS ์„œ๋ฒ„ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์—์„œ๋Š” ์ง๊ฐ„์ ‘์ ์œผ๋กœ ํ•ด๋‹น ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ ์‚ฌ์šฉํ•˜๊ฒŒ ๋œ๋‹ค.
npm install jsonwebtoken
npm install -D @types/jsonwebtoken

jsonwebtoken

JWT ๊ด€๋ จ ํ•จ์ˆ˜ ์ƒ์„ฑํ•˜๊ธฐ

  • app/libs ํด๋”์˜ jwt.ts์— ๊ตฌํ˜„ํ•ด์ฃผ์—ˆ๋‹ค.

signJwtAccessToken: token ์ƒ์„ฑ ํ•จ์ˆ˜

  • jwt.sign(): ํ† ํฐ์„ ๋ฐœ๊ธ‰ํ•˜๋Š” ํ•จ์ˆ˜
  • ์ฒซ ๋ฒˆ์งธ ์ธ์ž๋กœ๋Š” ํ† ํฐ์— ๋‹ด์„ JSON ๋ฐ์ดํ„ฐ(payload), ๋‘ ๋ฒˆ์งธ ์ธ์ž๋กœ๋Š” ํ‚ค(key)๋ฅผ ๋ฐ›๋Š”๋‹ค.
    • ๋‘ ๋ฒˆ์งธ ์ธ์ž๋กœ ์ „๋‹ฌ๋œ ํ‚ค๋Š” ์ดํ›„ ํ•ด๋‹น ํ† ํฐ ๊ฒ€์ฆ ์‹œ์—๋„ ์‚ฌ์šฉ๋œ๋‹ค.
  • ์„œ๋ช… ์‹œ ์•Œ๊ณ ๋ฆฌ์ฆ˜์„ ๋”ฐ๋กœ ์„ค์ •ํ•˜์ง€ ์•Š์„ ๊ฒฝ์šฐ HS256์ด ๊ธฐ๋ณธ ์•Œ๊ณ ๋ฆฌ์ฆ˜์œผ๋กœ ์‚ฌ์šฉ๋œ๋‹ค.
    • HS256: ์•”ํ˜ธํ™”์™€ ๋ณตํ˜ธํ™” ์‹œ ๋™์ผํ•œ ํ‚ค๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ๋Œ€์นญํ‚ค ์•Œ๊ณ ๋ฆฌ์ฆ˜
  • ๋‹ค๋ฅธ ๋น„๋Œ€์นญํ‚ค ์•Œ๊ณ ๋ฆฌ์ฆ˜์„ ์‚ฌ์šฉํ•˜๋ ค๋ฉด ์„ธ ๋ฒˆ์งธ ์ธ์ž๋ฅผ ํ†ตํ•ด algorithm ์˜ต์…˜์„ ๋ช…์‹œํ•ด์ค„ ์ˆ˜ ์žˆ๋‹ค.
import jwt, { JwtPayload, SignOptions } from "jsonwebtoken";

// jsonwebtoken ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ์—์„œ ๊ธฐ๋ณธ ์ œ๊ณตํ•˜๋Š” option ํƒ€์ž… 
const DEFAULT_AUTH_OPTION: SignOptions = {
// ํ† ํฐ์˜ ๋งŒ๋ฃŒ ์‹œ๊ฐ„ ์„ค์ • 
  expiresIn: "1h",
// algorithm๋„ ์„ค์ • ๊ฐ€๋Šฅํ•˜๋‹ค.
};

// jwt token ๋ฐœ๊ธ‰ ํ•จ์ˆ˜
export const signJwtAccessToken = (
  payload: JwtPayload,
  options: SignOptions = DEFAULT_AUTH_OPTION
) => {
  const secret_key = process.env.SECRET_KEY;
  const token = jwt.sign(payload, secret_key!, options);
  return token;
};

verifyJwt: token ๊ฒ€์ฆ ํ•จ์ˆ˜

  • ํ† ํฐ ๋ฐœ๊ธ‰ ์‹œ ์‚ฌ์šฉํ–ˆ๋˜ ํ‚ค์™€ ๋™์ผํ•œ ํ‚ค๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ์ฒซ ๋ฒˆ์งธ ์ธ์ž๋กœ ๋ฐ›์€ ํ† ํฐ ๋ฌธ์ž์—ด์„ ๊ฒ€์ฆํ•œ๋‹ค.
  • ํ•ด์„๋œ ๊ฒฐ๊ณผ์ธ JSON ๋ฐ์ดํ„ฐ์—๋Š” iat(issued at) ์†์„ฑ์ด ์ถ”๊ฐ€๋˜์–ด ์žˆ๋‹ค.
    • ํด๋ ˆ์ž„: JWT ํ† ํฐ์— ๋ถ€๊ฐ€์ ์œผ๋กœ ์ €์žฅ๋˜๋Š” ๋ฉ”ํƒ€ ๋ฐ์ดํ„ฐ
  • ํ† ํฐ์„ ๋ฐœ๊ธ‰ํ•  ๋•Œ์™€ ๋‹ค๋ฅธ ํ‚ค๋ฅผ ์‚ฌ์šฉํ•ด ๊ฒ€์ฆ์„ ์‹œ๋„ํ•˜๋ฉด ํ•ด๋‹น ํ‚ค๋กœ๋Š” ์„œ๋ช… ๋ณตํ˜ธํ™”๊ฐ€ ๋ถˆ๊ฐ€๋Šฅํ•˜๊ธฐ ๋•Œ๋ฌธ์— ์œ ํšจํ•˜์ง€ ์•Š์€ ์„œ๋ช…์ด๋ผ๋Š” ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ•œ๋‹ค.
// jwt token ๊ฒ€์ฆ ํ•จ์ˆ˜
export const verifyJwtAccessToken = (token: string) => {
  try {
    const secret_key = process.env.SECRET_KEY;
    const decoded = jwt.verify(token, secret_key!);
    return decoded as JwtPayload;
  } catch (error) {
    throw new Error("์œ ํšจํ•˜์ง€ ์•Š์€ token์ž…๋‹ˆ๋‹ค.");
  }
};

env์— JWT ๊ด€๋ จ SECRET_KEY ์„ค์ •ํ•˜๊ธฐ

SECRET_KEY = ์ž„์˜๋กœ ๋…ธ์ถœ๋˜์ง€ ์•Š์„ ๊ธด ๊ธ€์ž ์ฑ„์›Œ๋„ฃ๊ธฐ 

๋กœ๊ทธ์ธ ๋กœ์ง์—์„œ accessToken ์ƒ์„ฑํ•˜๊ธฐ

  • app/api/signin์—์„œ ๊ตฌํ˜„ํ•œ ๋กœ๊ทธ์ธ API ๋กœ์ง์— accessToken ๋ฐœ๊ธ‰๊ณผ ๊ด€๋ จ๋œ ๋ถ€๋ถ„์„ ์ถ”๊ฐ€ํ•ด์ฃผ์—ˆ๋‹ค.
export async function POST(req: Request) {
  // ๊ธฐ์กด ๋กœ์ง
  // ...

  // accessToken์„ ํฌํ•จํ•œ ์ƒˆ๋กœ์šด result ๊ฐ’์„ ๋ฐ˜ํ™˜ํ•œ๋‹ค.
  // ๋กœ๊ทธ์ธ ๋ฐ˜ํ™˜๊ฐ’์œผ๋กœ ์ „๋‹ฌ๋˜๋Š” user ์ •๋ณด์— accessToken๊นŒ์ง€ ํฌํ•จ๋œ๋‹ค.
    const accessToken = signJwtAccessToken(userWithoutPassword);
    const result = {
      ...userWithoutPassword,
      accessToken,
    };
    return new Response(JSON.stringify(result));
  } else return new Response(JSON.stringify(null));
}

NextAuth ํŒŒ์ผ์— accessToken ๊ด€๋ จ ๋กœ์ง ์ถ”๊ฐ€ํ•˜๊ธฐ

  • app/api/auth/[...nextauth]์˜ route.ts์—์„œ ๊ตฌํ˜„ํ•œ NextAuth์˜ options์— session๊ณผ callbacks๋ผ๋Š” ์†์„ฑ์„ ์‚ฌ์šฉํ•˜์—ฌ NextAuth์˜ session์—์„œ๋„ accessToken์„ ์ถ”๊ฐ€ํ•˜์—ฌ ๊ด€๋ฆฌํ•  ์ˆ˜ ์žˆ๋„๋ก ํ•ด์ฃผ์—ˆ๋‹ค.
// database์— sessionID๋ฅผ ์ง์ ‘ ์ €์žฅํ•  ๊ฒƒ์ธ์ง€, jwt ๋ฐฉ์‹์„ ์‚ฌ์šฉํ•  ๊ฒƒ์ธ์ง€ ์„ ํƒํ•  ์ˆ˜ ์žˆ๋‹ค.
session: {
    strategy: "jwt",
  },
  // ๋กœ๊ทธ์ธ ํผ์—์„œ email๊ณผ password๋ฅผ ์ž…๋ ฅํ•˜๊ณ  ์ œ์ถœ ๋ฒ„ํŠผ์„ ๋ˆŒ๋ €์„ ๋•Œ authorize ๋กœ์ง ์ˆ˜ํ–‰ ํ›„ ์‹คํ–‰๋˜๋Š” ๋ถ€๋ถ„
  // next auth์—์„œ ์‚ฌ์šฉํ•˜๋Š” session์— accessToken์„ ํฌํ•จ์‹œ์ผœ์ค€๋‹ค.
  callbacks: {
    // jwt๊ฐ€ token๊ณผ ์‚ฌ์šฉ์ž ์ •๋ณด๋ฅผ ๋ฐ˜ํ™˜ํ•œ๋‹ค.
    async jwt({ token, user }) {
      return { ...token, user };
    },
    // session ์ฝœ๋ฐฑ ํ•จ์ˆ˜์—์„œ user ๊ฐ์ฒด์— ํ•ด๋‹น ๊ฐ’์„ ์ง€์ •ํ•œ ๋’ค session์„ ๋ฐ˜ํ™˜ํ•œ๋‹ค.
    async session({ session, token }) {
      session.user = token as any;
      return session; // ํ•ด๋‹น session์—๋Š” accessToken์ด ํฌํ•จ๋œ๋‹ค. 
    },
  },

NextAuth์˜ user ํƒ€์ž… ์ปค์Šคํ„ฐ๋งˆ์ด์ง•ํ•˜๊ธฐ

  • NextAuth์—์„œ ๊ธฐ๋ณธ์œผ๋กœ ์ œ๊ณตํ•˜๋Š” session.user ํƒ€์ž…์—๋Š” accessToken์ด ํฌํ•จ๋˜์–ด ์žˆ์ง€ ์•Š๋‹ค.
  • ํƒ€์ž… ์˜ค๋ฅ˜๋ฅผ ๋ฐฉ์ง€ํ•˜๊ธฐ ์œ„ํ•ด types ํด๋”๋ฅผ ์ƒ์„ฑํ•œ ๋’ค next-auth.d.ts๋ผ๋Š” ํƒ€์ž… ์ •์˜ ํŒŒ์ผ์„ ๋งŒ๋“ค์–ด์ค€๋‹ค.
import NextAuth from "next-auth";

export enum UserType {
  User = "User",
  Admin = "Admin",
}

// ์„ธ์…˜ ์ •๋ณด์— accessToken๊ณผ ๊ถŒํ•œ ์ •๋ณด๊ฐ€ ๋‹ด๊ธฐ๋„๋ก customizing 
declare module "next-auth" {
  interface Session {
    user: {
      name: string;
      email: string;
      image: string;
      accessToken: string;
      userType: UserType;
    };
  }
}

์‚ฌ์šฉ์ž ์ ‘๊ทผ ์ œํ•œ ๋กœ์ง ๊ตฌํ˜„ํ•˜๊ธฐ

  • ๊ณต์‹๋ฌธ์„œ์—์„œ ๋‹ค์–‘ํ•œ ๊ฒฝ์šฐ์— ์ ‘๊ทผ ์ œํ•œ์„ ์–ด๋–ป๊ฒŒ ์ฒ˜๋ฆฌํ•  ์ˆ˜ ์žˆ๋Š”์ง€ ์„ค๋ช…ํ•ด์ฃผ๊ณ  ์žˆ๋‹ค.
  • ์—ฌ๊ธฐ์„œ๋Š” App router์—์„œ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋Š” ๋ฐฉ์‹์„ ์ด์šฉํ•˜์˜€๋‹ค.

authOptions ๋ถ„๋ฆฌํ•˜๊ธฐ

  • ์ธ์ฆ ์—ฌ๋ถ€๋ฅผ ํ™•์ธํ•ด์•ผํ•˜๋Š” ํŽ˜์ด์ง€์—์„œ๋„ NextAuth์˜ ์š”์ฒญ options๋ฅผ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋„๋ก ์žฌ์‚ฌ์šฉ ๊ฐ€๋Šฅํ•˜๊ฒŒ ๋ถ„๋ฆฌํ•ด์ฃผ์—ˆ๋‹ค.
// NextAuth์—์„œ ์ œ๊ณตํ•˜๋Š” ๊ธฐ๋ณธ Options ํƒ€์ž…์„ ๋งŒ์กฑํ•œ๋‹ค๊ณ  ๋ช…์‹œํ•ด์ฃผ์—ˆ๋‹ค.
export const authOptions = {
  // ์„ธ๋ถ€ options ๋กœ์ง
} satisfies NextAuthOptions;

// ์ง์ ‘ Options๋ฅผ ๋„ฃ๋Š” ๋Œ€์‹  authOptions๋ผ๋Š” ๋ณ€์ˆ˜์— ํ• ๋‹น๋œ ๊ฐ’์„ ์ „๋‹ฌํ•ด์ฃผ์—ˆ๋‹ค. 
const handler = NextAuth(authOptions);

export { handler as GET, handler as POST };

getServerSession์œผ๋กœ ์„ธ์…˜ ํ™•์ธํ•˜๊ธฐ

  • getServerSession์„ ํ†ตํ•ด ๊ถŒํ•œ ์ •๋ณด๋ฅผ ํ™•์ธํ•˜์—ฌ ์ฒ˜๋ฆฌํ•  ์ˆ˜ ์žˆ๋‹ค.
  • ๋กœ๊ทธ์ธํ•˜์ง€ ์•Š์€ ์‚ฌ์šฉ์ž๊ฐ€ ์‚ฌ์šฉ์ž/๊ด€๋ฆฌ์ž ํŽ˜์ด์ง€์— ์ ‘๊ทผํ•  ๊ฒฝ์šฐ ๊ถŒํ•œ ์—†์Œ ํŽ˜์ด์ง€๋ฅผ ๋ณด์—ฌ์ฃผ๋„๋ก ์ฒ˜๋ฆฌํ•˜์˜€๋‹ค.
import { getServerSession } from "next-auth";
import { authOptions } from "../api/auth/[...nextauth]/route";
import Unauthorized from "@/components/error/unauthorized";

const AdminPage = async () => {
  const session = await getServerSession(authOptions);
  if (!session) return <Unauthorized />;

  return (
    // ๊ด€๋ฆฌ์ž ํŽ˜์ด์ง€
  );
};

export default AdminPage;

getServerSession

  • ์‚ฌ์šฉ์ž๊ฐ€ ๋กœ๊ทธ์ธ์„ ํ•ด์„œ ์ธ์ฆ๋œ ์ฟ ํ‚ค๊ฐ€ ์กด์žฌํ•˜๋Š” ๊ฒฝ์šฐ์—๋งŒ session ๊ฐ์ฒด๋ฅผ ๋ฐ˜ํ™˜ํ•˜๊ณ  ์•„๋‹ ๊ฒฝ์šฐ null์„ ๋ฐ˜ํ™˜ํ•œ๋‹ค.
  • useSession์˜ ๊ฒฝ์šฐ ์‚ฌ์šฉ์ž์˜ ๋กœ๊ทธ์ธ ์—ฌ๋ถ€(์ฟ ํ‚ค๊ฐ€ ์กด์žฌํ•˜๋Š”์ง€ ์—ฌ๋ถ€)์™€ ๊ด€๊ณ„ ์—†์ด session ๊ฐ์ฒด๋ฅผ ๋ฐ˜ํ™˜ํ•œ๋‹ค.

์‚ฌ์šฉ์ž๋ณ„ ๊ฒŒ์‹œ๊ธ€ ํŽ˜์ด์ง€ ๊ตฌํ˜„ํ•˜๊ธฐ

๊ฒŒ์‹œ๊ธ€ ๋ชจ๋ธ ์ƒ์„ฑํ•˜๊ธฐ

  • ์›๋ž˜๋Š” ์‚ฌ์šฉ์ž์˜ id์™€ authorId๋กœ ์—ฐ๊ฒฐํ•˜๋ ค๊ณ  ํ•˜์˜€์œผ๋‚˜ ์‚ฌ์šฉ์ž์˜ id๊ฐ€ ์ผ๋ฐ˜ ์ˆซ์ž ๊ฐ’์ด ์•„๋‹Œ ์ƒํƒœ์—์„œ API ์š”์ฒญ์„ ์œ„ํ•ด ๊ณ ์œ  ์‹๋ณ„์ž๋ฅผ ๋…ธ์ถœ์‹œํ‚ค๋Š” ๊ฒƒ์ด ์˜ณ์€ ๋ฐฉ์‹์ธ์ง€ ์˜๋ฌธ์ด ๋“ค์–ด ๊ณ ์œ ๊ฐ’์ด์ง€๋งŒ ๋…ธ์ถœ๋˜์–ด๋„ ๊ดœ์ฐฎ์€ ๋‹‰๋„ค์ž„์„ FK๋กœ ์‚ฌ์šฉํ•˜๋„๋ก ์„ค์ •ํ•˜์˜€๋‹ค.
model Post {
  postId        Int      @id @default(autoincrement())
  title     String
  content   String?
  published Boolean  @default(false)
  author    User?    @relation(fields: [authorName], references: [name])
  authorName String?
}

์‚ฌ์šฉ์ž๋ณ„ ๊ฒŒ์‹œ๊ธ€ API ๊ตฌํ˜„ํ•˜๊ธฐ

  • User ๋ชจ๋ธ๊ณผ Post ๋ชจ๋ธ์„ ์—ฐ๊ฒฐํ•˜๋Š” FK์ธ name(authorName)์„ ์‚ฌ์šฉํ•˜์—ฌ ๊ฐ ์‚ฌ์šฉ์ž์˜ ๋‹‰๋„ค์ž„์— ๋งž๋Š” ๊ฒŒ์‹œ๊ธ€ ๋ฐ์ดํ„ฐ๋ฅผ ์กฐํšŒํ•  ์ˆ˜ ์žˆ๊ฒŒ ํ•˜์˜€๋‹ค.
  • dynamic routing์„ ์‚ฌ์šฉํ•˜์—ฌ app/api/user/[name] ํ•˜์œ„์— route.ts๋ฅผ ์ƒ์„ฑํ•œ ๋’ค ๊ตฌํ˜„ํ•˜์˜€๋‹ค.
import prisma from "@/app/libs/prisma";

export async function GET(
  request: Request,
  { params }: { params: { name: string } }
) {

  const authorName = params.name;
  const targetPosts = await prisma.post.findMany({
    where: {
      authorName,
    },
    include: {
      author: {
        select: {
          email: true,
          name: true,
        },
      },
    },
  });
  return new Response(JSON.stringify(targetPosts));
}

AccessToken์œผ๋กœ Next.js API ํ˜ธ์ถœ ๋ณดํ˜ธํ•˜๊ธฐ

  • ์œ„์˜ ์š”์ฒญ์—์„œ๋Š” ๋กœ๊ทธ์ธํ•˜์ง€ ์•Š์€ ์‚ฌ์šฉ์ž์˜ ๊ฒฝ์šฐ์—๋„ ๊ฐœ๋ณ„ ์‚ฌ์šฉ์ž์˜ ๊ฒŒ์‹œ๊ธ€ ๋‚ด์šฉ์„ ํ™•์ธํ•  ์ˆ˜ ์žˆ๋‹ค.
  • ์•ž์„œ NextAuth๋ฅผ ํ†ตํ•ด ๊ตฌํ˜„ํ•œ JWT ํ† ํฐ์„ ์‚ฌ์šฉํ•˜์—ฌ ์ด๋Ÿฐ ๊ฒฝ์šฐ ์š”์ฒญ์ด ์ œํ•œ๋˜๋„๋ก ์ฒ˜๋ฆฌํ•˜์˜€๋‹ค.
  • ์˜ฌ๋ฐ”๋ฅธ JWT ํ† ํฐ์„ headers์˜ authorization์— ์‹ค์–ด ๋ณด๋ƒˆ์„ ๋•Œ์—๋งŒ ๊ฒŒ์‹œ๊ธ€ ์กฐํšŒ๊ฐ€ ๊ฐ€๋Šฅํ•˜๋‹ค.
import { verifyJwtAccessToken } from "@/app/libs/jwt";
import prisma from "@/app/libs/prisma";

export async function GET(
  request: Request,
  { params }: { params: { name: string } }
) {
  // accessToken์œผ๋กœ ์„ธ์…˜ ๋ณดํ˜ธํ•˜๊ธฐ
  const accessToken = request.headers.get("authorization");
  if (!accessToken || !verifyJwtAccessToken(accessToken)) {
    return new Response(JSON.stringify({ error: "No Authorization" }), {
      status: 401,
    });
  }

  // ๊ธฐ์กด ๋กœ์ง๊ณผ ๋™์ผ 
}
โš ๏ธ **GitHub.com Fallback** โš ๏ธ