관리 메뉴

거니의 velog

(23) JWT를 통한 회원 인증 시스템 구현하기 3 본문

React_백엔드 프로그래밍

(23) JWT를 통한 회원 인증 시스템 구현하기 3

Unlimited00 2024. 2. 21. 14:08

3. 회원 인증 API 만들기

* 이제 회원 인증 API를 만들어 보겠다. 먼저 새로운 라우트를 정의한다. api 디렉토리에 auth 디렉터리를 생성하고 그 안에 auth.ctrl.js를 작성하자.

export const register = async (ctx) => {
  // 회원가입
};

export const login = async (ctx) => {
  // 로그인
};

export const check = async (ctx) => {
  // 로그인 상태 확인
};

export const logout = async (ctx) => {
  // 로그아웃
};

* 이번 라우트에서는 총 네 개의 API를 만들 것이다. 이렇게 함수의 틀만 잡아주고, auth 디렉터리에 index.js 파일을 만들어서 auth 라우터를 생성하자.

import Router from 'koa-router';
import * as authCtrl from './auth.ctrl';

const auth = new Router();

auth.post('/register', authCtrl.register);
auth.post('/login', authCtrl.login);
auth.get('/check', authCtrl.check);
auth.post('/logout', authCtrl.logout);

export default auth;

* 그 다음에는 auth 라우터를 api 라우터에 적용해 보자.

[src/api/index.js]

import Router from 'koa-router';
import posts from './posts';
import auth from './auth';

const api = new Router();

api.use('/posts', posts.routes());
api.use('/auth', auth.routes());

// 라우터를 내보낸다.
export default api;

* API 라우트 구조를 다 잡아 놓았으니 이제 본격적으로 기능을 하나씩 구현해 보자.


(1) 회원가입 구현하기

* src/api/auth 디렉터리에 auth.ctrl.js 파일을 만들고 register 함수를 다음과 같이 작성해 보자.

import Joi from 'joi';
import User from '../../models/user';

/*
  POST /api/auth/register
  {
    username: 'bbbb7788',
    password: 'mypass123'
  }
*/
export const register = async (ctx) => {
  // 회원가입
  // Request Body 검증하기
  const schema = Joi.object().keys({
    username: Joi.string().alphanum().min(3).max(20).required(),
    password: Joi.string().required(),
  });

  const result = Joi.validate(ctx.request.body, schema);
  if (result.error) {
    ctx.status = 400;
    ctx.body = result.error;
    return;
  }

  const { username, password } = ctx.request.body;
  try {
    // username 이 이미 존재하는지 확인
    const exists = await User.findByUsername(username);
    if (exists) {
      ctx.status = 409; // Conflict
      return;
    }

    const user = new User({
      username,
    });
    await user.setPassword(password); // 비밀번호 설정
    await user.save(); // 데이터베이스에 저장

    // 응답할 데이터에서 hashedPassword 필드 제거
    const data = user.toJSON();
    delete data.hashedPassword;
    ctx.body = data;
  } catch (e) {
    ctx.throw(500, e);
  }
};

export const login = async (ctx) => {
  // 로그인
};

export const check = async (ctx) => {
  // 로그인 상태 확인
};

export const logout = async (ctx) => {
  // 로그아웃
};

* 회원가입을 할 때 중복되는 계정이 생성되지 않도록 기존에 해당 username이 존재하는지 확인했다. 이 작업은 findByUsername  스태틱 메서드를 사용해 처리했다. 그리고 비밀번호를 설정하는 과정에서는 setPassword 인스턴스 함수를 사용했다.

* 이렇게 스태틱 또는 인스턴스 함수에서 해야 하는 작업들은 이 API 함수 내부에서 직접 구현해도 상관없지만, 이렇게 메서드들을 만들어서 사용하면 가독성도 좋고 추후 유지 보수를 할 때도 도움이 된다.

* 함수의 마지막 부분에서는 hashedPassword 필드가 응답되지 않도록 데이터를 JSON으로 변환한 후 delete를 통해 해당 필드를 지워 주었는데, 앞으로 비슷한 작업을 자주 하게 될 것이다. 따라서 이 작업을 serialize라는 인스턴스 함수로 따로 만들어 줄 것이다.

* 다음 인스턴스 메서드를 user.js 모델 파일에 넣어 주자.

[src/models/user.js]

UserSchema.methods.serialize = function () {
  const data = this.toJSON();
  delete data.hashedPassword;
  return data;
};

* 이제 기존의 코드를 user.serialize() 로 대체시키자.

[src/api/auth/auth.ctrl.js]

export const register = async (ctx) => {
    (...)

    const user = new User({
      username,
    });
    await user.setPassword(password); // 비밀번호 설정
    await user.save(); // 데이터베이스에 저장
    ctx.body = user.serialize();
  } catch (e) {
    ctx.throw(500, e);
  }
};

* 이제 이 API가 잘 작동하는지 확인하기 위해 다음 요청을 Postman으로 테스트해 보자.

POST http://localhost:4000/api/auth/register

{
    "username" : "bbbb7788",
    "password" : "mypass123"
}

회원가입 기능 구현 완료

* 다음과 같은 응답이 잘 나타나면 된다.

{
    "_id": "65d59292f176c733b8038e00",
    "username": "bbbb7788",
    "__v": 0
}

* Compass를 통해 데이터베이스에 실제로 데이터가 잘 생성되는지 확인해 보자. 좌측에 users 컬렉션이 보이지 않는다면 새로고침을 해 보자.

User 데이터 확인

* 데이터가 잘 만들어진 것을 확인했으면 같은 username으로 다시 요청을 보내 보자.

Conflict 에러

* 중복된 username으로 요청을 보냈을 때는 위와 같이 에러가 발생해야 한다. 에러가 잘 발생했는가?


(2) 로그인 구현하기

* 이번에는 로그인 기능을 구현해 보자. login 함수를 다음과 같이 작성한다.

[src/api/auth/auth.ctrl.js]

/*
  POST /api/auth/login
  {
    username: 'bbbb7788',
    password: 'mypass123'
  }
*/
export const login = async (ctx) => {
  // 로그인
  const { username, password } = ctx.request.body;

  // username, password 가 없으면 에러 처리
  if (!username || !password) {
    ctx.status = 401; // Unauthorized
    return;
  }

  try {
    const user = await User.findByUsername(username);
    // 계정이 존재하지 않으면 에러 처리
    if (!user) {
      ctx.status = 401;
      return;
    }
    const valid = await user.checkPassword(password);
    // 잘못된 비밀번호
    if (!valid) {
      ctx.status = 401;
      return;
    }
    ctx.body = user.serialize();
  } catch (e) {
    ctx.throw(500, e);
  }
};

* 이 API에서는 username, password 값이 제대로 전달되지 않으면 에러로 처리한다. 그리고 findByUsername을 통해 사용자 데이터를 찾고, 만약 사용자 데이터가 없으면 역시 에러로 처리한다. 계정이 유효하다면 checkPassword를 통해 비밀번호를 검사하고 성공했을 때는 계정 정보를 응답한다.

* 코드를 다 작성했다면 Postman으로 조금 전에 생성했던 계정 정보로 로그인 API를 요청해 보자.

POST http://localhost:4000/api/auth/login

{
    "username" : "bbbb7788",
    "password" : "mypass123"
}

* 다음과 같이 사용자 정보가 응답되면 된다.

로그인 기능 구현 완료

{
    "_id": "65d59292f176c733b8038e00",
    "username": "bbbb7788",
    "__v": 0
}

* 잘 작동했다면 틀린 비밀번호로도 한번 요청해 보자. 401 Unauthorized 에러가 발생하면 성공이다.