안녕하세요

ID/Password JWT 가입 및 로그인 구현 방법 CredentialsProvider() 알고 보자 본문

카테고리 없음

ID/Password JWT 가입 및 로그인 구현 방법 CredentialsProvider() 알고 보자

sakuraop 2023. 9. 16. 01:49

목차

1. 회원가입 구현

  • 프론트에서 회원가임 폼 작성하기
  • 서버에서 유효성 검사하고 DB에 데이터 저장 요청하기

2. CredentialsProvider()로 ID/Password 로그인 구현하기 (JWT 방식)

  • 1) 로그인페이지로 form 생성
  • 2) form을 통해 로그인 요청 시 db와 회원정보 대조
  • 3) session: cookie에 session을 저장할 때 만료시간 설정
  • 4) jwt callback - jwt token 생성 할 때 실행되는 코드: token에 정보 추가
  • +) session callback - session 생성할 때 실행되는 코드: session에 정보 추가

회원가입 구현하기

계정을 생성하는 과정은 일반적인 방식과 똑같이 구현하면 됩니다.

 

1. 프론트에서 회원가임 폼 작성하기

const Register = () => {
  return (
    <div className={styles.container}>
      <h2>닉네임/비밀번호로 로그인하기</h2>
      <form method="POST" action="/api/auth/signup">
        <label>
          <input name="name" type="text" placeholder="닉네임" />
        </label>
        <label className={styles.email}>
          <input name="email" type="email" placeholder="이메일" />
        </label>
        <label>
          <input name="password" type="password" placeholder="비밀번호" />
        </label>
        <button type="submit">로그인</button>
        <p>* 방문자는 방명록, 댓글 작성만 가능합니다.</p>
      </form>
    </div>
  );
};

export default Register;
  • id, password를 회원가입 api 로 전달합니다.
  • POST 주소를 "/api/auth/signup" 으로 작성하였다면,
    "/src/pages/api/auth/signup.ts" 와 같은 경로에 파일을 생성해주면 됩니다.

2. 서버에서 유효성 검사하고 DB에 데이터 저장 요청하기

import { connectDB } from "@/utils/db/db";
import { NextApiRequest, NextApiResponse } from "next";

const signUpHandler = async (req: NextApiRequest, res: NextApiResponse) => {
  if (req.method === "POST") {
    let { name, email, password } = req.body;
    name = name.trim();
    email = email.trim();
    password = password.trim();

    const userData = {
      name,
      email,
      password,
    };

    const db = (await connectDB).db("blog");
    await db.collection("user_credentials").insertOne({ ...userData });
    res.status(200).json("회원가입이 완료되었습니다.");
  }
};

export default signUpHandler;
  • 회원가입에 가장 기본적으로 필요한 단계는 4가지입니다.
    req.body 정보 가져오기 > db 연결하기 > 회원가입 정보 DB에 저장하기 > 응답하기
  • 여기에 부가적으로 유효성 검사, 중복되는 닉네임 검사 등을 추가해주면 됩니다.

🔻🔻🔻 작성 예시 펼쳐보기

더보기
import { connectDB } from "@/utils/db/db";
import { NextApiRequest, NextApiResponse } from "next";
import bcrypt from "bcrypt";
 
const LENGTH = {
  MIN_NAME: 2,
  MAX_NAME: 10,
  MIN_EMAIL: 5,
  MAX_EMAIL: 100,
  MIN_PASSWORD: 4,
  MAX_PASSWORD: 20,
};
 
const signUpHandler = async (req: NextApiRequest, res: NextApiResponse) => {
  if (req.method === "POST") {
    let { name, email, password } = req.body;
    name = name.trim();
    email = email.trim();
    password = password.trim();
 
    // a@a.a 최소한의 email 형식을 갖추지 못한 경우에는 visitor 강제 할당
    if (email.length < LENGTH.MIN_EMAIL) {
      email = "visitor";
    }
 
    // 길이가 짧은 문자열 경고
    if (name.length < LENGTH.MIN_NAME || password.length < LENGTH.MIN_PASSWORD) {
      return res.status(400).json("짧은 문자열 경고");
    }
 
    // 길이가 긴 문자열 경고
    if (
      name.length > LENGTH.MAX_NAME ||
      email.length > LENGTH.MAX_EMAIL ||
      password.length > LENGTH.MAX_PASSWORD
    ) {
      return res.status(400).json("긴 문자열 경고");
    }
 
    // 중복되는 name, email 비교
    const db = (await connectDB).db("blog");
    const collection = db.collection("user_credentials");
    const isDuplicateName = await collection.findOne({ name });
    const isDuplicateEmail = await collection.findOne({ email });
 
    // 중복되는 name경고
    if (isDuplicateName) {
      return res.status(400).json("중복 이름 경고");
    }
 
    // 중복되는 email 경고
    if (email !== "visitor" && isDuplicateEmail) {
      return res.status(400).json("중복 이메일 경고");
    }
 
    // 비밀번호 해쉬화
    const hashedPassword = await bcrypt.hash(req.body.password, 10);
    password = hashedPassword;
 
    // 부가 정보
    const role = "visitor";
    const created_at = new Date();
 
    const userData = {
      name,
      email,
      password,
      role,
      created_at,
    };
 
    await db.collection("user_credentials").insertOne({ ...userData });
    res.status(200).json("성공");
  }
};
 
export default signUpHandler;

CredentialsProvider()로 ID/Password 로그인 구현하기

nextauth CredentialsProvider() 설정하기

  • src/pages/api/auth/[...nextauth].ts 파일에 다음과 같이 작성합니다.
mport { connectDB } from "@/utils/db/db";
import { MongoDBAdapter } from "@next-auth/mongodb-adapter";
import NextAuth from "next-auth";
import GithubProvider from "next-auth/providers/github";
import CredentialsProvider from "next-auth/providers/credentials";
import bcrypt from "bcrypt";

export const authOptions = {
  providers: [
    CredentialsProvider({
      //1. 로그인페이지로 이동하여 작성할 form을 생성
      name: "credentials",
      credentials: {
        name: {
          label: "테스터 아이디",
          type: "text",
          placeholder: "tester id",
        },
        password: { label: "테스터 비밀번호", type: "password", placeholder: "tester password" },
      },

      //2. form을 통해 로그인 요청시 db와 회원정보 대조
      async authorize(credentials) {
        let db = (await connectDB).db("blog");
        let user = await db.collection("user_credentials").findOne({ name: credentials.name });

        // 존재하지 않는 유저인 경우
        if (!user) {
          return null;
        }

        const isValidPassword = await bcrypt.compare(credentials.password, user.password);
        // 비밀번호가 일치하지 않으면 로그인 실패
        if (!isValidPassword) {
          return null;
        }
        return user;
      },
    }),
  ],

  // 3. cookie에 session token을 저장할 때 만료시간 설정
  session: {
    strategy: "jwt",
    maxAge: 3 * 24 * 60 * 60, // 3일
  },

  callbacks: {
    //4. jwt 생성 시 실행되는 코드
    jwt: async (token: Promise<JWT>) => {
      return token;
    },
    
    //5. 유저 세션이 조회될 때 session에 user 정보를 저장하여 이용할 수 있도록 함
    session: async ({ session }: { session: Promise<Session> }) => {
      return session;
    },
  },

  secret: `${process.env.NEXTAUTH_SECRET}`,
  adapter: MongoDBAdapter(connectDB),
};
export default NextAuth(authOptions);

 

주석이 달린 코드 덩어리 별로 무엇이 일어나는지 살펴봅시다.

 

1. 로그인페이지로 이동하여 작성할 form 생성

export const authOptions = {
  providers: [
    CredentialsProvider({
      //1. 로그인페이지로 이동하여 작성할 form을 생성
      name: "credentials",
      credentials: {
        name: {
          label: "테스터 아이디",
          type: "text",
          placeholder: "tester id",
        },
        password: { label: "테스터 비밀번호", type: "password", placeholder: "tester password" },
      },
  • signin() 함수를 실행한 nextauth 로그인 페이지에서 필요한 폼을 만들 수 있습니다.
  • 위의 코드대로 작성을 한 페이지에서 보여지는 내용은 아래의 사진과 같습니다.

 

2. form을 통해 로그인 요청시 db와 회원정보 대조

      //2. form을 통해 로그인 요청시 db와 회원정보 대조
      async authorize(credentials) {
        let db = (await connectDB).db("blog");
        let user = await db.collection("user_credentials").findOne({ name: credentials.name });

        // 존재하지 않는 유저인 경우
        if (!user) {
          return null;
        }

        const isValidPassword = await bcrypt.compare(credentials.password, user.password);
        // 비밀번호가 일치하지 않으면 로그인 실패
        if (!isValidPassword) {
          return null;
        }
        return user;
      },
  • 로그인하려는 유저 정보가 db에 존재하는지 검사하는 과정을 만들어주면 됩니다.
  • null을 반환할 경우 로그인에 실패하였다는 알림을 띄워줍시다.

 

3. cookie에 session token을 저장할 때 만료시간 설정

  // 3. cookie에 session token을 저장할 때 만료시간 설정
  session: {
    strategy: "jwt",
    maxAge: 3 * 24 * 60 * 60, // 3일
  },
  • 로그인 된 유저의 브라우저에는 session-token이 발급되는데, 이때의 만료기간을 설정합니다.

4. jwt callback - jwt token 생성 할 때 실행되는 코드: token에 정보 추가 가능

   callbacks: {
    //4. jwt 생성 시 실행되는 코드
    jwt: async ({ token }: { token: Promise<JWT> }) => {
      return token;
    },
  • jwt callback 메서드는 JWT token을 생성하기 전 실행되는 코드입니다.

getToken() 함수가 실행될 때 전달되는 정보를 추가할 수 있습니다.

    jwt: async ({ token }: { token: Promise<JWT> }) => {
      token.hello = "hello"
      return token;
    },

출력되는 정보

  const token = await getToken({ req: request });
  console.log(token);

```
{
  name: '비밀',
  email: '비밀',
  hello: 'hello',
  ...
}
```

 

The returned value will be encrypted, and it is stored in a cookie.

리턴되는 값은 암호화되어 cookie에 저장됩니다.JWT Token을 디코딩할 때 필요한 서명은 전체

코드 하단의 secret의 NEXTAUTH_SECRET 환경변수에 포함해야합니다.

  secret: `${process.env.NEXTAUTH_SECRET}`,

 

https://next-auth.js.org/configuration/options#secret

 

Options | NextAuth.js

Environment Variables

next-auth.js.org

 

 

+) session callback - session 생성할 때 실행되는 코드: session에 정보 추가 가능

    //5. 유저 세션이 조회될 때 session에 user 정보를 저장하여 이용할 수 있도록 함
    session: async ({ session }: { session: Promise<Session> }) => {
      return session;
    },
  },

session 방식으로 진행할 때도 마찬가지입니다.


session, jwt 타입 모듈화 하는 방법

https://next-auth.js.org/getting-started/typescript

 

TypeScript | NextAuth.js

NextAuth.js has its own type definitions to use in your TypeScript projects safely. Even if you don't use TypeScript, IDEs like VSCode will pick this up to provide you with a better developer experience. While you are typing, you will get suggestions about

next-auth.js.org