Backend - 로그인 정보 저장: passport 모듈

BigbrotherShin·2020년 3월 18일
1

Backend

목록 보기
3/15
post-thumbnail
post-custom-banner

사용자 정보는 서버의 세션에
프론트에는 세션을 조회할 수 있는 쿠키를 전달

필요 모듈

  • cookie-parser
  • express-session
  • dotenv
  • passport, passport-local

express에 모듈 연결

backend/index.js

const express = require('express');
const morgan = require('morgan');
const cors = require('cors');
const cookieParser = require('cookie-parser');
const expressSession = require('express-session');
const dotenv = require('dotenv');
const passport = require('passport');

const passportConfig = passport('./passport');
const db = require('./models');
const userAPIRouter = require('./routes/user');
const postsAPIRouter = require('./routes/posts');
const postAPIRouter = require('./routes/post');

dotenv.config(); // 'dotenv' 실행
const app = express();
db.sequelize.sync();
passportConfig();

app.use(morgan('dev'));
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
app.use(cors());
app.use(cookieParser(process.env.COOKIE_SECRET)); // cookie 암호화 키. dotenv 라이브러리로 감춤
app.use(
  expressSession({ // 옵션은 반드시 넣어줘야 한다.
    resave: false, // 매번 세션 강제 저장
    saveUninitialized: false, // 빈 값도 저장
    secret: process.env.COOKIE_SECRET, // cookie 암호화 키. dotenv 라이브러리로 감춤
    cookie: {
      httpOnly: true, // javascript로 cookie에 접근하지 못하게 하는 옵션
      secure: false, // https 프로토콜만 허락하는 지 여부
    },
  }),
);
app.use(passport.initialize());
app.use(passport.session()); // express-session 모듈 아래에 코드를 작성해야 한다. 미들웨어 간에 서로 의존관계가 있는 경우 순서가 중요

passport

작동 순서

  • 로그인 시 req.login() 요청으로 passport.serializeUser() 실행
  • 로그인 중에 로그인 상태가 필요한 요청을 하게된다면
  • 프론트에서 서버로는 cookie만 보냄(asvxzc)
  • 서버가 cookie-parser, express-session으로 쿠키 검사 후 서버 메모리에서 id: 3 발견
  • passport.desirializeUser() 실행
  • id:3 이 deserializeUser에 들어감
  • DB에서 사용자 정보를 찾은 후, req.user로 사용자 정보가 들어감
  • 프론트에서 요청보낼 때마다 deserialize가 실행됨(db 요청 1번씩 실행)
  • 실무에서는 deserialize 결과물 캐싱

예제

passport/index.js

const passport = require('passport');
const db = require('../models');
const local = require('./local');

module.exports = () => {
  passport.serializeUser((user, done) => {
    // router의 req.login 요청이 들어오면 실행된다.
    // 역할: 서버 메모리를 아끼기 위해 많은 사용자 정보 중에서 필요한 부분만 메모리에 저장하도록함. (여기에서는 id)
    // 서버쪽에 [{ id: 3, cookie: 'asvxzc' }] 저장, cookie는 프론트로 보냄
    return done(null, user.id);
  });

  passport.deserializeUser(async (id, done) => {
    try {
      const user = await db.User.findOne({
        // 프론트에서 cookie를 보내면, 서버는 메모리에서 cookie와 관련된 id를 찾은 뒤 DB에서 user 정보를 불러옴.
        where: { id },
      });
      return done(null, user); // 불러온 user 정보는 req.user에 저장
    } catch (e) {
      console.error(e);
      return done(e);
    }
  });

  local();
};

passport.serializeUser()
역할: 서버 메모리를 아끼기 위해 많은 사용자 정보 중에서 필요한 부분만 가벼운 객체로 바꾸어 메모리에 저장하도록함. (여기에서는 id, cookie만 추출)

passport.desirializeUser()
역할: 프론트에서 요청이 있을 때 마다 프론트에서 보내온 cookie와 서버 메모리 상의 id를 사용하여 DB에 요청 후 data를 가져와서 req.user에 저장. 이후 route에서 req.user 객체 사용 가능.

passport/local.js

const passport = require('passport');
const { Strategy: LocalStrategy } = require('passport-local');
const bcrypt = require('bcrypt');
const db = require('../models');

module.exports = () => {
  passport.use(
    new LocalStrategy(
      { // 프론트에서 req.body에 넣어주는 정보. 객체 key 값을 정확히 적어줘야한다.
        usernameField: 'userId', // req.body = { userId: 'abcd', passport: 'xxx' }
        passwordField: 'password',
      },
      async (userId, password, done) => {
        try {
          const user = await db.User.findOne({ where: { userId } });
          if (!user) {
            // 유저가 있는지 확인 후 유저가 없다면
            return done(null, false, { reason: '존재하지 않는 사용자입니다.' });
          }
          const result = await bcrypt.compare(password, user.password);
          if (result) {
            // 유저가 있다면 비밀번호 확인 후 done 두 번째 인자로 유저 정보 넘김
            return done(null, user);
          }
          return done(null, false, { reason: '비밀번호가 틀립니다.' }); // 비밀번호 틀렸을 때
        } catch (e) {
          console.error(e);
          return done(e); // 서버 에러가 있는 경우 done 첫 번째 인자로 error 정보 넘김
        }
      },
    ),
  );
};

passport strategy를 구성했다면, 서버의 router에 연결해줍니다.

routes/user.js

const express = require('express');
const passport = require('passport');

const router = express.Router();

router.post('/login', (req, res, next) => {
  // POST /api/user/login
  passport.authenticate('local', (err, user, info) => {
    // (err, user, info) 는 passport의 done(err, data, logicErr) 세 가지 인자
    if (err) {
      // 서버에 에러가 있는 경우
      console.error(err);
      next(err);
    }
    if (info) {
      // 로직 상 에러가 있는 경우
      return res.status(401).send(info.reason);
    }
    return req.login(user, loginErr => { // req.login() 요청으로 passport.serializeUser() 실행
      if (loginErr) {
        return next(loginErr);
      }
      const filteredUser = Object.assign({}, user.toJSON());
      // user 객체는 sequelize 객체이기 때문에 순수한 JSON으로 만들기 위해 user.toJSON()
      // user.toJSON() 하지 않으면 에러 발생
      // toJSON()을 붙여주는 이유는 서버로부터 전달받은 데이터를 변형하기 때문임.
      delete filteredUser.password; // 서버로부터 전달받은 데이터를 변형하지 않는다면
      return res.json(filteredUser); // toJSON()을 붙이지 않고 바로 응답하여도 무방
    });
  })(req, res, next);
  // 미들웨어(router) 내의 미들웨어(passport)에는 (req, res, next)를 붙입니다.
});

req.login() 요청으로 passport.serializeUser() 실행

profile
JavaScript, Node.js 그리고 React를 배우는
post-custom-banner

0개의 댓글