이 블로그는 광고 클릭 수익으로 운영됩니다!
괜찮으시다면 광고 차단을 풀어주세요 ㅠㅠ

게시글

강좌10 - NodeJS - 2년 전 등록 / 6일 전 수정

Passport로 회원가입 및 로그인하기

조회수:
0
이 블로그는 광고 클릭 수익으로 운영됩니다!
괜찮으시다면 광고 차단을 풀어주세요 ㅠㅠ
이 블로그는 광고 클릭 수익으로 운영됩니다!
괜찮으시다면 광고 차단을 풀어주세요 ㅠㅠ

안녕하세요. 이번 시간에는 Passport.js 패키지를 사용해 회원가입과 로그인을 구현하겠습니다! 이 강좌는 ReactJS 리덕스 강좌와도 이어집니다. 

npm install --save passport passport-local express-session

일단 두 패키지를 설치합니다. passport는 한글로 여권입니다. 이름처럼 자신의 웹사이트에 방문할 때 여권같은 역할을 합니다. 로그인을 쉽게 할 수 있게 도와줍니다. 이름 잘 지었죠? passport-local은 로그인을 직접 구현할 때 사용됩니다. 이외에 passport-google-oauth, passport-facebook, passport-twitter, passport-kakao, passport-naver 같이 SNS 계정을 통해서 바로 로그인할 수 있는 패키지도 있습니다. 제 블로그의 로그인 창을 보면 위의 패키지들을 사용한 것을 알 수 있습니다.

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

이렇게 설치했으니 passport를 사용해보겠습니다. 이게 우리나라에 들어올 수 있게 해주는 여권입니다.

passport.js

const passport = require('passport');
const LocalStrategy = require('passport-local').Strategy;
const Users = require('./user');

module.exports = () => {
  passport.serializeUser((user, done) => { // Strategy 성공 시 호출됨
    done(null, user); // 여기의 user가 deserializeUser의 첫 번째 매개변수로 이동
  });

  passport.deserializeUser((user, done) => { // 매개변수 user는 serializeUser의 done의 인자 user를 받은 것
    done(null, user); // 여기의 user가 req.user가 됨
  });

  passport.use(new LocalStrategy({ // local 전략을 세움
    usernameField: 'id',
    passwordField: 'pw',
    session: true, // 세션에 저장 여부
    passReqToCallback: false,
  }, (id, password, done) => {
    Users.findOne({ id: id }, (findError, user) => {
      if (findError) return done(findError); // 서버 에러 처리
      if (!user) return done(null, false, { message: '존재하지 않는 아이디입니다' }); // 임의 에러 처리
      return user.comparePassword(password, (passError, isMatch) => {
        if (isMatch) {
          return done(null, user); // 검증 성공
        }
        return done(null, false, { message: '비밀번호가 틀렸습니다' }); // 임의 에러 처리
      });
    });
  }));
};

passport는 독특하게 Strategy(전략)이라는 것을 사용합니다. 모든 passport의 플러그인들은 사용하려면 전략을 짜 주어야 합니다. 위의 경우는 local 로그인의 경우의 전략입니다. 일단 serializeUser과 deserializeUser은 잠시 뒤에 설명할 거고요. LocalStrategy 부분을 보시죠.

usernameField<strong>와 </strong>passwordField는 어떤 폼 필드로부터 아이디와 비밀번호를 전달받을 지 설정하는 옵션입니다. 위의 경우는 body에 데이터가 { id: 'zerocho', pw: 'pswd' } 이렇게 오면 뒤의 콜백 함수의 id 값이 zerocho, password 값이 pswd가 됩니다. session은 말 그대로 세션을 사용할 지 안 할 지를 선택합니다. passReqToCallback은 true로 해두면 뒤의 콜백이

(req, id, password, done) => {};

으로 바뀝니다. id 매개변수 앞에 req 매개변수가 추가되었습니다. req를 통해서 express의 req 객체에 접근할 수 있습니다. 저는 보통 passReqToCallback을 true로 해 두어 req 객체를 passport 인증 시 활용합니다.

아이디와 비밀번호 값이 들어오면 뒤의 콜백 함수가 실행되는데, 내용을 보면 id로 유저를 찾은 후, 유저가 없으면 존재하지 않는 아이디라고 에러를 보냅니다. 유저가 있다면, 이제 비밀번호를 비교해서(비밀번호를 비교하는 부분은 아래 user.js 파일의 comparePassword를 참조하세요), 비교 과정에서 서버 에러가 나면 done(findError)로 에러를 리턴하고, 비밀번호가 맞을 경우 done(null, user);로 user 객체를 전송해주고, 틀렸을 경우는 비밀번호가 틀렸다고 done(null, false, { message: '에러메시지' })로 메시지를 전송합니다.

done이 인자를 세 개나 받아 헷갈릴 수도 있는데 다음과 같습니다. 첫 번째 인자는 DB조회 같은 때 발생하는 서버 에러를 넣는 곳입니다. 무조건 실패하는 경우에만 사용합니다. 두 번째 인자는 성공했을 때 return할 값을 넣는 곳이고요. 성공했으면 당연히 첫 번째 인자는 null이어야겠죠? 에러가 있으면 안 되니까요. 세 번째 인자는 언제 사용하나면, 사용자가 임의로 실패를 만들고 싶을 때 사용합니다. 첫 번째 인자를 사용하는 경우는 서버에서 에러가 났을 때 무조건 실패하는 경우라고 했죠. 세 번째 인자는 위에서 비밀번호가 틀렸다는 에러를 표현하고 싶을 때 사용하면 됩니다. 이것은 서버 에러도 아니고, 사용자가 임의로 만드는 에러이기 때문에, 직접 에러 메시지도 써주는 겁니다.

다음은 많은 분들이 헷갈려하는 serializeUser와 deserializeUser입니다.

serializeUser은 방금 전에 로그인 성공 시 실행되는 done(null, user);에서 user 객체를 전달받아 세션(정확히는 req.session.passport.user)에 저장합니다. 세션이 있어야 페이지 이동 시에도 로그인 정보가 유지될 수 있습니다. deserializeUser은 실제 서버로 들어오는 요청마다 세션 정보(serializeUser에서 저장됨)를 실제 DB의 데이터와
비교합니다. 해당하는 유저 정보가 있으면 done의 두 번째 인자를 req.user에 저장하고, 요청을 처리할 때 유저의 정보를 req.user를 통해서 넘겨줍니다. 하지만 위의 예시에서는 그냥 아무런 처리과정 없이 (물론 세션에는 저장이 됩니다) 그냥 넘겨주도록 했습니다.

serializeUser에서 done으로 넘겨주는 user가 deserializeUser의 첫 번째 매개변수로 전달되기 때문에 둘의 타입이 항상 일치해야 합니다. 만약 serializeUser에서 id만 넘겨줬다면 deserializeUser의 첫 번째 매개변수도 id를 받아야 하고요. id만 있으면 그 자체로는 req.user을 만들 수 없기 때문에 User.findById(id) 메소드로 완전한 user 객체를 만들어서 done을 해주면 됩니다. 아래와 같이 주로 사용합니다.

passport.serializeUser((user, done) => { // Strategy 성공 시 호출됨
  done(null, user._id); // 여기의 user._id가 req.session.passport.user에 저장
});
passport.deserializeUser((id, done) => { // 매개변수 id는 req.session.passport.user에 저장된 값
  User.findById(id, (err, user) => {
    done(null, user); // 여기의 user가 req.user가 됨
  });
});

이 두 메소드는 꼭 있어야 passport가 작동합니다. 반드시 넣어주셔야 합니다.

user.js

const mongoose = require('mongoose');

const userSchema = new mongoose.Schema({
  id: String,
  password: String
});

userSchema.methods.comparePassword = (inputPassword, cb) => {
  if (inputPassword === this.password) {
    cb(null, true);
  } else {
    cb('error');
  }
};

module.exports = mongoose.model('users', userSchema, 'users');

몽구스를 사용하여 스키마를 만들고 비밀번호 비교 메소드 comparePassword도 만들었습니다. 현재 코드에는 비밀번호를 평문 그대로 저장하고, 비교할 때도 평문끼리 비교하고 있는데, 실제 배포 단계에서는 절대로 이래서는 안 됩니다. 이것에 관한 강좌로 암호화 강좌 가 있습니다. 서버에는 passport를 모듈을 불러와 사용할 수 있도록 코드를 추가해줍니다.

server.js

const express = require('express');
const path = require('path');
const session = require('express-session'); // 세션 설정
const db = require('./db.js');
const route = require('./route.js');
const passport = require('passport'); // 여기와
const passportConfig = require('./passport'); // 여기
const app = express();
app.set('view engine', 'pug');
app.set('views', path.join(__dirname, 'html'));
app.use(session({ secret: '비밀코드', resave: true, saveUninitialized: false })); // 세션 활성화
app.use(passport.initialize()); // passport 구동
app.use(passport.session()); // 세션 연결
db();
passportConfig(); // 이 부분 추가
app.use(express.static(path.join(__dirname, 'html')));
app.use('/', route);
app.listen(8080, () => {
  console.log('Express App on port 8080!');
});

가운데 session을 설정하는 session(), passport.initialize(), passport.session() 이 부분을 잊지 말고 넣어주셔야 합니다. 마지막으로 route.js에 로그인을 처리하는 라우트를 추가해줍니다.

route.js

const passport = require('passport');
...
router.post('/login', passport.authenticate('local', {
  failureRedirect: '/'
}), (req, res) => {
  res.redirect('/');
});

이제 /login으로 post 요청을 보내면(로그인 html은 직접 만들어 보세요. action이 '/login'이고 method가 'post', 각각의 input name이 id와 pw인 form을 만들면 됩니다) passport에서 local에 대한 인증 작업을 시작합니다. LocalStrategy를 사용해서요. 실패할 경우와 성공할 경우 어디로 돌려보낼 지 각각 failureRedirect와 res.redirect에 위치를 지정할 수 있습니다.

후, 이제 로컬 로그인이 끝났네요. 다음 시간에는 passport로 facebook 로그인을 만드는 것을 알아볼까요? 로컬 로그인보다는 간단합니다.

투표로 게시글에 관해 피드백을 해주시면 많은 도움이 됩니다. 오류가 있다면 어떤 부분에 오류가 있는지도 알려주세요! 잘못된 정보가 퍼져나가지 않도록 도와주세요.
Copyright © 2016- 무단 전재 및 재배포 금지

댓글

6개의 댓글이 있습니다.
6일 전
server.js파일에 보면,app.use(passport.initialize()); // passport 구동
app.use(passport.session()); // 세션 연결

이 두부분이 계속 passport.initialize is not a function 또는 passport.session is not a function 이라는 오류가 계속 반복됩니다. 계속 머리굴려봤는데도 안됩니다.
환경은 ubuntu 16.04 lts입니다.
6일 전
수정했습니다. 감사합니다.
4달 전
일단 빠른 댓글 감사합니다.

저는 웹 페이지는 하지 않고 모바일에서만 동작하는 서버를 만들려고 하는데 그럼 passport 모듈을 통한 인증은 사용할 수 없는건가요? 토큰 방식을 찾아봐도 jwt만 나오는데 jwt는 서버에서 강제로 토큰을 만료 시킬 수 없어서 다른 토큰 방식을 찾고 있습니다. 혹시 모바일에서의 인증을 위한 모듈 있을까요? 조언 부탁드립니다.ㅜㅜ
4달 전
passport-google-token이나 passport-facebook-token같이 oauth2 방식의 토큰을 적용하셔야 할 것 같습니다. 굳이 passport를 사용하려고 하신다면요.
4달 전
안녕하세요. 글 잘 읽어봤습니다. 제가 iOS개발자라 서버쪽을 잘 모르고 처음 node js를 접해봐서 질문드립니다.
'/login' 이 로그인 api가 호출되면
router.post('/login', passport.authenticate('local', {
failureRedirect: '/'
}), (req, res) => {
res.redirect('/');
});

위의 authenticate 코드를 통해서

passport.use(new LocalStrategy({ // local 전략을 세움
usernameField: 'id',
passwordField: 'pw',
session: true, // 세션에 저장 여부
passReqToCallback: false,
}, (id, password, done) => {
Users.findOne({ id: id }, (findError, user) => {
if (findError) return done(findError); // 서버 에러 처리
if (!user) return done(null, false, { message: '존재하지 않는 아이디입니다' }); // 임의 에러 처리
return user.comparePassword(password, (passError, isMatch) => {
if (isMatch) {
return done(null, user); // 검증 성공
}
return done(null, false, { message: '비밀번호가 틀렸습니다' }); // 임의 에러 처리
});
});
}));

이 부분으로 넘어가는 건가요?
그리고 디비에서 정상적으로 이메일을 찾으면 return done(null, user); 을 호출하고
passport.serializeUser(function(user, done) {
done(null, user.id);
});
이 부분을 통해 세션을 저장하는건가요?

근데 로그인 한 후에 어떻게 클라이언트와 세션을 유지하는 건가요? 클라이언트에서는 다른 api를 사용할 때 어떤식으로 해야하는지 모르겠습니다.
제가 어설프게나마 알고있는 방법으로는 로그인을 하게되면 서버에서는 access token을 만들어서 디비에 저장하고 이 토큰을 클라이언트에 응답으로 전달해서 그 이후에 클라이언트는 다른 api를 사용할 때 이 토큰을 보내 서버에서는 이 토큰이 유효한지 보고 유효하다면 해당 api에 대한 응답을 해주고.. 이런걸 생각했는데
이 게시물이 제가 생각한 부분과 맞는지 모르겠습니다. 동일한 건데 제가 이해를 못한건지 아님 다른 부분인지 궁금합니다. 맞다면 어떤 흐름으로 로직이 흘러가는지 알고싶습니다. 감사합니다.
4달 전
네 전략 부분으로 넘어갑니다. serializeUser로 세션에 유저 아이디만 따로 저장하고요. 나중에 요청이 왔을 때 디비에서 유저 아이디로 유저 객체를 불러옵니다.

클라이언트(웹 브라우저)에 세션 아이디 정보를 가진 쿠키가 심어져있어 매 요청시 쿠키를 서버로 같이 보내 서버에서 세션 유무와 만료 여부를 체크합니다. 액세스 토큰 방식은 위의 방식과는 별개입니다. 현재 쿠키가 액세스 토큰을 대체한다고 생각하시면 됩니다.
6달 전
음 NodeJs공부하는데 도움이 많이되고 있습니다~ 감사합니다. 저 질문이.. 제목에 회원가입이 있어 가입처리 부분에 관련한 내용이 있는 듯한데 안보이네용..
6달 전
로컬 회원가입은 없습니다. 사실 암호화 강좌를 먼저 썼어야 그 다음에 회원가입 구현을 하는데 순서 착오때문에 뺐습니다 ㅠㅠ
7달 전
초보자도 이해하기 쉽게 설명해주셔서 감사합니다.
9달 전
app.use(session({ secret: '비밀코드', resave: true, saveUninitialized: false }); 이부분에서 마지막 ')' 빠졌습니다. app.use(session({ secret: '비밀코드', resave: true, saveUninitialized: false })); 이런식으로 closing이 되어야 오류가 나지 않습니다.
9달 전
감사합니다~