Skip to content

Latest commit

 

History

History
355 lines (279 loc) · 9.59 KB

File metadata and controls

355 lines (279 loc) · 9.59 KB

User Authentication

실제 로그인 / 회원가입 절차를 만들어보자

목차

  1. Authentication 이란?
  2. Credential을 사용하고 저장하기
  3. 시큐어 라우팅 (지금 프로젝트는 URL로 로그인 상태여야지 볼 수 있는 페이지 접근 가능)

1. What is Authentication?

authentication

  • 로그인 한 유저와 로그인 하지 않은 유저를 식별해서 서로 다른 views 와 백앤드 로직을 제공하기 위해 하는 절차
  • 주로 세션을 이용해 상태를 저장한다.

How is Authentication Implemented?

authentication_implement

  • 유저가 로그인 상태를 유지하면 쿠키에 세션 아이디를 저장하고 매 요청마다 세션이 유효한지 확인한다.

프로젝트에 적용하기

Signup page Controller 만들기

  1. controllers/auth.jspostLogin 추가

    exports.postSignup = (req, res, next) => {
      const email = req.body.email;
      const password = req.body.password;
      const confirmPassword = req.body.confirmPassword;
      // Email 겹치는지 확인하기
      Uuser.findOne({ email: email })
        .then(userDoc => {
          if (userDoc) {
            return res.redirect('/signup');
          }
          const user = new User({
            email: email,
            password: password,
            cart: { items: [] }
          });
          return user.save();
        })
        .then(result => {
          res.redirect('/login');
        })
        .catch(err => {
          console.log(err);
        });
    };
  2. app.js mongoose 연결 부분 수정하기

    mongoose
      .connect(MONGODB_URI)
      .then(result => {
        app.listen(3000);
      })
      .catch(err => {
        console.log(err);
      });

패스워드 암호화 하기

  1. $ npm i --save bcryptjs 설치하기

  2. controllers/auth.jsbcryptjs 가져오고 패스워드에 해시 적용하기

    const bcrypt = require('bcryptjs')
    
    //...
    
    exports.postSignup = (req, res, next) => {
      const email = req.body.email;
      const password = req.body.password;
      const confirmPassword = req.body.confirmPassword;
      // Email 겹치는지 확인하기
      User.findOne({ email: email })
        .then(userDoc => {
          if (userDoc) {
            return res.redirect('/signup');
          }
          // 패스워드 암호화 , 12번의 해싱
          return bcrypt
            .hash(password, 12)
            .then(hashedPassword => {
              const user = new User({
                email: email,
                password: hashedPassword,
                cart: { items: [] }
              });
              return user.save();
            })
            .then(result => {
              res.redirect('/login');
            });
        })
    
        .catch(err => {
          console.log(err);
        });
    };

로그인 기능 넣기

  1. controllers/auth.jspostLogin 수정하기

    exports.postLogin = (req, res, next) => {
      const email = req.body.email;
      const password = req.body.password;
      User.findOne({ email: email })
        .then(user => {
          if (!user) {
            return res.redirect('/login');
          }
          //bcrypt.compare(평문,해시값) => promise 리턴
          bcrypt
            .compare(password, user.password)
            .then(doMatch => {
              // 패스워드까자 맞으면 실행
              if (doMatch) {
                req.session.isLoggedIn = true;
                req.session.user = user;
                return req.session.save(err => {
                  console.log(err);
                  return res.redirect('/');
                });
              }
              // 패스워드가 일치하지 않으면
              res.redirect('/login');
            })
            .catch(err => {
              console.log(err);
              res.redirect('/login');
            });
        })
        .catch(err => console.log(err));
    };

URL 접근 통제 하기

  1. middleware/is-auth.js 파일 만들고 로그인 상태 체크 코드 넣기

    module.exports = (req, res, next) => {
      if (!req.session.isLoggedIn) {
        return res.redirect('/login');
      }
      next();
    };
  2. 로그인이 필요한 라우터에 is-auth.js 파일 불러와서 상태 체크 하기

    const isAuth = require('../middleware/is-auth');
    
    router.get('/add-product', isAuth, adminController.getAddProduct);
    
    router.get('/products', isAuth, adminController.getProducts);
    
    ...

2. CSRF 공격

Cross - Site Request Forgery

교차 사이트 요청 위조

CSRF

  1. 사용자가 로그인 한다.
  2. 로그인 상태에서 CSRF 공격 코드가 들어간 게시물을 확인한다.
    코드에는 세션 ID가 포함 쿠키가 있는데 공격자는 이 쿠키를 탈취한다.
  3. 공격자가 사용자 권한으로 Server에 요청을 보낸다. (자기 계좌로 송금 등)

해결방법 - CSRF 토큰 사용하기

요청이 들어 올 때마다 토큰이 있는지 확인한다. 토큰이 있어야 정상적인 실행.

토큰은 해시값으로 추측할 수 없다.

우리는 view와 server에 토큰을 포함할 것이다.

  1. $ npm i --save csurf 설치하기

  2. app.js 에 CSRF 토큰 설정하기

    const csrf = require('csurf');
    
    // 토큰 설정
    const csrfProtection = csrf();
    app.use(csrfProtection);
  3. controllers/shop.js getIndexcsrf 토큰 넣기

    exports.getIndex = (req, res, next) => {
      Product.find()
        .then(products => {
          res.render('shop/index', {
            prods: products,
            pageTitle: 'Shop',
            path: '/',
            isAuthenticated: req.session.isLoggedIn,
            csrfToken: req.csrfToken()
          });
        })
        .catch(err => {
          console.log(err);
        });
    };
  4. views/includes/navigation.ejsLogoutCSRF 토큰 넣기

    <form action="/logout" method="post">
        <input type="hidden" name="_csrf" value="<%= csrfToken %>" />
        <button type="submit">Logout</button>
    </form>

CSRF 토큰 모든 라우터에 추가하기

  1. app.jscsrf 추가하기

    // 모든 라우터에 csrf 토큰 적용하기
    // res.locals은 view로 전달되는 로컬 변수를 설정함
    app.use((req, res, next) => {
      res.locals.isAuthenticated = req.session.isLoggedIn;
      res.locals.csrfToken = req.csrfToken();
      next();
    });
  2. 모든 view 파일에 csrf 토큰 input 추가하기

     <form action="/cart-delete-item" method="POST">
         <input type="hidden" value="<%= p.productId._id %>" name="productId">
         <button class="btn danger" type="submit">Delete</button>
         <input type="hidden" name="_csrf" value="<%= csrfToken %>" />
    </form>
    
    ...
    CSRF 토큰을 사용하기 위해선 form 태크에 위 코드를 추가해주는 것이 필수적이다

유저 피드백 추가하기

  • 로그인에 실패 했을 때 사용자가 어떤 값을 잘못 입력했는지 피드백 해준다
  1. $ npm i --save connect-flash

  2. app.jsconnect-flash 가져오기

    const flash = require('connect-flash');
    
    app.use(flash());
  3. controllers/auth.jspostLogin에서 에러 메세지 req 에 넣기

    exports.postLogin = (req, res, next) => {
      const email = req.body.email;
      const password = req.body.password;
      User.findOne({ email: email })
        .then(user => {
          if (!user) {
            // flash(에러 이름, 메세지)
            req.flash('error', 'Invalid Email or Password!');
            return res.redirect('/login');
          }
          //bcrypt.compare(평문,해시값) => promise 리턴
          bcrypt
            .compare(password, user.password)
            .then(doMatch => {
              // 패스워드까자 맞으면 실행
              if (doMatch) {
                // 세션에 로그인 상태 저장
                req.session.isLoggedIn = true;
                req.session.user = user;
                return req.session.save(err => {
                  console.log(err);
                  return res.redirect('/');
                });
              }
              // 패스워드가 일치하지 않으면
              res.redirect('/login');
            })
            .catch(err => {
              console.log(err);
              res.redirect('/login');
            });
        })
        .catch(err => console.log(err));
    };
  4. controllers/auth.jsgetLogin 에 에러 정보 뷰에 렌더링하기

    exports.getLogin = (req, res, next) => {
      let message = req.flash('error'); // 에러면 message[0] = ['Invalid Email or Password!']
        							// 없으면 빈 배열
      if (message.length > 0) {
        message = message[0];
      } else {
        message = null;
      }
      res.render('auth/login', {
        path: '/login',
        pageTitle: 'Login',
        errorMessage: message
      });
    };
  5. views/auth/login.ejs 에 메세지 표시하기

            <% if (errorMessage) { %>
            <div class="user-message user-message--error"><%= errorMessage %></div>
            <% } %>