본문으로 바로가기

 

MySQL Sequelize를 설치했다고 가정하고 진행되었습니다. 그러나 무슨 SQL을 사용하던 흐름이나 코드는 동일합니다.

 

local의 로그인/로그아웃 순서는 대략 이렇습니다.

 

1. form에 회원 정보를 입력하고 Login 버튼을 누름으로써 웹 서버에 POST 메소드를 보냅니다.

2. 백엔드측은 POST를 받으면 passport.authenticate를 실행합니다.

3. passport.authenticate("local", (authError, user, info) => {...}) 을 통해 local 전략을 수행합니다.(LocalStrategy.js) 전략의 수행 결과로 done 함수에 (에러, 성공, 실패) 가 담깁니다.

4. local 전략을 통과했다면(DB에 있는가, 비밀번호는 일치하는가 등) passport.authenticate의 콜백 부분을 실행하여 유저 정보 객체와 req.login를 호출합니다. 

5. req.login 메서드는 자동으로 passport.serializeUser를 호출합니다. 

6. serializeUser에서 지정한대로 세션에 사용자 아이디를 저장합니다.

7. 그 이후 passport.session() 미들웨어는 항상 passport.deserializeUser를 호출합니다.

8. req.session에 저장된 유저의 아이디를 DB에서 찾고 req.user에 저장합니다.

9. 라우터에서 req.user를 사용할 수 있게 됩니다. middleware.js에 등록해서 전역 객체화 해서 편하게 사용합시다

10. form에서 Logout을 누르면 req.logout 메서드를 실행합니다. req.user 객체를 제거합니다. req.session.destroy() 까지 실행한다면 req.session 객체의 내용도 제거합니다.

 


 

* passport를 import함으로써 req.user(로그인이 된 유저의 도큐먼트), req.login, req.logout, req.isAuthenticated(로그인 여부를 boolean값으로 알려줌)이 활성화됩니다. req.session은 serialize, req.user는 deserialize의 결과로 생성됩니다. 

 

* passport.authenticate('local', ...), passport.authenticate('kakao', ...) 꼴로 로그인 처리를 합니다.

 

* passport와 관련된 미들웨어 (passport.initialize(), passport,session()은 가급적 middleware의 최하단에 위치시킵시다. express-session을 사용하기 때문에 더 위에 있으면 작동하지 않습니다.

 

* async/await가 자주 사용됩니다. 로그인 정보를 처리할 때까지 기다려야 하기 때문입니다. async/await가 적용될 때는 try/catch로 오류를 잡아주는 습관을 들입시다.

 

* done(cb)은 passport가 주는 함수입니다.

 

* req.user가 undefined가 떴다면 미들웨어의 순서가 잘못되었을 가능성이 높습니다. 미들웨어의 순서가 굉장히 중요합니다. 미들웨어의 순서가 잘못되면 req.user가 undefined으로 뜬다던지 세션이 저장이 안되는 등의 오류가 발생합니다. 순서를 위해 가져와봤습니다.

 

app.use(helmet());
app.set("view engine", "pug");
app.use("/uploads", express.static("uploads"));
app.use("/static", express.static("static"));
app.use(cookieParser(process.env.COOKIE_SECRET));
app.use(bodyParser.urlencoded({ extended: true }));
app.use(bodyParser.json());
app.use(morgan("dev"));
app.use(
  session({
    secret: process.env.COOKIE_SECRET,
    resave: true,
    saveUninitialized: false
  })
);
app.use(passport.initialize());
app.use(passport.session());

app.use(localsMiddleware);
app.use(routes.home, globalRouter);
app.use(routes.users, userRouter);
app.use(routes.videos, videoRouter);

 

* Unknown authentication strategy "local"과 같이 인증 방법을 못 찾겠다는 오류가 나오면 대개 app.js에서 passport와 passport의 전략들을 import하지 않았기 때문에 발생하는 오류입니다.

 

* req.login시 serializeUser를 호출함. 반면 deserializeUser는 passport.session()에서 매 요청시마다 매번 실행됨. 그래서 deserializeUser는 캐싱해서 DB 요청 횟수를 줄여 효율적으로 만드는 것이 중요하다.

 

* .find() was removed on sequelize V5. instead of using .find() you can use findOne(). 그러니까 sequelize v5 부터 find가 사라졌으니 findOne을 사용해야 한다.

 

npm i passport passport-local passport-kakao bcrypt

 

passport를 사용하기 위해 중앙 통제실에 해당하는 app.js에 다음을 import, 연동, middleware를 추가합니다.

// app.js

import passport from "passport";
import passportConfig from "./passport";

//mysql 연동
db.sequelize.sync();

//passport config
passportConfig(passport);

// middleware
app.use(cookieParser(process.env.COOKIE_SECRET));
app.use(
  session({
    secret: process.env.COOKIE_SECRET,
    resave: false,
    saveUninitialized: true,
    cookie: {
      httpOny: true,
      secure: false
    }
  })
);
app.use(passport.initialize());
app.use(passport.session());

 

 

 

🚗 회원가입과 local 로그인

 

1️⃣회원가입과 로그인 form의 input을 받을 auth router를 코딩합니다 (+ middleware)

2️⃣ passport에 로컬 전략을 코딩합니다.

3️⃣ app.js와 연결합니다

 

 

반드시 순서를 따를 필요는 없습니다. 설명을 위해서 나열했을 뿐이고, 실제로는 왔다갔다 하면서 코딩합니다.

 

우선, 필요한 패키지를 설치한다. passport, passport-local, bcrypt

npm i passport passport-local bcrypt

 

passport 폴더 내 사용하고자하는 Strategy.js를 정의하고 인증을 처리할 router도 만들어주자.

 

 

 

1️⃣ 회원가입과 로그인 form의 input을 받을 auth router를 코딩합니다 (+ middleware)

 

- routes/auth.js

 

로그인, 로그아웃, 회원가입을 눌렀을 때 이동하는 /auth 라우터입니다. 회원가입에는 <form action="/auth/join">, 로그인에는 <form action"/auth/login> 따위가 있을 겁니다. 또, /auth는 db 정보를 가공하기 위해 거치는 곳이며 곧바로 다른 페이지로 이동합니다. /auth는 단순 처리를 위해 거쳐가는 라우터라고 생각하면 좋습니다.

 

 

회원가입부터 봅시다.

우선 동일한 아이디를 가진 유저가 있는지 탐색한후 받은 비밀번호를 bcrypt를 이용해 암호화하여 db에 저장합니다.

 

 

import express from "express";
import bcrypt from "bcrypt";
import passport from "passport";
import { isLoggedIn, isNotLoggedIn } from "./middlewares";
// User 테이블을 가져옵니다
import { User } from "../models";

const authRouter = express.Router();

// 회원가입을 처리합니다 POST /auth/join
authRouter.post("/join", isNotLoggedIn, async (req, res, next) => {
  // form을 통해 전달된 input 결과물을 가져옵니다
  const { email, nick, password } = req.body;
  try {
    // email이 중복되지는 않는지 확인합니다
    const exUser = await User.findOne({ where: { email } });
    // 중복되었다면 redirect로 돌려보냅니다.
    if (exUser) {
      req.flash("joinError", "이미 가입된 이메일입니다");
      return res.redirect("/join");
    } else {
      // 이메일이 중복되지 않았다면 가입시키기 위해 비밀번호를 암호화합니다
      // hash함수의 두번째 인자는 암호화의 복잡도이며 큰 수일수록 오래 걸립니다
      // 1초 정도 걸리게 console.time, console.timEnd를 찍어봅시다
      console.time("암호화에 걸리는 시간");
      const hash = await bcrypt.hash(password, 12);
      console.timeEnd("암호화에 걸리는 시간");
      await User.create({ email, nick, password: hash });
    }
    return res.redirect("/");
  } catch (error) {
    console.log(error);
    next(error);
  }
});

// 로그인을 처리합니다 POST /auth/login
authRouter.post("/login", isNotLoggedIn, (req, res, next) => {
  passport.authenticate("local", (authError, user, info) => {
    // authError, user, info에는 각각 done(에러, 성공, 실패) 정보가 담겨있습니다
    if (authError) {
      // 에러면 에러 핸들러로 보냅니다
      console.log(authError);
      return next(authError);
    }
    if (!user) {
      // 유저 정보가 없다는 것을 로그인 실패를 의미합니다
      req.flash("loginError", info.message);
      return res.redirect("/");
    }
    // 유저 정보가 있으면 성공이므로 req.login 시킨다.
    // 다만 여기서도 혹시 오류가 날 수도 있으니 에러 처리를 해준다
    return req.login(user, loginError => {
      if (loginError) {
        console.log(loginError);
        return next(loginError);
      }
    });
  })(req, res, next); // 미들웨어 내의 미들웨어에는 (req, res, next)를 붙입니다.
});

// 로그아웃을 처리합니다 GET /auth/logout
authRouter.get("/logout", isLoggedIn, (req, res) => {
  req.logout();
  req.session.destroy(); // 안해도 됨
  res.redirect("/");
});

export default authRouter;

 

 

 

- routes/middlewares.js

로그인 하지도 않았는데 로그아웃 페이지나 프로필 페이지에 접속할 수 있다면 오류를 발생시키게 될 것입니다. 이를 막기 위해 다음과 같은 미들웨어를 만들어 미들웨어로 포함 시켜주면 오류를 줄일 수 있습니다. 이와 같은 처리는 특수한 것이 아니라 passport.js를 사용함에 있어 일반적인 것입니다. 다들 한다는 말입니다.

 

예를 들어 프로필 페이지에는 다음과 같이 isNotLoggedIn을 삽입해줍니다

// 프로필 페이지
router.get("/profile", isLoggedIn, (req, res, next) => {
  res.render("profile", { title: "내 정보 - Twitter", user: null });
});
// 로그인이 된 사람만 다음 내용을 볼 수 있게 합니다.
export const isLoggedIn = (req, res, next) => {
  if (req.isAuthenticated()) {
    next();
  } else {
    res.status(403).send("로그인 필요");
  }
};

// 로그인이 안 된 사람만 다음 내용을 볼 수 있게 합니다.
export const isNotLoggedIn = (req, res, next) => {
  if (!req.isAuthenticated()) {
    next();
  } else {
    res.redirect("/");
  }
};

 

- routes/index.js

로그인이 되었다는 것을 보여줘야하므로 controller 함수에 전달하는 변수에 user를 추가합니다. 유저 정보를 사용하는 라우터는 모두 user:req.user를 통해서 유저 정보를 전달해줘야 합니다. 유저 정보를 사용할지 안 할지 모르겠다면 우선 다 넣고 보세요.

// 메인페이지
router.get("/", (req, res, next) => {
  res.render("main", {
    title: "Twitter",
    twits: [],
    // deserialize가 보내준 req.user
    user: req.user,
    loginError: req.flash("loginError")
  });
});

// 프로필 페이지
router.get("/profile", isLoggedIn, (req, res, next) => {
  res.render("profile", { title: "내 정보 - Twitter", user: req.user });
});

// 회원가입 페이지
router.get("/join", isNotLoggedIn, (req, res) => {
  res.render("join", {
    title: "회원가입 - Twitter",
    // deserialize에서 담아준 req.user
    user: req.user,
    joinError: req.flash("joinError")
  });
});


 

 

2️⃣ passport에 로컬 전략을 코딩합니다.

 

- /passport/index.js

import local from "./localStrategy";
import kakao from "./kakaoStrategy";
import { User } from "../models";

const user = {};

export default passport => {
  passport.serializeUser((user, done) => {
    // user에는 {id: 1, email:xx@mail.com} 등이 들어 있습니다.
    // 세션에 모두 저장하기에는 무거우므로 고유값(id)만 저장합시다
    // mysql에서 자동생성하는 건 id, mongoDB는 _id 입니다
    done(null, user.id);
  });
  passport.deserializeUser((id, done) => {
    // 세션에서 고윳값을 가지고 DB에서 유저의 정보를 찾아냅니다.
    // 찾아낸 정보는 req.user에 담아줍니다
    if (user[id]) {
      done(user[id]);
    } else {
      User.findOne({ where: id })
        .then(user => done(null, user))
        .catch(err => done(err));
    }
  });
  local(passport);
  kakao(passport);
};

 

 - /passport/localStrategy.js

로컬 전략을 사용할 경우의 논리적인 처리입니다. 폼에게 받은 input을 검증하고 암호화하여 User 모델에 추가합니다.

import { Strategy as LocalStorage } from "passport-local";
import bcrypt from "bcrypt";
// User 모델을 불러옵니다
import { User } from "../models";

export default passport => {
  passport.use(
    new LocalStorage(
      {
        //제출된 input의 name과 동일해야 합니다
        // urlencoded 미들웨어가 해석한 req.body.email, req.body.password를
        // usernameField, passwordField에 연결합니다.
        usernameField: "email",
        passwordField: "password"
      },
      async (email, password, done) => {
        // done(에러, 성공, 실패)
        // 빈 값은 done(null, false) 입니다
        try {
          const exUser = await User.findOne({ where: { email } });
          if (exUser) {
            // 비밀번호가 일치하면 로그인 성공 아니면 실패
            const result = await bcrypt.compare(password, exUser.password);
            if (result) {
              done(null, exUser);
            } else {
              done(null, false, { message: "비밀번호가 일치하지 않습니다." });
            }
          } else {
            // 이메일이 없으면 가입되지 않은 회원
            done(null, false, { message: "가입되지 않은 회원입니다." });
          }
        } catch (error) {
          console.log(error);
          done(error);
        }
      }
    )
  );
};

 

3️⃣ app.js와 연결합니다

 

app.js에는 다음과 같이 passport 패키지를 연결하고 사용합니다

// app.js
import passport from "passport";
import passportConfig from "./passport";

// passport/index.js 활용
passportConfig(passport);

// middleware 최하단에
app.use(passport.initialize());
app.use(passport.session());

 

이제 회원가입을 하고 그 정보로 로그인하는 것이 가능해졌다. mysql도 확인해보면 가입한 정보들이 쌓인 것을 볼 수 있다.

 

 

🚗 로그아웃

 

passport에서 마련해준 req.logout을 실행하면, 쿠키, 세션 삭제 등 모든 일을 다 해준다. 소셜 로그인도 마찬가지이다. 꿀!

globalRouter.get("/logout", (req, res) => {
  req.logout();
  res.redirect(routes.home);
};);

darren, dev blog
블로그 이미지 DarrenKwonDev 님의 블로그
VISITOR 오늘 / 전체