Back-end/Node.js

[Node.js] 익스프레스로 SNS 서비스 만들기 (2)

poppy 2021. 7. 27. 16:47
반응형

https://soohyun6879.tistory.com/170

 

[Node.js] 익스프레스로 SNS 서비스 만들기 (1)

익스프레스로 간단한 SNS 서비스를 만들어보겠습니다! 로그인, 이미지 업로드, 게시글 작성, 해시태그 검색, 팔로잉 기능이 들어갈 예정입니다 하나씩 차근차근 해보겠습니다 :) 1. 프로젝트 세팅

soohyun6879.tistory.com

저번 포스팅에서 프론트 구성과 데이터베이스 세팅까지 완료하였습니다! 이번 포스팅에서는 Passport 모듈로 로그인을 구현해보겠습니다

 

1. Passport 모듈 연결

로그인 구현을 위한 필요한 패키지들을 설치합니다.

npm install passport passport-local passport-kakao bcrypt

 

app.js 파일을 다음과 같이 수정합니다. 추후에 필요한 라우터들을 모두 연결하고 패스포트와 연결합니다. 아직 만들지 않은 라우터들은 추후에 추가할 예정입니다! passport.initialize 는 req 객체에 패스포트 설정을 심는 것이고, passort.session 은 req.session 객체에 패스포트 정보를 저장합니다.

// app.js
const express = require('express');
const cookieParser = require('cookie-parser');
const morgan = require('morgan');
const path = require('path');
const session = require('express-session');
const nunjucks = require('nunjucks');
const dotenv = require('dotenv');
const passport = require('passport');

dotenv.config();
// 라우터 가져오기
const pageRouter = require('./routes/page');
const authRouter = require('./routes/auth');
const postRouter = require('./routes/post');
const userRouter = require('./routes/user');
const { sequelize } = require('./models');
const passportConfig = require('./passport/index');

const app = express();
passportConfig(); // 패스포트 설정
app.set('port', process.env.PORT || 3000);
app.set('view engine', 'html');
nunjucks.configure('views', {
  express: app,
  watch: true,
});
sequelize.sync({ force: false })
  .then(() => {
    console.log('데이터베이스 연결 성공');
  })
  .catch((err) => {
    console.error(err);
  });

app.use(morgan('dev'));
app.use(express.static(path.join(__dirname, 'public')));
app.use('/img', express.static(path.join(__dirname, 'uploads'))); // 업로드한 이미지 제공 라우터
app.use(express.json());
app.use(express.urlencoded({ extended: false }));
app.use(cookieParser(process.env.COOKIE_SECRET));
app.use(session({
  resave: false,
  saveUninitialized: false,
  secret: process.env.COOKIE_SECRET,
  cookie: {
    httpOnly: true,
    secure: false,
  },
}));
app.use(passport.initialize()); // req 객체에 패스포트 설정 심기
app.use(passport.session()); // req.session 객체에 패스포트 정보 저장

// 라우터 연결하기
app.use('/', pageRouter);
app.use('/auth', authRouter);
app.use('/post', postRouter);
app.use('/user', userRouter);

app.use((req, res, next) => {
  const error =  new Error(`${req.method} ${req.url} 라우터가 없습니다.`);
  error.status = 404;
  next(error);
});

app.use((err, req, res, next) => {
  res.locals.message = err.message;
  res.locals.error = process.env.NODE_ENV !== 'production' ? err : {};
  res.status(err.status || 500);
  res.render('error');
});

app.listen(app.get('port'), () => {
  console.log(app.get('port'), '번 포트에서 대기중');
});

 

passport 폴더를 만들고 index.js 를 만듭니다. serializeUser 는 로그인 시 실행되며 req.session 객체에 어떤 데이터를 저장할지 정하는 메서드입니다. done 의 첫번째 인수는 에러 발생시 사용하는 것이고, 두번째 인수는 저장하고 싶은 데이터를 넣습니다. deserializeUser 는 매 요청이 실행되는 메서드입니다. passport.session 가 이 메서드를 호출합니다. deserializeUser 매개변수(id) 는 serializeUser의 done의 두번째 인수로 넣었던 데이터가 됩니다.

// passport/index.js
const passport = require('passport');
const local = require('./localStrategy');
const kakao = require('./kakaoStrategy');
const User = require('../models/user');

module.exports = () => {
  passport.serializeUser((user, done) => { // 로그인시 실행
    done(null, user.id); // 세션에 사용자 아이디 저장
  });

  passport.deserializeUser((id, done) => { // 매 요청시 실행
    User.findOne({ // 세션에 저장했던 사용자 아이디로 사용자 조회
      where: { id },
      include: [{ // 팔로워 목록 조회
        model: User,
        attributes: ['id', 'nick'],
        as: 'Followers',
      }, { // 팔로잉 목록 조회
        model: User,
        attributes: ['id', 'nick'],
        as: 'Followings',
      }],
    })
      .then(user => done(null, user)) // user를 req.user에 저장
      .catch(err => done(err));
  });

  local();
  kakao();
};

 

2. 로컬 로그인 구현하기

로컬 로그인을 구현하기 위해서는 passport-local 모듈이 필요합니다. 로그인한 사용자는 회원가입과 로그인 라우터에 접근할 수 없고 로그인하지 않은 사용자는 로그아웃 라우터에 접근할 수 없다. 따라서 라우터에 접근 권한을 제어하는 미들웨어가 필요합니다. 이 미들웨어는 routes 폴더 안에 middlewares.js 로 만듭니다

// routes/middlewares.js
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 message = encodeURIComponent('로그인한 상태입니다.');
    res.redirect(`/?error=${message}`);
  }
};

 

routes 폴더 안의 page.js 파일을 수정합니다. 넌적스에서 user 객체를 통해 사용자 정보에 접근할 수 있도록 res.locals.user 속성에 req.user 를 넣습니다. 프로필은 로그인 해야 볼 수 있으므로 isLoggedIn 을 사용했고 isAuthenticated() 가 true 여야 next 가 호출되어 res.render 로 넘어올 수 있습니다. 회원가입은 로그인 하지 않은 상태일 때 보여야 하므로 isNotLoggedIn 을 사용했습니다. 

// routes/page.js
const express = require('express');
const { isLoggedIn, isNotLoggedIn } = require('./middlewares');
const { Post, User, Hashtag } = require('../models');

const router = express.Router();

// 템플릿 엔진에서 사용할 변수 설정
router.use((req, res, next) => {
  res.locals.user = req.user; // 사용자 객체
  res.locals.followerCount = req.user ? req.user.Followers.length : 0; // 팔로워 수
  res.locals.followingCount = req.user ? req.user.Followings.length : 0; // 팔로잉 수
  res.locals.followerIdList = req.user ? req.user.Followings.map(f => f.id) : []; // 팔로워 아이디 리스트
  next();
});

// 프로필
router.get('/profile', isLoggedIn, (req, res) => {
  res.render('profile', { title: '내 정보 - NodeBird' });
});

// 회원가입
router.get('/join', isNotLoggedIn, (req, res) => {
  res.render('join', { title: '회원가입 - NodeBird' });
});

module.exports = router;

 

회원 가입, 로그인, 로그아웃 라우터를 만들겠습니다. routes 폴더 안에 auth.js 를 만듭니다. 

회원가입 라우터는 같은 이메일로 가입한 사용자가 있는지 조회한 뒤 같은 이메일로 가입한 사용자가 없다면 비밀번호를 암호호한 후 사용자 정보를 저장합니다. 로그인 라우터는 로그인 전략 코드가 성공하거나 실패하면 authenticate 메서드의 콜백 함수가 실행됩니다. 

// routes/auth.js
const express = require('express');
const passport = require('passport');
const bcrypt = require('bcrypt');
const { isLoggedIn, isNotLoggedIn } = require('./middlewares');
const User = require('../models/user');

const router = express.Router();

// 회원가입
router.post('/join', isNotLoggedIn, async (req, res, next) => {
  const { email, nick, password } = req.body; // 입력받은 정보
  try {
    const exUser = await User.findOne({ where: { email } }); // 같은 이메일로 가입한 사용자 정보
    if (exUser) { // 같은 이메일로 가입한 사용자가 있다면 
      return res.redirect('/join?error=exist'); // 회원가입 페이지로 보냄
    }
    // 같은 이메일로 가입한 사용자가 없다면
    const hash = await bcrypt.hash(password, 12); // 비밀번호 암호화
    await User.create({ // 사용자 저장
      email,
      nick,
      password: hash,
    });
    return res.redirect('/');
  } catch (error) {
    console.error(error);
    return next(error);
  }
});

// 로그인
router.post('/login', isNotLoggedIn, (req, res, next) => {
  passport.authenticate('local', (authError, user, info) => { // 로컬 로그인 전략 수행
    if (authError) { // 가입한 회원이 아닐 때
      console.error(authError);
      return next(authError);
    }
    if (!user) { // 비밀번호가 일치하지 않을 때
      return res.redirect(`/?loginError=${info.message}`);
    }
    return req.login(user, (loginError) => { // 로그인 성공시 passoprt.serializeUser 호출
      if (loginError) {
        console.error(loginError);
        return next(loginError);
      }
      return res.redirect('/');
    });
  })(req, res, next); // 미들웨어 내의 미들웨어에는 (req, res, next)를 붙입니다.
});

// 로그아웃
router.get('/logout', isLoggedIn, (req, res) => {
  req.logout(); // req.user 객체 제거
  req.session.destroy(); // req.session 객체의 내용 제거
  res.redirect('/');
});

module.exports = router;

 

로그인 전략을 만들겠습니다. passport 폴더 안에 localStrategey.js 파일을 만듭니다. passport-local 모듈에서 Strategy 생성자를 불러와 그 안에 전략을 구현하면 됩니다.

// passport/localStrategy.js
const passport = require('passport');
const LocalStrategy = require('passport-local').Strategy;
const bcrypt = require('bcrypt');

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

module.exports = () => {
  passport.use(new LocalStrategy({
    usernameField: 'email', // = req.body.email
    passwordField: 'password', // req.body.password
  }, async (email, password, done) => {
    try {
      const exUser = await User.findOne({ where: { email } }); // 입력 받은 이메일로 가입된 사용자 정보
      if (exUser) { // 입력 받은 이메일로 가입된 사용자가 있다면
        const result = await bcrypt.compare(password, exUser.password); // 비밀번호가 맞는지 확인
        if (result) { // 비밀번호가 맞다면
          done(null, exUser); // passport.authenticate의 콜백 함수
        } else { // 비밀번호가 맞지 않다면
          done(null, false, { message: '비밀번호가 일치하지 않습니다.' });
        }
      } else { // 입력 받은 이메일로 가입된 사용자가 없다면
        done(null, false, { message: '가입되지 않은 회원입니다.' });
      }
    } catch (error) {
      console.error(error);
      done(error);
    }
  }));
};

 

3. 카카오 로그인 구현하기

카카오 로그인 전략을 만들겠습니다. passport 폴더 안에 kakaoStrategy.js 파일을 만듭니다. SNS 로그인은 따로 회원가입 절차가 없어서 처음 로그인할 때는 회원가입 처리를 하고, 두번째 로그인부터는 로그인 처리를 하면 됩니다. passport-kakao 모듈로부터 Strategy 생성자를 불러와 전략을 구현합니다.

// passport/kakaoStrategy.js
const passport = require('passport');
const KakaoStrategy = require('passport-kakao').Strategy;

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('kakao profile', 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 && profile._json.kaccount_email,
          nick: profile.displayName,
          snsId: profile.id,
          provider: 'kakao',
        });
        done(null, newUser);
      }
    } catch (error) {
      console.error(error);
      done(error);
    }
  }));
};
// .env
COOKIE_SECRET=nodebirdsecret
KAKAO_ID=5d4daf57becfd72fd9c919882552c4a6

 

auth.js 파일에 카카오 로그인 라우터를 만듭니다. 카카오 로그인은 로그인 성공시 내부적으로 res.login을 호출하기 대문에 passport.authenticate 메서드에 콜백 함수를 제공하지 않습니다.

const express = require('express');
const passport = require('passport');
const bcrypt = require('bcrypt');
const { isLoggedIn, isNotLoggedIn } = require('./middlewares');
const User = require('../models/user');

const router = express.Router();

// .. 생략

// 카카오 로그인
router.get('/kakao', passport.authenticate('kakao')); 

// 카카오 로그인 후 성공 여부 결과
router.get('/kakao/callback', passport.authenticate('kakao', {
  failureRedirect: '/',
}), (req, res) => {
  res.redirect('/');
});

module.exports = router;
반응형