[투데이 링크] 회원가입, 로그인 구현하기

김세현·2023년 3월 2일
1

NextAuth.js 사용하기

투데이 링크에서는 사용자 인증을 위해 NextAuth를 이용하고 있습니다.

NextAuthNext.js로 구축된 웹 어플리케이션에서 사용자 인증 과정을 아주 쉽게 구현할 수 있도록 도와주는 npm 패키지입니다.

이전에 저는 jsonwebtoken이라는 패키지를 통해 간단한 인증을 구현해 본 경험이 있었기 때문에

투데이 링크 인증에도 jsonwebtoken을 사용할 계획이 있었습니다.

그런데, Next.js를 학습하며 NextAuth라는 기술을 알게 되었고, NextAuth의 여러 장점들을 알게 되었습니다.

  1. 토큰에 서명하고 생성하고 검증하는 과정을 모두 NextAuth가 처리해 줍니다.
  2. 클라이언트 측, 서버 측에서 필요한 기능을 모두 제공해 줍니다. (useSession , getServerSession 등)
  3. NextAuth가 제공하는 signIn, signOut 등의 함수를 통해 간단하게 로그인, 로그아웃을 구현할 수 있습니다.
  4. 다양한 인증 방식이 존재합니다. (소셜 로그인, 이메일 인증, 커스텀 인증 등)
  5. Firebase FireStore, MongoDB, MySQL, Postgres 등 데이터베이스의 종류에 상관없이 사용자 인증을 구현할 수 있습니다.
  6. NextAuth 또한 jwt를 이용할 수 있습니다.

저는 이러한 장점들이 투데이 링크 프로젝트에 아주 적합하다고 생각했기 때문에 NextAuth를 이용하여 투데이 링크의 인증을 구현했습니다.


회원가입 구현하기

NextAuth는 사용자 인증을 아주 쉽게 구현할 수 있도록 도와주지만, 사용자 유저의 정보를 관리하는 것은 개발자가 직접 수행해야 합니다. (회원가입 및 유저 정보를 데이터베이스에 저장하는 것)

투데이 링크는 유저의 정보를 관리하기 위해 Firebase Firestore를 이용하고 있습니다.

유저 데이터의 구조는 다음과 같습니다.

  • id : 유저별 고유 ID
  • userId : 투데이 링크 서비스 이용 시 사용하는 유저 계정
  • password : 계정 암호
  • email : 본인 인증을 위한 방법으로 이메일을 사용하고 이메일을 통해 아이디 찾기, 비밀번호 찾기가 가능합니다.

회원가입 시 사용자가 보는 클라이언트 측 UI는 다음과 같습니다.

회원가입시 요청하는 서버 측 api의 구조는 다음과 같습니다.

pages
├── api
     └── auth
         └── signUp
             ├── checkId.ts
             ├── index.ts
             └── verifyEmail.ts
             

이처럼, 투데이 링크 회원가입 과정에서는 별도로 중복된 아이디를 체크하기 위한 단계와 이메일 인증을 위한 단계가 존재합니다.

먼저, 중복된 아이디를 체크하기 위해 클라이언트 측에서 입력된 정보로 서버 측 API pages/api/auth/signUp/checkId를 호출합니다.
회원가입 API 코드 보기

그리고 서버 측에서는 다음과 같은 순서로 클라이언트의 요청을 처리합니다.

  1. 사용자 입력값(ID)에 대한 유효성을 검사합니다. -> 오류 시 에러를 응답합니다.
  2. 입력된 ID 값이 유효하다면, 해당 ID가 데이터베이스에 존재하는지 검사합니다.
  3. 아이디 존재 여부에 따라 응답합니다 : res.status(200).json({ message: "success", isExistsId });

결과적으로 아이디 중복 체크를 수행하는 클라이언트 측 UI는 다음과 같이 보여집니다.

또한 회원가입은 본인 인증을 위한 이메일 인증 과정도 거쳐야 합니다.

이메일 인증은 이메일을 전송하는 과정이 필요한데, 이메일 전송은 이메일 서버가 필요합니다.

하지만, 이메일 서버를 직접 구축하는 것은 많이 부담스럽고 어려운 작업이었기 때문에 직접 메일 서버를

구축하지 않고 nodemailer 패키지를 사용했으며, 메일 전송을 위해 네이버의 POP3/SMTP를 이용했습니다.

네이버의 POP3/SMTP는 네이버 웹 메일에 직접 접속하지 않고서도 외부 메일 프로그램을 이용해서 네이버 메일을 보내고 받을 수 있는 기능입니다.
POP3/SMTP 참고

이메일 인증을 위한 서버 측 API pages/api/auth/signUp/verifyEmail을 호출하게 되면, 서버 측은 다음과 같은 과정을 거칩니다.

  1. 사용자가 입력한 이메일 입력 값에 대한 유효성 검사를 수행합니다. -> 오류 시 에러 응답
  2. 입력된 이메일이 이미 사용 중인 이메일인지 검사합니다.
  3. 사용 가능한 이메일이라면, 인증 코드 6자리를 생성합니다.
  4. nodemailer 패키지를 이용해 인증 코드가 담긴 메일을 전송합니다.
  5. 메일 전송이 완료되면, 클라이언트에게 인증 코드를 담아 응답합니다.

결과적으로 이메일 인증을 수행하는 클라이언트 측 UI는 다음과 같이 보여집니다.

로그인 구현하기

로그인 과정에는 사용자를 인증하고 권한을 부여하는 작업이 필요합니다.

NextAuth를 이용한다면 사용자를 인증하고 권한(토큰)을 가지는지의 여부를 쉽게 확인할 수 있습니다.

Next.js로 구축된 프로젝트에서 NextAuth를 이용하기 위해선 별도의 동적 API 라우트를 추가해야 합니다.

API 경로는 다음과 같습니다 pages/api/auth/[...nextauth].ts

동적 라우트인 이유는 NextAuth 패키지는 내부적으로 /api/auth/~~에 해당하는 여러 라우트를

사용하고 특정 경로에 따라 적절하게 처리해 주기 때문입니다. 참고 문서

예를 들어, NextAuth 패키지를 사용해서 로그인을 구현할 경우 클라이언트 측에서는 signIn() 메소드를 호출해야 하고,

signIn()메소드를 호출하게 되면, NextAuth 내부에서는/api/auth/signin/:provider 경로로 POST 요청을 하게 됩니다.

따라서 pages/api/auth/[...nextauth].ts API는 사용자의 로그인 요청 시에 호출되며,

이 안에서 사용자 입력값이 유효한지, 비밀번호가 맞는지, 데이터베이스에 유저 정보가 존재하는지에 대한 코드를 작성할 수 있습니다.

우선, pages/api/auth/[...nextauth].ts 경로에서 NextAuth를 적절하게 사용하기 위해선 옵션을 설정해 주어야 합니다.

먼저, Provider 옵션을 설정해야 하는데, Provider 옵션은 인증 방식을 결정하는 옵션입니다.

인증 방식에는 대표적으로 OAuth, Email, Credentials가 있습니다.

투데이 링크에서는 자체적으로 아이디와 패스워드를 통해 사용자를 인증하므로 CredentialsProvider 옵션을 사용했습니다. 참고 문서

또한 CredentialsProvider 옵션에서 중요한 것은 authorize 메소드 입니다.

authorize 메소드는 로그인 요청이 있을 때 Next.js가 호출하는 메소드입니다.

이 메소드의 인자로 사용자가 입력한 데이터(아이디, 비밀번호)와 요청 객체 req가 전달되기 때문에

이 메소드 안에서 유효성 검사를 수행하거나 데이터베이스에 접근하는 등 자체적인 인증 로직을 작성할 수 있습니다.

결과적으로 실패할 경우, null또는 false를 반환하고, 성공하게 되면 객체를 반환하는데, 이때 반환되는 객체는JWT으로 부호화됩니다.

또한 로그인 요청이 성공할 경우 Next.jsJson Web Token을 쿠키에 보관합니다.

/api/auth/[...nextauth] 전체 코드 보기

이후 사용자가 인증이 되었는지(세션 객체 존재 여부)를 확인하고 싶다면 클라이언트 측에서는 useSession훅 또는 getSession을 사용할 수 있으며,

서버 측에서는 getServerSession을 통해 확인할 수 있습니다. 참고 문서

그리고 이 세션 객체(토큰)를 기반으로 권한이 필요한 라우트 접근을 제한하거나 API 요청을 제한할 수 있습니다.

권한 검사를 통한 라우트 보호 및 API 요청 제한

1. 마이페이지, 패스워드 변경 라우트 보호하기

마이페이지와 비밀번호 변경 페이지는 사용자가 로그인해야만 접근할 수 있는 페이지입니다.

즉, 사용자는 이 라우트(페이지)에 접근하기 위한 권한이 필요합니다.

저는 사용자가 해당 라우트(페이지)에 접근하는 시점에서 권한을 체크하기 위해getServerSideProps 함수와 NextAuthgetServerSession 함수를 이용했습니다.

(참고: 저는 조금 더 빠른 시점에서 권한을 체크하기 위해 getServerSideProps를 이용했습니다. 같은 로직을 클라이언트 측에서 getSession이나 useSession을 사용해서 구성할 수도 있습니다.)

getServerSession이라는 함수는 NextAuth에서 제공하는 함수이며 세션 객체(토큰)를 반환합니다.getServerSession 참고 문서

저는 이 함수를 통해 세션의 존재 유무를 검사했고, 세션이 존재하면 라우트(페이지) 접근을 허용하고

세션이 존재하지 않으면, 사용자를 로그인 페이지로 리다이렉트했습니다.

최종 코드는 다음과 같습니다.

// pages/mypage/changePassword
// pages/mypage
export const getServerSideProps: GetServerSideProps = async (context) => {
  const session = await getServerSession(context.req, context.res, authOptions);
  if (!session) {
    return {
      redirect: {
        destination: "/auth/signIn",
        permanent: false,
      },
    };
  }
  return {
    props: {
      session,
    },
  };
};
  • 리다이렉트 예시

2. API 요청 제한하기

권한이 없을 경우, 클라이언트 측 라우트(페이지)의 접근을 막는 것뿐만 아니라 서버 측 API 요청도 제한할 필요가 있습니다.

왜냐하면, 서버 측 API에 요청하는 행동은 클라이언트 앱을 통해서만 할 수 있는 것이 아니라

Postman 등 다른 방법들을 통해서도 가능하기 때문입니다.

따라서 저는 클라이언트 측에서도 비밀번호 변경 페이지의 접근을 제한했을 뿐만 아니라

서버 측 비밀번호 변경 API에서도 들어온 요청에 대해 적절한 권한이 있는지를 검사하고 있습니다.

서버 측 권한 검사를 위한 코드는 다음과 같습니다.
전체 코드 보기

profile
under the hood

0개의 댓글