passport

박상은·2021년 11월 29일
1

🎁 분류 🎁

목록 보기
14/16

1. passport

전략이라는 단어를 사용해서 여러 로그인 전략을 사용할 수 있도록 도와주는 패키지다.

2. 실행 흐름

  1. 로그인 api에 맞게 라우터로 로그인 요청 및 정보 전달받음 ( id, password )
  2. 로그인 라우터에서 passport.authenticate() 호출
    여기서 첫번째 인자를 보고 어떤 전략을 실행할지 결정한다.
  3. 해당 전략에 맞게 passport.use()로 등록해놓은 전략을 실행한다.
  4. 해당 전략에서 호출한 done()으로 passport.authenticate()의 두번째 인자인 콜백함수를 실행하고, done()에서 넣어준 3개의 인자를 넘겨준다.
  5. 유저의 인증이 성공적이면 req.login()을 호출해준다.
  6. req.login()에서 클라이언트로 세션쿠키를 만들어서 넘겨준다. ( 아마 내부적으로 실행됨 )
  7. 이후 첫 로그인 성공이므로 passport.serializeUser()가 실행된다.
    여기서는 유저의 식별자를 done()에 넣어주고 그 값이 세션에 저장시켜진다.
  8. 이후부터 로그아웃까지 요청에 passport.deserializeUser()이 자동적으로 호출되며,
    해당함수에서 done()에 전달한 현재 로그인한 유저의 데이터가 req.user에 들어가게 된다.
    여기서 세션쿠키를 이용해서 유저의 식별자를 passport.deserializeUser()의 인자로 넘겨주는 것 같다.

3. 예시 코드

디테일한 부분은 생략하고 가장 기본적인 구조만 작성했음
실행의 흐름을 보기 편하도록 하나의 파일에 모두 작성했음

const express = require("express");
const session = require("express-session");
const passport = require("passport");
const localStrategy = require("passport-local").Strategy;

const app = express();

// passport내부적으로 세션을 이용하기 때문에 express-session을 등록함
app.use(session({
  resave: false,
  saveUninitialized: false,
  secret: process.env.COOKIE_SECRET,
}));
// 밑에 두개는 정확히 뭔지는 모르겠지만 등록해줘야 오류없이 사용가능함
app.use(passport.initialize());
app.use(passport.session());

/**
* localStrategy()의 콜백의 3번째 인자인 done()의 두번째 인자의 값이
* passport.serializeUser()의 콜백의 첫번째 인자로 들어온다.
* req.login() 실행시 여기가 실행된다.
* 즉, 로그인 성공시 한번만 실행되는 부분이다.
* 여기서는 세션에 저장할 데이터를 넘겨준다.
* 식별자 하나만 저장해두는 이유는 세션은 서버측의 메모리를 사용하기 때문에
* 다수의 유저가 로그인할수록 서버에 부담이 커지기 때문에 최소한의 데이터만 유지하는 것임
*/
passport.serializeUser((user, done) => {
  done(null, user.id);
});

/**
* 여기는 로그인 성공 이후의 호출부터 로그아웃전까지 계속 실행하는 부분이다.
* passport.serializeUser()의 done()에서 전달한 값(식별자)이 첫번째 인자로 들어오고
* 그 값을 이용해서 유저 전체정보를 찾아서 done()에 전달해준다.
* 그렇게 되면 req.user에 해당 값이 들어가게 된다. 즉, req.user에 현재 로그인한 유저의 전체 정보가 들어가게 된다.
**/
passport.deserializeUser(async (id, done) => {
  try {
    const user = await User.findOne({ where: { id } });
    done(null, user);
  } catch (error) {
    console.error("deserializeUser >> ", error);
    done(error);
  }
});

/**
* 로컬 전략을 등록하는 부분이다.
* passport.authenticate("local", callback)을 호출하면 "local"이라는 문자열을 보고
* 등록된 local전략을 실행하게 된다.
* 첫번째 인자는 req.body로 받은 로그인할 유저의 데이터의 이름을 변경하는 부분이다
* 기본적으로 req.body.username, req.body.password 이지만 그 값을 변경하는 부분이다.
* 아래는 req.body.email로 받으라고 passport에게 알려주는 것임
* 두번째 인자인 콜백에 인수로 req.body의 값들이 들어온다.
* done()은 done(서버측에러, 성공한유저정보, 클라이언트측에러) 순으로 값을 넣어야 한다.
* 아래는 아이디와 비밀번호가 일치하는 유저가 존재하는지 확인후에 존재하면 해당 유저의 정보를 반환하는 내용임
*/
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("passport local >> ", error);
        return done(error);
      }
    }
  )
);

app.post("/login", (req, res, next) => {
  // 여기서 "local"만 확인하고 local전략에 가서 실행한 후 done()에 넣어준 인자들이
  // passport.authenticate()의 두번째 인자인 콜백함수의 인자로 들어온다.
  passport.authenticate("local", (error, user, info) => {
    // 서버측 에러일 경우
    if (error) {
      console.error("POST /api/user/login1 >> ", error);
      return next(error);
    }
    // 클라이언트측 에러일 경우 ( 아이디 혹은 패스워드 불일치 )
    if (info) {
      return res.status(401).send({ result: false, message: info.reason });
    }

    /**
    * 로그인 성공 시 실행
    * 여기서 passport가 사용할 세션 쿠키를 만들어서 브라우저에게 전송해 준다.
    * 그것 외에도 클라이언트 측에 보내줄 응답 데이터를 전송한다.
    */
    return req.login(user, async (loginError) => {
      if (loginError) {
        console.error("POST /api/user/login2 >> ", error);
        return next(loginError);
      }
      try {
        const userWithoutPassword = await User.findOne({
          where: {
            id: {
              [Op.eq]: user.id,
            },
          },
          attributes: ["id", "nickname", "email"],
          include: [
            {
              model: Post,
            },
            {
              model: User,
              as: "Followings",
            },
            {
              model: User,
              as: "Followers",
            },
          ],
        });
        return res.json(userWithoutPassword);
      } catch (error) {}
    });
  })(req, res, next);
});

4. 주의할점

  • 클라이언트와 서버의 cors문제 해결해야 제대로 테스트할 수 있음
  • 내부적으로 세션쿠키를 사용하기 때문에 axios를 사용시 withCredentials: true 옵션을 명시해줘야 하며, 서버측에서도 cors에서 credentials: true속성을 줘야한다.

5. 마무리

실행의 흐름은 어느 정도 이해한 것 같은데 공식 문서를 읽은 것도 아니고 그냥 여기저기서 주워들은 지식으로 정리한 거라 정확하지 않은 정보들이 많을 수 있어서 추후에 공식 문서 읽은 후 다시 정리할 생각이다.

0개의 댓글