회원가입 및 로그인 구현 #3

chu·2021년 4월 9일
0

이번 시간에는 Router와 passport에 대해서 정리를 하려고 한다.


passport?

Node.js에서 간단하게 인증을 할 수 있게 도와주는 Node.js용 미들웨어입니다.
일반적인 로그인뿐만 아니라 카카오톡, 구글 등 SNS 로그인 인증도 가능합니다.

아래는 passport의 인증 전략에 대한 코드이다.
일단 전략도 중요하지만, 로그인을 시도하면 Router에서 거친 뒤 passport로
와서 인증을 거치고 나서 다시 Router에서 진행되는 흐름이 있다.

이 부분까지 설명하기에는 매우 복잡하기 때문에 생략하도록 하겠다.

passport/index.js

const passport = require('passport');
const local = require('./local');
const { User } = require('../models');

module.exports = () => {
  // 로그인 성공 시 쿠키와 id만 들고있는다.
  passport.serializeUser((user, done) => {
    // null - 서버 에러
    // user.id - 성공해서 user의 id를 가져온다.
    done(null, user.id)
  });

  // 서버에서 유저에 대한 모든 정보를 갖고 있게되면, 서버 과부화가 생기게된다.
  // 그래서 서버는 id만 갖고있다가, 페이지 이동 시 필요한 유저 정보는 DB에서 찾아서 가져온다.
  // 그게 deserializeUser 역할이다.
  passport.deserializeUser( async (id, done) => { // DB에서 정보를 찾으면 req.user로 넣어준다.
    try {
      const user = await User.findOne({ where: { id }});
      done(null, user); // done 시 callback
    } catch(error) {
      console.error(error);
      done(error);
    }
  });

  local();
};

passports/local.js

const passport = require('passport');
const bcrypt = require('bcrypt');
// Strategy -> LocalStrategy로 이름 변경
const { Strategy: LocalStrategy } = require('passport-local');
const { User } = require('../models');

// local 로그인 전략
// done : 첫번째인자 - 서버 에러 / 두번째인자 - 응답 실패,성공 유무 / 세번째인자 - 실패 시 나타낼 문구(reason: XXXX);
module.exports = () => {
  passport.use(new LocalStrategy({
    usernameField: 'email', // req.body.email 라고 명시적으로 알려줌 (정확한 명을 넣어야한다.)
    passwordField: 'password'
  }, async (email, password, done) => { // 함수가 추가된다.
    try {
      const user = await User.findOne({ // 로그인 시도에서 이메일 있는 조건으로 찾아보기.
        where: { email }
      });
      if (!user) {
        // passport에서는 res로 응답이 아닌, 우선 done으로 처리를 한다.
        return done(null, false, { reason: '이메일이 일치하지 않습니다.'});
      }
      // 비밀번호 비교 체크
      // 첫번째 인자 password : 사용자가 입력한 비밀번호
      // 두번째 인자 user.password : 실제 DB에 있는 비밀번호
      const result = await bcrypt.compare(password, user.password);
      if (result) { // 비밀번호 일치할 경우
        return done(null, user); // 두번째 user는 성공의 의미
      }
      // 비밀번호 일치하지 않을 경우
      return done(null, false, { reason: '비밀번호가 일치하지 않습니다.' });
    } catch (err) {
      console.error(err);
      return done(err); // done의 첫번째 인자는 서버 에러시 넣는다.
    }
  }));

};

Router - routes/page.js

클라이언트에서 API요청을 할 경우 해당하는 주소 및 메소드에 맞게 Router를 만들고,
작업 수행에 필요한 코드를 작성하면 된다.

const express = require('express'); // express 가져오기
const { User } = require('../models'); // User 가져오기
const bcrypt = require('bcrypt'); // 비밀번로 해쉬화에 필요한 라이브러리
const passport = require('passport');

const router = express.Router(); // express에서 제공하는 Router 미들웨어

// GET /
router.get('/', (req, res) => {
    res.send('hello~ express');
});

위에서 예시로 만든 router.get은 클라이언트에서 get을 사용해서 서버로 요청이 오면 실행이 된다. /는 기본 주소를 뜻한다. 여기서 app.js를 살펴봐야 한다.

app.js

// app.js에 만든 코드
const pageRouter = require('../routes/page');

app.use('/', pageRouter);

위 처럼 설정은 해놓은 상태였다. 여기서 pageRouter/ 기본적인 주소를 갖게 된다.
즉, http://localhost:3000/을 뜻한다.

그래서 위 코드를 실행해야할 경우에 어떻게 요청을 해야할까?

const 함수명 = createAsyncThunk('변수명', async () => {
  const response = await axios.get('http://localhost:3000/');
  return response;
});

이런식으로 get에는 / 하나만 추가 됐기 때문에 응답으로 hello~ express를 받게된다.

그럼 작성했던 내용을 쭈욱 정리하겠다.

// 회원가입 
// POST /signup
router.post('/signup', isNotLoggedIn, async (req, res, next) => { // POST /signup/
  try {
    const exEmail = await User.findOne({ // 이메일 검사
      where: { // where : DB에서 조건을 건다.
        email: req.body.email,
      }
    });
    const exNickname = await User.findOne({ // 이메일 검사
      where: {
        nick: req.body.nickname,
      }
    });
    if (exEmail) { // 이메일 검사 후 이메일이 기존에 있다면?
      // return으로 res(응답)을 한번만 보내도록 한다. 응답 후 router 종료된다.
      return res.status(403).send('이미 사용중인 이메일입니다.');
    }
    if (exNickname) { // 이메일 검사 후 닉네임이 기존에 있다면?
      return res.status(403).send('이미 사용중인 닉네임입니다.');
    }
    // bcrypt - 비밀번호 해쉬화하기
    const hashedPassword = await bcrypt.hash(req.body.password, 12);
    // User 테이블에 신규 유저 생성하기
    await User.create({
      nick: req.body.nickname,
      email : req.body.email,
      password: hashedPassword,
    });
    // 요청에 대한 성공으로 status(201) : 생성이 됐다는 의미 (기재하는게 좋다.)
    res.status(201).send('create User!');
  } catch(err) {
    console.error(err);
    next(err); // status(500) - 서버에러
  }
});

// 로그인
// 미들웨어 확장법 (req, res, next를 사용하기 위해서)
// passport index.js에서 전달되는 done의 세가지 인자를 받는다.
router.post('/login', isNotLoggedIn, (req, res, next) => {
  passport.authenticate('local', (err, user, info) => { // 여기서 local를 실행한다.
    if (err) { // 서버 에러
      console.error(err);
      return next(err);
    }
    if (info) { // 클라이언트 에러 (비밀번호가 틀렸거나, 계정이 없거나), info.reason에 에러 내용이 있음.
      res.status(403).send(info.reason);
    }
    // 아래는 마지막으로 에러를 검사하는 코드다.
    // 성공하면 passport의 serialize가 실행된다.
    return req.login(user, async (loginErr) => {
      if (loginErr) {
        console.error(loginErr);
        return next(loginErr);
      }
      // 비밀번호를 제외한 모든 정보 가져오기
      const fullUserWithoutPassword = await User.findOne({
        where: { id: user.id },
        attributes: {
          exclude: ['password'], // exclude: 제외한 나머지 정보 가져오기
        },
      });
      // 비밀번호를 제외한 유저 정보를 json으로 응답
      return res.status(200).json(fullUserWithoutPassword);
    });
  })(req, res, next); // 미들웨어 확장에서는 끝에 항상 넣어줘야한다.
});

// 로그아웃
// POST /logout/
router.post('/logout', isLoggedIn, (req, res) => {
  req.logOut();
  req.session.destroy();
  res.send('로그아웃');
});

module.exports = router;

엄청 복잡해 보이지만 사실 좀 복잡하다. 하지만 흐름에 대해서 정리를 한다면,
크케 복잡하진 않다. 이렇게만 하면 기본적인 회원가입과 로그인은 가능하다.

하지만 로그인을 하고 다른 페이지 이동하거나 새로고침을 해도 로그인을 유지하고자 하지 않았나?
이 부분은 이제 프론트에서도 SSR을 설정해줘야하고, 백에서도 지속적인 로그인을 검사해야하는
route를 추가적으로 작성해야한다.

이 부분은 다음 시간에 진행하도록 하겠다.

오늘은 SNS 로그인 작업을 해야하기 때문에!!

profile
한 걸음 한걸음 / 현재는 알고리즘 공부 중!

0개의 댓글