* 이 글은 공부한 것을 바탕으로 정리한 내용이기 때문에 부족함이 있을 수 있으며, 제 개인 프로젝트 환경에서 진행한 내용이기 때문에 설명에 다소 차이가 있을 수 있습니다. Template engine은 Handlebars를, DB, Session Store는 MySQL을 활용했고, 일회성 메세지를 위한 flash 모듈도 사용했습니다. 로그인 Form은 Bootstrap의 Modal 양식을 가져와 진행하였습니다.
http://www.passportjs.org/

PassportNode.js에서 제공하는 미들웨어이며, 인증 절차를 편하고, 안전하게 할 수 있도록 도와주는 역할을 한다.

그렇지만 마냥 편하다고 얘기할 수는 없는 게 처음 쓸 때 흐름이 꽤나 헷갈린다. 따라서 이 글에서는 Passport의 전략 중에 가장 Basic한 느낌이 드는 Local Strategy (ID와 pw로 인증하는 방식) 의 흐름에 대해 다뤄보려고 한다.

Passport 및 필요한 모듈 설치

npm install -s passport
npm install -s passport local
npm install -s connect-flash
npm install -s express-session
npm install -s express-mysql-session

Passport 환경 Setting

app.js

const passport = require('passport')

, LocalStrategy = require('passport-local').Strategy;

const session = require('express-session')
const flash = require('connect-flash');
const MySQLStore = require('express-mysql-session')(session)
const mysql = require('mysql');

app.use(session({
    secret: 'asadlfkj!@#!@#dfgasdg',
    resave: false,
    saveUninitialized: true,
    store: new MySQLStore({
        host:'localhost',
        port: 3306,
        user: 'root',
        password: '1234',
        database: 'todo'
    }),
  }))

app.use(flash());
app.use(passport.initialize());
app.use(passport.session());

const db = mysql.createConnection({
    host     : 'localhost',
    user     : 'root',
    password : 'softkei7&',
    database : 'todo'
  });
db.connect();

connect-flash는 필수적인 미들웨이는 아니지만 나는 로그인 및 회원가입에 실패했을 때 일회성 메세지를 보내기 위해, 그리고 Database는 MySQL을 사용했다. 굳이 MySQL을 사용할 필요없이 그냥 객체나 파일을 활용해도 무관하다. flash 미들웨어는 express-session을 사용하기 때문에 꼭 이보다 뒤에 위치해야만 한다. app.use 를 통해 passport를 사용함과 동시에 내부적으로 session을 쓰겠다고 선언했다. passport를 사용하기 위해서는 꼭 app.use(passport.initialize())passport를 초기화시켜줘야 한다. 이로 인해 user 정보가 req.user로 들어가게된다. app.use(passport.session()) 또한 로그인을 지속시켜 인증된 사용자에게 그에 맞는 UI 등을 보여주기 위해서는 필수적으로 사용해줘야한다.

세션 스토어는 express-mysql-session을 이용해 MySQL에 따로 저장했다. 이 또한 그냥 일반 세션 스토어를 사용해도 무관하다.

MySQL Workbench의 sessions table

login-form(modal)

<!-- Login Modal -->
<div class="modal modal-center fade" id="login">
  <div class="modal-dialog modal-center">
    <div class="modal-content">

      <!-- Modal Header -->
      <div class="modal-header text-center">
        <h4 class="modal-title">Login</h4>
        <button type="button" class="close" data-dismiss="modal">&times;</button>
      </div>

      <!-- Modal body -->
      <div class="modal-body">
        <input id="login_username" placeholder="username" /><br><br>
        <input id="login_password" placeholder="password" type="password"/>
        <p class="auth-flash-massage"></p>
      </div>

      <!-- Modal footer -->
      <div class="modal-footer">
        <button type="button" id="login_button" class="btn btn-outline-info" >Login</button>
        <button type="button" id="login_register_button" data-target="#register" data-toggle="modal" class="btn btn-outline-info" >Register</button>
      </div>
    </div>
  </div>
</div>

jQuery.js

function login() {
    var username = $('#login_username').val()
    var password = $('#login_password').val()
    $.ajax({
        url: '/auth/login',
        type: 'post',
        data:{
            username:username,
            password:password
        },
        success: function() {
            $('#login').modal("hide");
            location.reload()
        },
        error: function(req) {
            $('.auth-flash-massage').text(req.responseText)
        }
    })
}

app.js

app.post('/auth/login',
  passport.authenticate('local', { 
      successRedirect: '/', 
      failureRedirect: '/auth/login/fail', 
      failureFlash:true
  }));

app.post의 두번째 인자로 passport.authenticate 함수를 넣어 local 전략을 쓰겠다고 선언한다. jQuery.js에서 ajax를 통해 usernamepassword 같은 사용자가 전송한 데이터가 넘어오는데 이는 아래에서 볼 수 있겠지만 passport.authenticate의 내부 로직에 의해 new LocalStrategy를 사용해 인증로직을 구현할 수 있도록 도와준다. 두번째 인자로는 성공하면 root('/')로 리다이렉트하고, 실패하면 /auth/login/fail로 리다이렉트 할 수 있도록 만들어주는 객체가 들어온다. 나는 추후에 실패했을 경우 이 api를 통해 error를 flash 메세지로 넘기는 라우팅 로직을 작성할 것이다. 따라서 failureFlashtrue로 설정했다.

인증 로직 구현

app.js

passport.use(new LocalStrategy(
    function(username, password, done) {
        db.query("SELECT * FROM users WHERE username = ?;", username, (err,user)=>{
            if(err) {
                return done(err);
            }
            if(user.length===0) {
                return done(null,false, {message: '유저가 존재하지 않습니다.'})
            }
            if(password!==user[0].password){
                return done(null,false, {message:'잘못된 비밀번호입니다.'})
            }
            return done(null,user)
        })
    }
));

로그인의 성공여부를 판단하는 로직이다. passport.use로 위에서 언급했듯이 new LocalStrategy를 사용하면 이곳에서 인증이 이루어질 수 있도록 usernamepassword를 각각 1,2번 인자로 넘겨준다. 이렇게 하면 실제로 사용자가 login form(혹은 ajax)에서 데이터를 전송할 때마다 new LocalStrategy 안에 있는 콜백함수가 호출되도록 약속 되어있다. 그렇게 들어오는 로그인 정보를 우리가 가지고 있는 실제 데이터(나는 MySQL에 있는 데이터를 가져와 비교했다.)와 비교하여 맞다면 콜백함수의 세번째 인자로 넘어온 함수 done에 두번째 인자로 false가 아닌 사용자의 실제 데이터인 user를 주입해주면 된다.

여기서 주의할 점은 login form의 input 태그의 name 값(혹은 ajax에서 넘겨주는 데이터)가 꼭 usernamepassword로 설정이 되어있어야 한다. 만약 비교할 데이터가 email pwd 같은 이름으로 되어있다면 이를 new LocalStrategy 의 첫번째 인자로 아래와 같이 객체를 넣어줘야한다.
passport.use(new LocalStrategy({
    usernameField: 'email',        // username field 값 셋팅
    passwordField: 'pwd'        // password field 값 셋팅
}, function(username, password, done) {
        ...
    }
));

하지만 번거로우니 애초에 넘어오는 데이터들을 username password로 설정하는게 맘 편하다고 조심스레 생각해본다.

세션 정보 확인

passport.serializeUser(function(user, done) {
    done(null, user[0].username);
});

passport.deserializeUser(function(username, done) {
    db.query("SELECT * FROM users WHERE username = ?;", username, (err,user)=>{
        done(null, user);  
    })
});

자 다시 돌아와서 우리는 new LocalStrategy 안의 콜백함수의 세번째 인자로 넘어온 done 함수에 사용자의 실제 데이터를 넘겨주었다. 그 데이터는 passport.serializeUser의 콜백함수를 호출하도록 약속되어 있으며, 그 콜백함수는 첫번째 인자로 아까 우리가 넘겨준 데이터인 user를 주도록 약속되어 있다. 우리는 이 데이터에서 session에서 사용자의 식별자(id) 역할을 할 수 있는 username를 추출하여 done 함수의 두번째 인자로 넘겨주면 식별자 값인 usernamesession 데이터의 passport 안에 있는 user의 값으로 들어간다.

session 데이터의 passport 안에 있는 user 정보

즉, passport.serializeUser로그인에 성공했을 때 로그인에 성공했다는 사실을 세션 스토어에 저장하는 기능을 한다. 로그인에 성공하면 serializeUser가 딱 한 번만 호출이 되는 것이다.

그렇다면 passport.deserializeUser는 어떤 일을 하는가? 일단 콜백함수의 첫번째 인자로 넘어오는 값은 serializeUser에서 넘어온 식별자 값이 된다. 즉 여기서는 username이다. 사용자가 로그인에 성공한 후 각각의 페이지에 방문할 때마다 우리는 그 사람이 로그인한 사용자인지 아닌지를 체크해야 하는데 그 일을 deserializeUser가 한다. 즉 deserializeUser저장된 데이터를 기준으로 해서 우리가 필요한 정보를 조회할 때 사용하는 것이다. 위 코드에서는 식별자 값인 username을 활용하여 MySQL에서 유저 정보를 가져와서 done의 두번째 인자로 다시 넘겨줬는데 이는 라우팅 로직을 작성할 때 req.user로 받을 수 있다.

세션 정보를 활용한 로그인 유저 식별 및 권한 설정


app.js

app.get('/',(req,res)=>{
    var isLogined, username
    if(req.user) {
        var isLogined = true;
        var username = req.user[0].username;
    }
    res.render('index',{
        isLogined,
        username
    })
})

우리는 위와 같이 이 데이터를 이용해 로그인된 유저인지 아닌지를 식별할 수 있으며, 그에 맞게 UI를 제공할 수 있다.

로그아웃

로그아웃 로직은 간단하다.

app.js

app.get('/auth/logout', (req,res)=>{
    req.logout()
    res.redirect('/')
})

마치며...

Passport로 Local 전략의 로그인 기능을 구현하면서 힘들었던 점들이 여럿 있었지만 그 중 하나는 어느정도 이해하고, 알고 있다고 생각했으나 머릿속으로 그려보려고 하면 엉망이었다는 것이다. 이번 기회에 정리하는 시간을 가져보면서 얕게 사용법만 알고 있던 수준에서 각각의 코드들이 어떤 기능을 하는 지와 세션에 대해서 보다 더 알아볼 수 있어서 큰 도움이 되었다. 추후 Modal 창에 flash message를 띄우는 것도 정리해봐야겠다.

참고한 사이트들

https://opentutorials.org/course/3402

https://velog.io/@ground4ekd/nodejs-passport

https://bcho.tistory.com/920

https://backback.tistory.com/340

https://cheese10yun.github.io/Passport-part1/