NEXTJS 로 twitter클론 해보기(user passport를 활용하여 login하기)

LeeJaeHoon·2021년 12월 16일
0
post-thumbnail

설치

npm i passport passport-local express-session

passport는 이름처럼 자신의 웹사이트에 방문할 때 여권같은 역할을 합니다.

passport-local은 로그인을 직접 구현할 때 사용됩니다.

express-session은 passport로 로그인 후 유저 정보를 세션에 저장하기 위해 사용합니다.

passport 설정

passport/local.js

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

module.exports = () => {
  passport.use(
    new LocalStrategy(
      {
        usernameField: "email",
        passwordField: "password",
      },
      async (email, password, done) => {
        try {
          const user = await User.findOne({
            where: { email },
          });
          if (!user) {
            return done(null, false, { reason: "존재하지 않는 이메일입니다!" });
          }
          const result = await bcrypt.compare(password, user.password);
          if (result) {
            return done(null, user);
          }
          return done(null, false, { reason: "비밀번호가 틀렸습니다." });
        } catch (error) {
          console.error(error);
          return done(error);
        }
      }
    )
  );
};

usernameField와 passwordField는 어떤 폼 필드로부터 아이디와 비밀번호를 전달받을 지 설정하는 옵션입니다. 위의 경우는 body에 데이터가 { email: 'JaeHoon', password: '123' } 이렇게 오면 뒤의 콜백 함수의 email 값이 JaeHoon, password 값이 123이 됩니다.

이메일과 비밀번호 값이 들어오면 뒤의 콜백 함수가 실행되는데, 내용을 보면 이메일로 유저를 찾은 후, 유저가 없으면 존재하지 않는 이메일이라고 에러를 보냅니다.

user가 존재한다면 비밀번호를 bcrypt.compare로 비교해줍니다. 비밀번호가 일치하지 않는다면 비밀번호가 틀렸다고 에러를 보냅니다.

비밀번호까지 맞게했다면 return done(null, user);을 해줍니다.

done인자는 세개의 인자를 받습니다.

첫번째 : DB조회 같은 때 발생하는 서버 에러를 넣는 곳입니다. 무조건 실패하는 경우에만 사용합니다.

두번째: 성공했을 때 return할 값을 넣는 곳입니다.

세번째: 위에서 비밀번호가 틀렸다는 에러를 표현하고 싶을 때 사용하면 됩니다. 이것은 서버 에러도 아니고, 사용자가 임의로 만드는 에러이기 때문에, 직접 에러 메시지도 써주는 겁니다.

passport/index.js

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

module.exports = () => {
  passport.serializeUser((user, done) => {
    done(null, user.id);
  });

  passport.deserializeUser(async (id, done) => {
    try {
      const user = await User.findOne({ where: { id } });
      done(null, user); // req.user
    } catch (error) {
      console.error(error);
      done(error);
    }
  });

  local();
};

serializeUser은 방금 전에 로그인 성공 시 실행되는 done(null, user);에서 user 객체를 전달받아 세션(정확히는 req.session.passport.user)에 저장합니다. 세션이 있어야 페이지 이동 시에도 로그인 정보가 유지될 수 있습니다. user가 아닌 user.id를 해준이유는 메모리를 아끼기 위해서 해줬습니다.

deserializeUser 은 실제 서버로 들어오는 요청마다 세션 정보(serializeUser에서 저장됨)를 실제 DB의 데이터와

비교합니다. 해당하는 유저 정보가 있으면 done의 두 번째 인자를 req.user에 저장하고, 요청이 처리할 떄 유저의 정보를 req.user를 통해서 넘겨줍니다

serializeUser에서 done으로 넘겨주는 user.id가 deserializeUser의 첫 번째 매개변수로 전달되기 때문에 둘의 타입이 항상 일치해야 합니다.

app.js

const express = require("express");
const cors = require("cors");
const session = require("express-session");
const cookieParser = require("cookie-parser");
const passport = require("passport");
const dotenv = require("dotenv");

const postRouter = require("./routes/post.js");
const userRouter = require("./routes/user.js");
const db = require("./models");
const passportConfig = require("./passport");

dotenv.config();
const app = express();
db.sequelize
  .sync()
  .then(() => {
    console.log("DB 연결 성공");
  })
  .catch(err => {
    console.error(err);
  });
passportConfig();
app.use(
  cors({
    origin: true,
    credentials: false,
  })
); // argumnet로 {origin: true}로 해주면 *대신 보낸곳의 주소가 자동으로 들어간다.
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
app.use(cookieParser(process.env.COOKIE_SECRET));
app.use(
  session({
    saveUninitialized: false,
    resave: false,
    secret: process.env.COOKIE_SECRET,
  })
);
app.use(passport.initialize());
app.use(passport.session());
app.get("/", (req, res) => {
  res.send("Hi");
});
app.use("/post", postRouter);
app.use("/user", userRouter);

app.listen(3065, () => {
  console.log("서버 실행 중");
});

가운데 session을 설정하는 session()passport.initialize()passport.session() 이 부분을 잊지 말고 넣어주셔야 합니다. 또한 앞쪽에 passportConfig();를 넣어주셔야합니다.

router/user.js

const express = require("express");
const bcrypt = require("bcrypt");
const passport = require("passport");
const { User } = require("../models");

const router = express.Router();
...
router.post("/login", (req, res, next) => {
  passport.authenticate("local", (err, user, info) => {
    if (err) {
      console.error(error);
      return next(error);
    }
    if (info) {
      //401은 허가되지않음
      return res.status(401).send(info.reason);
    }
    return req.login(user, loginError => {
      if (loginError) {
        console.log(loginError);
        return next(loginError);
      }
      return res.status(200).json(user);
    });
  })(req, res, next);
});

module.exports = router;

프론트에서 해당 router의 login으로 post요청을 보낸다면 passport.authenticate가 실행됩니다. 이것이 실행되면 앞에서 세운 전략이 실행됩니다.(passport/local.js)해당 전략에서 return done()되는 순간 passport.authenticate 의 콜백함수가 실행됩니다. 해당 콜백함수의 인자로는 done과 같이 세가지가 들어가는데 done으로 보내준 3가지의 인자가 해당 콜백함수의 인자로 넘겨주는 것입니다.

done으로 첫번째 인자값을 넣어줬다면 return next(error);를 해줍니다.

done으로 세번째 인자값을 넣어줬다면 return res.status(401).send(info.reason);을 해줍니다.

done으로 두번째 인자값으로 user을 넘겨줬다면 return req.login이 실행됩니다. 이때 동시에 passport/index.js에 있는 passport.serializeUser가 실행됩니다. 여기서 세션에 user.id를 저장합니다.

그리고 나서 return res.status(200).json(user)를 통해 프론트에 쿠키랑 user를 프론트로 보내줍니다.

쿠키를 통해 서버에서 passport.deserializeUser를 통해 해당 쿠키에 해당되는 user.id를 찾아 user를 찾은후 req.user에 사용자 정보를 넣어줍니다.

logout

const express = require("express");
const bcrypt = require("bcrypt");
const passport = require("passport");
const { User } = require("../models");

const router = express.Router();
...
router.post("logout", (req, res) => {
	req.logout();
	req.session.destroy();
	res.send("ok"); 
})

module.exports = router;

Front

백엔드로 Post요청을 보내 user에 대한 정보를 가져옵니다.

function logInAPI(data) {
  return axios.post("/user/login", data);
}

function* logIn(action) {
  try {
    console.log("saga Login");
    const result = yield call(logInAPI, action.data); //call fork 차이 fork는 비동기 call은 동기
    yield put({
      type: LOG_IN_SUCCESS,
      data: result.data,
    });
  } catch (err) {
    yield put({
      //put은 dispatch라고 생각하면 된다.
      type: LOG_IN_FAILURE,
      error: err.response.data,
    });
  }
}

function* watchLogIn() {
  yield takeLatest(LOG_IN_REQUEST, logIn);
}

export default function* userSaga() {
  yield all([
    fork(watchLogIn), 
    fork(watchLogOut),
    fork(watchSignUp),
    fork(watchFollow),
    fork(watchUnfollow),
  ]);
}

가져온 user정보를 state에 넣어줍니다.

const reducer = (state = initialState, action) => {
  return produce(state, draft => {
    switch (action.type) {
      // LOG IN
      case LOG_IN_REQUEST:
        draft.logInLoading = true;
        draft.logInError = false;
        draft.logInDone = null;
        break;
      case LOG_IN_SUCCESS:
        draft.logInLoading = false;
        draft.logInDone = true;
        draft.me = action.data;
        break;
      case LOG_IN_FAILURE:
        draft.logInLoading = false;
        draft.logInError = action.error;
        draft.logInDone = false;
        break;
      ...
    }
  });
};

0개의 댓글