[백엔드]회원가입&로그인 구현하기

Updated:

passport를 이용한 회원가입&로그인 구현

이 게시글은 Node.js를 이용한 프로젝트에서 회원가입과 로그인을 구현하는 방법에 대해 알아본다

사전작업

npm i passport passport-local passport-kakao bcrypt

passport-local은 이메일 로그인을 위해, passport-kakao는 카카오 로그인을 위해 필요하고 bcrypt는 비밀번호 암호화를 위해 사용한다

1. 회원가입 & 이메일 로그인

-> 사실, 회원가입에는 passport가 필요하지 않지만 로그인을 위해 미리 설정해두도록 한다

passport 기본설정


const passport = require('passport');

const authRouter = require('./routes/auth');

const passportConfig = require('./passport');
passportConfig();

app.use(passport.initialize());
app.use(passport.session());

app.use('/auth', authRouter);
  • authRouter를 생성하여 라우터 분리
  • 사용자의 passport폴더의 index.js파일의 설정을 사용하기 위해 passportConfig() 코드 작성
  • req.isAuthenticated() 등 req에서 passport설정을 사용하기 위해 passport.initialize() 코드 작성
  • req.session 객체에서 passport정보를 저장하기 위해 passport.session() 코드 작성
    • req.session 객체를 express-session에서 생성하므로, express-session미들웨어보다 아래에 작성해야함
const passport = require('passport');
const User = require('../models/user');

module.exports = () => {
  passport.serializeUser((user, done) => {
    done(null, user.id);
  });
  
  passport.deserializeUser((id, done) => {
    User.findOne({ where: { id } })
    	.then(user => done(null, user))
    	.catch(err => done(err));
  });
};
  • passport하위의 index.js에 위와 같이 작성한다
  • passport의 핵심은 serializeUserdeserializeUser이다
    • serializeUser는 로그인 시에만 실행되며, user를 인수로 받아 user.id를 req.session에 저장한다
    • deserializeUser는 매요청마다 passport.session 미들웨어에 의해 실행되며, 위에서 로그인 시에 저장한 user.id를 인수로 받아 DB에서 사용자 조회후 해당 user를 req.user에 저장한다 => 이후 로그인한 사용자 정보를 req.user로 접근하여 사용가능하다
exports.isLoggedIn = (req, res, next) => {
  if(req.isAuthenticated()){
    next();
  } else{
    res.status(403).send('로그인되지 않은 상태에서 접근불가능한 경로입니다.');
  }
};

exports.isNotLoggedIn = (req, res, next) => {
  if(!req.isAuthenticated()){
    next();
  } else{
    const errMessage = encodeURIComponent('로그인된 상태에서 접근불가능한 경로입니다.');
    res.redirect(`/?error=${errMessage}`);
  }
};
  • middlewares하위의 index.js에 위와 같이 작성한다
  • 로그인된 상태에서만 접근가능한 미들웨어에는 isLoggedIn을 가져다 쓰고, 로그아웃 상태에서만 접근가능한 미들웨어에는 isNotLoggedIn을 가져다 쓴다
  • req.isAuthenticated()를 통해 현재 로그인중인지의 여부를 확인가능하다
const express = require('express');
const { isLoggedIn, isNotLoggedIn } = require('../middlewares');
const { join, login, logout } = require('../controller/auth');
const router = express.Router();

router.post('/join', isNotLoggedIn, join);
router.post('/login', isNotLoggedIn, login);
router.get('/logout', isLoggedIn, logout);

module.exports = router;

  • routes하위의 auth.js에 위와 같이 작성한다
  • 앞서 작성한 middleware 하위의 index.js코드와 앞으로 작성할 controller 하위의 auth.js코드를 구조분해할당을 통해 가져온다
  • join과 login은 로그인되지 않은 상태에서만 접근가능하고, logout은 로그인된 상태에서만 접근가능하도록 코드를 작성하였다

회원가입


const bcrypt = require('bcrypt');
const User = require('../models/user');

exports.join = async (req, res, next) => {
  const { email, nick, password } = req.body;
  try{
    const exUser = await User.findOne({ where: { email } });
    if(exUser){
      const errMessage = encodeURIComponent('이미 존재하는 이메일입니다.');
      return res.redirect(`/join?error=${errMessage}`);
    }
    
    const realPw = await bcrypt.hash(password, 12);
    await User.create({
      email,
      nick,
      password: realPw,
    });
    
    return res.redirect('/');
  } catch(err){
    console.error(err);
    return next(err);
  }
}
  • controllers 하위의 auth.js에 위와 같이 회원가입 코드를 작성한다
  • 회원가입 전에 해당 이메일의 사용자가 DB에 존재하는지 확인 후에 존재하면 에러를 띄워주고 없을 경우 bcrypt를 이용하여 비밀번호를 암호화하여 DB에 저장한다
  • async, await를 이용하여 비동기처리한다

이메일 로그인


const passport = require('passport');

exports.login = (req, res, next) => {
  passport.authenticate('local', (authErr, user, info) => {
    if(authErr){
      console.error(authErr);
      return next(authErr);
    }
    if(!user){
      return res.redirect(`/?error=${info.message}`);
    }
    return req.login(user, (loginErr) => {
      if(loginErr){
        console.error(loginErr);
        return next(loginErr);
      }
      return res.redirect('/');
    });
  })(req, res, next);
};
  • controllers 하위의 auth.js에 위와 같이 로그인 코드를 추가한다
  • passport.authenticate의 첫번째 인자는 전략의 종류로 ‘local’일 경우 이메일 로그인, ‘kakao’일 경우 카카오 로그인을 의미한다
    • passport하위의 index.js로 이동하여 전략의 종류에 맞는 작업을 수행한다
  • passport.authenticate의 두번째 인자는 첫번째 인자인 전략의 종류에 따라 작업을 수행한 후에 실행할 콜백함수이다
    • 이 콜백함수는 전략의 종류에 따른 작업 수행부분에서 done()의 인자값을 그대로 전달받는다
    • 첫번째 인자는 작업수행간 발생한 시스템적 오류, 두번째 인자는 사용자정보, 세번째 인자는 시스템적오류가 없는 상태지만 로그인시키면 안되는 이유를 설명해준다
  • 시스템적 오류가 발생했을 경우 에러처리 미들웨어로 넘기고, 시스템적 오류가 없지만 로그인시키면 안되는 경우 해당 메시지를 띄워주고, 둘다 아닐 경우에 req.login을 통해 로그인시켜준다
    • req.login은 앞서 passport하위의 index.js에 작성한 serializeUser를 실행시키며 여기서 첫번째 인자값으로 적은 user를 전달한다
  • 미들웨어 내부에서 외부의 인자값을 사용하므로 미들웨어 확장 문법을 사용한다
const passport = require('passport');
const LocalStartegy = require('passport-local').Strategy;
const bcrypt = require('bcrypt');
const User = require('../models/user');

module.exports = () => {
  passport.use(new LocalStartegy({
    usernameField: 'email',
    passwordField: 'password',
    passReqToCallback: false,
  }, async (email, password, done) => {
    try{
      const exUser = await User.findOne({ where: { email } });
      if(exUser){
        const compare = await bcrypt.compare(password, exUser.password);
        if(compare){
          done(null, exUser);
        } else{
          done(null, false, { message: '비밀번호가 틀립니다.' });
        }
      } else{
        done(null, false, { message: '가입하지 않은 회원입니다.' });
      }
    } catch(err){
      console.error(err);
      done(err);
    }
  }));
};
  • passport 하위의 localStrategy.js에 위와 같이 코드를 작성한다
  • LocalStrategy 생성자의 첫번째 인자로 전략에 관한 설정을 한다
    • usernameField와 passwordField에 각각 id와 비밀번호인 req.body.email과 req.body.password가 들어간다
  • LocalStrategy 생성자의 두번째 인자로 전략을 수행하는 함수이다
    • 첫번째 인수에서 넣어준 email과 password 그리고 done함수가 여기서 3개의 인자로 들어간다
    • done의 인자로 적은 값들이 controllers 하위의 auth.js에서 passport.authenticate의 두번재 인자값인 함수의 3개의 인자로 들어간다
  • 로그인 시에 입력한 이메일에 해당하는 사용자를 DB에서 찾아 존재할 경우 비밀번호를 비교하고 일치할 경우 해당 사용자정보를 passport.authenticate로 넘겨준다. 일치하지 않을 경우와 DB에 존재하지 않을 경우는 done의 세번째 인자값에 담아 passport.authenticate로 넘긴다
  • async, await를 이용하여 비동기처리한다

로그아웃


exports.logout = (req, res) => {
  req.logout(() => {
    res.redirect('/');
  });
};
  • controller하위의 auth.js에 위와 같이 로그아웃 코드를 추가한다

2. 카카오 로그인

const passport = require('passport');

router.get('/kakao', passport.authenticate('kakao'));
router.get('/kakao/callback', passport.authenticate('kakao', {
  failureRedirect: '/?error=카카오로그인실패',
}), (req, res) => {
  res.redirect('/');
});
  • routes하위의 auth.js에 위와 같이 카카오 로그인 코드를 추가한다
  • /kakao를 통해 카카오 로그인창으로 이동하고, /kakao/callback을 통해 로그인 성공여부를 전달받는다
    • 카카오 로그인은 이메일 로그인과 다르게 카카오에서 자체적으로 req.login을 호출한다
    • 로그인 실패 시와 성공 시의 redirect경로를 적어준다
const passport = require('passport');
const KakaoStrategy = require('passport-kakao');
const User = require('../models/user');

module.exports = () => {
  passport.use(new KakaoStrategy({
    clientID: process.env.KAKAO_ID,
    callbackURL: '/auth/kakao/callback',
  }, async (accessToken, refreshToken, profile, done) => {
    console.log(profile);
    try{
      const exUser = await User.findOne({
        where: { snsId: profile.id, provider: 'kakao' },
      });
      if(exUser){
        done(null, exUser);
      } else{
        const newUser = await User.create({
          email: profile._json?.kakao_account?.email,
          nick: profile.displayName,
          snsId: profile.id,
          provider: 'kakao',
        });
        done(null, newUser);
      }
    } catch(err){
      console.error(err);
      done(err);
    }
  }));
};
  • passport하위의 kakaoStrategy.js에 위와 같이 코드를 작성한다
  • localStrategy와 유사하지만 전략에 대한 설정과 전략을 수행하는 함수가 다름을 확인할 수 있다
    • clientID는 .env에서 설정한 KAKAO_ID를 가져다 쓰고 callbackURL은 routes하위의 auth.js에 작성한 미들웨어의 콜백경로와 일치해야 한다
    • profile에 카카오에서 취급하는 사용자 정보가 들어있는데, 자주 바뀌므로 console.log를 통해 찍어보는 것이 권장된다
    • 참고로 이메일 로그인 시의 provider는 default값인 local이 들어간다
  • localStrategy의 로직과 다르게 DB에서 찾은 사용자가 없어도 오류를 발생시키는 것이 아니라 DB에 저장후 로그인시킨다
    • 카카오 코드는 사실상 회원가입과 로그인을 동시에 처리함을 알 수 있다
  • async, await를 이용하여 비동기처리한다

Leave a comment