보안 전공생의 공부

게시판 만들기 / login 기능 - passport package 본문

WEB/Node

게시판 만들기 / login 기능 - passport package

수잉 2021. 12. 7. 14:34

passport package를 사용해 login 기능을 만든다.

 

· passport : node.js에서 user authentication(사용자 인증, login)을 만들기 위해 사용하는 package

                단독으로 사용 X , passport strategy package와 함꼐 사용해야 함

· passport strategy : 구체적인 인증 방법을 구현하는 package

 

-> 인증 방법별로 수십가지(Facebook strategy, Twitter strategy, Naver strategy 등)가 존재하기 때문에

package가 나눠지게 되었다.

실제 한 사이트에서 사용하는 strategy는 이 중 몇 개밖에 안된다.

즉, 사이트에 필요한 인증밥법만 설치하기 위해 package를 분리한 것이다.

 


[로그인 기본 원리]

 

  server와 client 간의 정보교환 : 단발성(어떤 일이 단 한 번 만으로 그치는성질)

  사용자의 browser(client)d에서 주소 입력 or link가 click 되면 server로 요청(request) 전달

                                                                     -> server는 요청에 맞는 결과를 응답(response)

  => server-client간의 통신은 연결을 유지 X

       client를 구별하기 위해서는 각각의 request에 고유한 식별코드가 필요함

 

  이 때, 식별코드는 사이트에 처음 접속한 순간 생성되어 client의 브라우저에 저장되고, server에 요청할 때마다 server로 전달됨 / server에서는 식별코드가 session에 저장되어 어느 client로부터 요청이 오는지 구별할 수 있게 됨

 

 로그인 성공하면 server의 session에 기록되고, 다음번 request부터는 로그인한 상태로 인식됨

 

  로그인 == DB에 이미 등록된 user을 찾는 것

 - serialize(직렬화) : 로그인 시, DB로부터 user을 찾아 session에 user 정보의 일부(전부)를 등록하는 것

 - deserialize(역직렬화) :  session에 등록된 user 정보로부터 해당 user를 oject로 만드는 것 -> server에 요청 올 때마다 거치는 과정

https://www.geeksforgeeks.org/serialization-in-java/


1. 필요한 package를 설치한다.

 

2. 코드 수정, 추가

//index.js

const express=require('express')//모듈 가져오기
const mongoose=require('mongoose')
const methodOverride = require('method-override')
const flash=require("connect-flash")
const session=require("express-session")
const passport=require("./config/passport")
const app = express()//어플리케이션 생성

// DB setting
mongoose.connect(process.env.MONGO_DB);
const db = mongoose.connection; 

db.once('open', function(){
  console.log('DB connected');
})
db.on('error', function(err){
  console.log('DB ERROR : ', err);
});

//other setting
app.set('view engine','ejs')//템플릿 엔진
app.use(express.json());
app.use(express.static('public'));
app.use(express.urlencoded({extended:true}));
app.use(methodOverride('_method'));
app.use(flash());
app.use(session({secret:'MySecret', resave:true, saveUninitialized:true}));
app.use(passport.initialize())
app.use(passport.session())

//custom middlewares
app.use(function(req,res,next){
  res.locals.isAuthenticated = req.isAuthenticated();
  res.locals.currentUser = req.user;
  next();
})

//routes
app.use('/', require('./routes/home'));
app.use('/posts', require('./routes/posts'))
app.use('/contacts', require('./routes/contacts'));
app.use('/home', require('./routes/home'))
app.use('/users', require('./routes/users'))

//port setting
const server=app.listen(3000, ()=>{
  console.log('Start Server : localhost:3000')
})
const passport=require("./config/passport")

: passport 말고 config/passport module을 변수 passport에 담았다.

 passport와 passport-local package는 index.js에 require되지 않고, config의 passport.js에서 require된다.

 

app.use(passport.initialize())

: passport를 초기화시키는 함수

app.use(passport.session())

: passport와 session을 연결해주는 함수

더보기

- session을 이용하기 위해 앞서 user error처리를 하기위해 작성한 아래 코드가 반드시 필요함

const session=require("express-session")
app.use(session({secret:'MySecret', resave:true, saveUninitialized:true}));

 

//custom middlewares
app.use(function(req,res,next){
  res.locals.isAuthenticated = req.isAuthenticated();
  res.locals.currentUser = req.user;
  next();
})

: app.use에 함수를 넣은 것 = middleware

app.use에 있는 함수는 request가 올 때마다 route에 상관없이 무조건 해당 함수가 실행된다.

위치가 중요하다 ( ∵ 위에 있는 것부터 순차적으로 실행됨)

route에 들어가는 함수와 동일하게 3개의 parameter( req, res, next )를 가진다.

함수 안에 반드시 next()를 넣어야 다음으로 넘어갈 수 있다.

 

- req.isAuthenticated() - passport에서 제공하는 함수 (현재 로그인이 되어있으면 true, 아니면 false를 return)

- req.user - passport에서 추가하는 항목. 로그인 되면 session으로부터 user를 deserialize하여 생성된다.

- res.locals - 위의 두가지를 담고 있다. res.lcals에 담긴 변수는 ejs에서 바로 사용가능

 

res.locals.isAuthenticated : ejs에서 user의 로그인 유무를 확인하는 데 사용됨

res.localse.currentUser : 로그인된 user의 정보를 불러오는데 사용됨

 

[error]

error

이러한 error가 발생하였는데, custom middlewares 코드의 위치 문제때문이였다.

routes 뒤에 custom middlewares를 위치시켰더니 위의 error가 발생하여,

custom middlewares를 routes 앞으로 옮겼더니 해결되었다.

 

미들웨어의 로드 순서가 중요하다.

routes에 있는 루트 경로에 대한 라우팅(app.use('/', require('./routes/home'));) 이후에 custom middlewares가 로드되면, 

루트 경로의 라우트 핸들러가 요청-응답 주기를 종료하므로 요청은 절대 custom middlewares로 도달하지 못하게 되며

위와 같은 error가 발생한다.

 

따라서 앞으로도 index.js에 추가할 middleware는 routes 앞에 위치시켜야 한다.

(route에 상관없이 무조건 해당 함수가 실행되어야 하므로) 

 

참조 : https://expressjs.com/ko/guide/writing-middleware.html

 

Express 앱에서 사용하기 위한 미들웨어 작성

Express 앱에서 사용하기 위한 미들웨어 작성 개요 미들웨어 함수는 요청 오브젝트(req), 응답 오브젝트 (res), 그리고 애플리케이션의 요청-응답 주기 중 그 다음의 미들웨어 함수 대한 액세스 권한

expressjs.com

//config/passport.js

const passport = require('passport');
const LocalStrategy = require('passport-local').Strategy;
const User = require('../models/User');

// serialize & deserialize User
passport.serializeUser(function(user, done) {
  done(null, user.id);
});
passport.deserializeUser(function(id, done) {
  User.findOne({_id:id}, function(err, user) {
    done(err, user);
  });
});

// local strategy
passport.use('local-login',
  new LocalStrategy({
      usernameField : 'username', 
      passwordField : 'password', 
      passReqToCallback : true
    },
    function(req, username, password, done) {
      User.findOne({username:username})
        .select({password:1})
        .exec(function(err, user) {
          if (err) return done(err);

          if (user && user.authenticate(password)){
            return done(null, user);
          }
          else {
            req.flash('username', username);
            req.flash('errors', {login:'The username or password is incorrect.'});
            return done(null, false);
          }
        });
    }
  )
);

module.exports = passport;
const LocalStrategy = require('passport-local').Strategy;

: strategy들은 대부분 require 다음에 .Strategy가 붙는다 (.Strategy없이 사용해도 되는 것들도 있음)

 

passport.serializeUser(function(user, done) {
  done(null, user.id);
});

: login시 DB에서 발견한 user를 어떻게 session에 저장할지 정하는 부분

 user정보 전체를 session에 저장할 수 있지만,

 session에 저장되는 정보가 과다하면 사이트 성능이 하락할 수 있고, 전체 user정보가 session에 저장되어 있으므로  user object를 변경하면(회원정보수정)  해당 부분을 변경해줘야 하는 문제들이 있으므로

 user의 id만 session에 저장한다.

passport.deserializeUser(function(id, done) {
  User.findOne({_id:id}, function(err, user) {
    done(err, user);
  });
});

: request시에 session에서 어떻게 user object를 만들지를 정하는 부분

 매번 request마다 user정보를 DB에서 새로 읽어오는데, user가 변경되면 바로 변경된 정보가 반영된다.

+) done함수의 첫번째 parameter는 항상 error를 담는다. error가 없다면 null을 담는다.

 

user 정보를 전부 session에 저장하여 DB접촉 감소 -> session에 저장되는 정보가 과다해 사이트 성능 하락할 수 있음

request마다 user를 DB에서 읽어와서 데이터의 일관성을 확보 -> 매 request마다 DB에서 user을 읽어와야 함

=> 상황에 맞게 선택

 

usernameField : 'username',
passwordField : 'password',

로그인 form의 username과 password항목의 이름을 넣는다.

 

   User.findOne({username:username}) //DB에서 해당 user를 찾음
        .select({password:1})
        .exec(function(err, user) {
          if (err) return done(err);

          if (user && user.authenticate(password)){
            return done(null, user);
          }
          else {
            req.flash('username', username);
            req.flash('errors', {login:'The username or password is incorrect.'});
            return done(null, false);
          }
        });

로그인 시에 호출되는 함수

user model

user model에서 정의한 user.authenticate 함수를 사용해 입력받은 password와 DB에 저장된 해당 user의 password(user.password) hash를 비교해서 값이 일치하면 해당 user를 done에 담아 return한다.

그렇지 않으면 username flash, error flash를 생성하고 false를 done에 담아 return한다. 

 

 

//routes/home.js

const express=require('express')
const router=express.Router()
const passport=require('../config/passport')

router.get('/', function(req, res){
  res.render('home/main');
});

router.get('/about', function(req, res){
  res.render('home/about');
});

router.get('/login',function(req,res){
  const username=req.flash('username')[0];
  const errors=req.flash('errors')[0]||{};
  res.render('home/login',{
    username:username,
    errors:errors
  })
})

router.post('/login',
  function(req,res,next){
    const errors = {};
    const isValid = true;

    if(!req.body.username){
      isValid = false;
      errors.username = 'Username is required!';
    }
    if(!req.body.password){
      isValid = false;
      errors.password = 'Password is required!';
    }

    if(isValid){
      next();
    }
    else {
      req.flash('errors',errors);
      res.redirect('/login');
    }
  },
  passport.authenticate('local-login', {
    successRedirect : '/posts',
    failureRedirect : '/login'
  }
));

// Logout
router.get('/logout', function(req, res) {
  req.logout();
  res.redirect('/');
});

module.exports=router;
const passport=require("./config/passport")

: passport 말고 config/passport module을 변수 passport에 담았다.

 passport와 passport-local package는 home.js에 require되지 않고, config의 passport.js에서 require된다

 (index.js와 동일)

 

router.get('/login',function(req,res){
  const username=req.flash('username')[0];
  const errors=req.flash('errors')[0]||{};
  res.render('home/login',{
    username:username,
    errors:errors
  })
})

login view를 보여주는 route

 

router.post('/login',
  function(req,res,next){
    const errors = {};
    const isValid = true;

    if(!req.body.username){
      isValid = false;
      errors.username = 'Username is required!';
    }
    if(!req.body.password){
      isValid = false;
      errors.password = 'Password is required!';
    }

    if(isValid){
      next();
    }
    else {
      req.flash('errors',errors);
      res.redirect('/login');
    }
  },
  passport.authenticate('local-login', {
    successRedirect : '/posts',
    failureRedirect : '/login'
  }
));

login form에서 보내진 post request를 처리해 주는 route

두 개의 callback 중

첫번째 callback은 보내진 form의 validation을 위한 것이다.

에러가 있으면( isValid = false; ) flash를 만들고 login view로 redirect한다.

두번째 callback은 passport local strategy를 호출해서 authentiction(로그인)을 진행한다.

config/passport.js

(참조 : http://www.passportjs.org/docs/login/)

 

Documentation: Log In

Log In Passport exposes a login() function on req (also aliased as logIn()) that can be used to establish a login session. req.login(user, function(err) { if (err) { return next(err); } return res.redirect('/users/' + req.user.username); }); When the login

www.passportjs.org

 
// Logout
router.get('/logout', function(req, res) {
  req.logout();
  res.redirect('/');
});

logout을 해주는 route

passport에서 제공된 req.logout함수를 사용하여 로그아웃하고 "/"로 redirect한다.

 

(참조 : http://www.passportjs.org/docs/logout/)

 

Documentation: Log Out

Log Out Passport exposes a logout() function on req (also aliased as logOut()) that can be called from any route handler which needs to terminate a login session. Invoking logout() will remove the req.user property and clear the login session (if any). app

www.passportjs.org

 

-login view-

<!-- views/home/login.ejs -->

<!DOCTYPE html>
<html>
  <head>
    <%- include('../partials/head') %>
  </head>
  <body>
    <%- include('../partials/nav') %>

    <div class="container">

      <h3 class="mb-3">Login</h3>

      <form class="user-form" action="/login" method="post">

        <div class="form-group row">
          <label for="username" class="col-sm-3 col-form-label">Username</label>
          <div class="col-sm-9">
            <input type="text" id="username" name="username" value="<%= username %>" class="form-control <%= (errors.username)?'is-invalid':'' %>">
            <% if(errors.username){ %>
              <span class="invalid-feedback"><%= errors.username %></span>
            <% } %>
          </div>
        </div>

        <div class="form-group row">
          <label for="password" class="col-sm-3 col-form-label">Password</label>
          <div class="col-sm-9">
            <input type="password" id="password" name="password" value="" class="form-control <%= (errors.password)?'is-invalid':'' %>">
            <% if(errors.password){ %>
              <span class="invalid-feedback"><%= errors.password %></span>
            <% } %>
          </div>
        </div>

        <% if(errors.login){ %>
          <div class="invalid-feedback d-block"><%= errors.login %></div>
        <% } %>

        <div class="mt-3">
          <input class="btn btn-primary" type="submit" value="Submit">
        </div>

      </form>

    </div>
  </body>
</html>

 

 

-nav.ejs-

<html>
  <head>
    <%- include('../partials/head') %>
  </head>
  <body>
    <%- include('../partials/nav') %>

    <div class="container contact contact-new">
      <h2>New</h2>
      <form class="cotact-form" action="/contacts" method="post">
        <div class="form-group">
          <label for="name">Name</label>
          <input type="text" id="name" name="name" value="" class="form-control">
        </div>
        <div class="form-group">
          <label for="email">Email</label>
          <input type="text" id="email" name="email" value="" class="form-control">
        </div>
        <div class="form-group">
          <label for="phone">Phone</label>
          <input type="text" id="phone" name="phone" value="" class="form-control">
        </div>
        <div>
          <button type="submit" class="btn btn-primary">Submit</button>
        </div>
      </form>
    </div>
  </body>
</html>

변경 전 navbar-nav ml-auto
변경 전

 

변경 후&nbsp;변경 전 navbar-nav ml-auto
변경 후 ( isAuthenticated ==false )

 

변경 후 ( isAuthenticated == true )

 

※ 로그인이 일어날 때 코드 진행순서

1. Login 버튼 클릭 -> routes/home.js의 router.post('/login', ~)가 실행

   1) login route 실행 (첫번째 callback)

   2)  passport local strategy (config/passport.js)를 호출해서 로그인 진행 (두번째 callback)

2. 로그인이 성공하면 config/passport.js의 serialize 코드 실행

3. routes/home.js의 passport.authenticate의 succssRedirect route로 redirect

4. 로그인 된 이후 모든 request가 config/passport.js의 deserialize코드를 거치게 됨

 

 

 

 

출처및참조 : https://www.a-mean-blog.com/ko/blog/Node-JS-%EC%B2%AB%EA%B1%B8%EC%9D%8C/%EA%B2%8C%EC%8B%9C%ED%8C%90-%EB%A7%8C%EB%93%A4%EA%B8%B0/%EA%B2%8C%EC%8B%9C%ED%8C%90-Login-%EA%B8%B0%EB%8A%A5-%EC%B6%94%EA%B0%80

 

Node JS 첫걸음/게시판 만들기: 게시판 - Login 기능 추가 - A MEAN Blog

소스코드 이 게시물에는 코드작성이 포함되어 있습니다. 소스코드를 받으신 후 진행해 주세요. MEAN Stack/개발 환경 구축에서 설명된 프로그램들(git, npm, atom editor)이 있어야 아래의 명령어들을 실

www.a-mean-blog.com

 

Comments