accessToken, refreshToken으로 auth 구현하기(Next JS, Prisma)

김은호·2023년 2월 26일
2

1. schema 작성

// This is your Prisma schema file,
// learn more about it in the docs: https://pris.ly/d/prisma-schema

generator client {
  provider = "prisma-client-js"
}

datasource db {
  provider = "sqlite"
  url      = "file:dev.db"
}

model User {
  id  Int @id @default(autoincrement())
  name String?
  email String? @unique
  password String?
  birthday String?
  userImage String?
  accessToken String?
  refreshToken String?
}

작성후 npx prisma migrate dev로 db를 생성하고 npx prisma generate로 prisma Client 객체를 생성하자.

2. 필요한 유틸 작성

user data 다루는 유틸 작성

// lib/data/user.ts

import { StoredUserType } from '../type';
import prisma from '../prismadb';

// 이메일로 DB에 유저가 있는지 찾기
const findDB = async ({ email }: { email: string }) => {
  const user = await prisma.user.findUnique({
    where: {
      email,
    },
  });
  return user;
};

// DB에 새 유저 등록
const writeDB = async (users: StoredUserType) => {
  await prisma.user.create({
    data: {
      name: users.name,
      email: users.email,
      password: users.password,
      birthday: users.birthday,
      userImage: users.userImage,
      accessToken: users.accessToken,
      refreshToken: users.refreshToken,
    },
  });
};

export default { findDB, writeDB };

// lib/user/index.ts
import user from './user';

const Data = { user };

export default Data;

api call 작성

// lib/api/index.ts
import Axios from 'axios';

const axios = Axios.create({
  baseURL: process.env.NEXT_PUBLIC_API_URL,
});

export default axios;

// lib/api/auth.ts
import { UserType } from '../type';
import axios from '.';

interface SignUpAPIBody {
  email: string;
  name: string;
  password: string;
  birthday: string;
}

export const signupAPI = (body: SignUpAPIBody) =>
  axios.post<UserType>('/api/auth/signup', body);

export const loginAPI = (body: { email: string; password: string }) =>
  axios.post<UserType>('/api/auth/login', body);

2. 로그인, 회원가입 api 작성

회원가입

// pages/api/auth/signup.ts

import { NextApiRequest, NextApiResponse } from 'next';
import bcrypt from 'bcryptjs';
import jwt from 'jsonwebtoken';
import { StoredUserType } from '../../../lib/type';
import Data from '@/lib/data';

export default async (req: NextApiRequest, res: NextApiResponse) => {
  if (req.method === 'POST') {
    const { email, name, password, birthday } = req.body;

    if (!email || !name || !password || !birthday) {
      res.statusCode = 400;
      return res.send('필요한 데이터가 없습니다.');
    }
    const userExist = await Data.user.findDB({ email });
    if (userExist) {
      res.statusCode = 405;
      return res.send('이미 가입된 이메일입니다.');
    }

    // accessToken 발급
    const accessToken = jwt.sign({ email }, process.env.JWT_SECRET!, {
      expiresIn: '1d',
    });

    // refreshToken 발급
    const refreshToken = jwt.sign({ email }, process.env.JWT_SECRET!, {
      expiresIn: '7d',
    });

    // hashing된 비밀번호 생성
    const hashedPassword = bcrypt.hashSync(password, 8);
    const newUser: StoredUserType = {
      email,
      name,
      password: hashedPassword,
      birthday,
      userImage: '/default_user.png',
      accessToken,
      refreshToken,
    };
    
    // DB에 쓰기
    Data.user.writeDB(newUser);

    console.log('access', accessToken);
    console.log('refresh', refreshToken);

    res.setHeader('Set-Cookie', [
      `accessToken=${accessToken}; HttpOnly; Path=/; Max-Age=86400; SameSite=None; Secure`,
      `refreshToken=${refreshToken}; HttpOnly; Path=/; Max-Age=604800; SameSite=None; Secure`,
    ]);

    res.statusCode = 200;

    res.json({ accessToken, refreshToken });
  }
};

jwt.sign()으로 토큰을 발급할 때 이메일은 unique한 값이므로 이메일을 데이터로 넣었다.

로그인

// pages/api/auth/signin.ts
import { NextApiRequest, NextApiResponse } from 'next';
import bcrypt from 'bcryptjs';
import jwt from 'jsonwebtoken';
import Data from '@/lib/data';
import prisma from '../../../lib/prismadb';

export default async (req: NextApiRequest, res: NextApiResponse) => {
  if (req.method === 'POST') {
    const { email, password } = req.body;
    if (!email || !password) {
      res.statusCode = 400;
      return res.send('필수 데이터가 없습니다.');
    }

    const userExist = await Data.user.findDB({ email });

    // 유저 검색
    if (!userExist) {
      res.statusCode = 404;
      return res.send('가입되지 않은 사용자입니다.');
    }

    // 비밀번호 일치 여부
    const isPasswordMatched = bcrypt.compareSync(password, userExist.password!);
    if (!isPasswordMatched) {
      res.statusCode = 403;
      return res.send('비밀번호가 일치하지 않습니다.');
    }

    // 일치
    const accessToken = jwt.sign({ email }, process.env.JWT_SECRET!, {
      expiresIn: '1d',
    });

    const refreshToken = jwt.sign({ email }, process.env.JWT_SECRET!, {
      expiresIn: '7d',
    });

    // 로그인하면 새로운 토큰으로 업데이트
    await prisma.user.update({
      where: {
        email,
      },
      data: {
        accessToken,
        refreshToken,
      },
    });

    // 쿠키에 토큰 저장
    res.setHeader('Set-Cookie', [
      `accessToken=${accessToken}; HttpOnly; Path=/; Max-Age=86400; SameSite=None; Secure`,
      `refreshToken=${refreshToken}; HttpOnly; Path=/; Max-Age=604800; SameSite=None; Secure`,
    ]);

    // 유저에게 회원정보를 돌려줄 땐 비밀번호는 제거한 상태로 반환
    delete userExist.password;

    res.statusCode = 200;

    res.json({ userExist });
  }
};

token 값 브라우저에서 가져오기

getServerSideProps로 쿠키에 있는 토큰을 가져온다.

export async function getServerSideProps(context: NextPageContext) {
  const { req, res } = context;
  const { accessToken } = req.cookies;

  return {
    props: { accessToken },
  };
}

0개의 댓글