Node.js + MongoDB 공부 - part3

YuJangHoon·2022년 2월 6일
0

Web-Development

목록 보기
5/6
post-thumbnail

Node.js + MongoDB를 공부해보자! 위대하신 코딩애플(codingapple.com)님과 함께:)

CSS 파일 저장

CSS 파일은 관습적으로 public 폴더에 보관한다고 한다.
(CSS, 이미지 등 잘 안 바뀌는 static 파일들은 다 public에 넣어 주면된다.)

다만, link태그만 단순히 HTML 파일 위에 첨부하면 되는게 아니라,
서버용 JS 파일에도 public 폴더가 있다는 사실을 알려주어야 한다.

<script>
app.use('/public', express.static('public'))
</script>

HTML 조립식 개발하기

여러가지 페이지에 동일한 요소가 있는 경우, 변경되었을 때 모든 파일을 다시 수정하는 것을 너무나도 불합리하고 비효율적인 업무 방식이다.

navbar 같은 경우가 그러한데, 이럴 때 별도로 html 파일을 만들어서 빼주고, 그 파일을 다른 파일들에 include(첨부)하여 사용하도록 하자!
(.html이 아닌 .ejs 에서 사용되는 문법)

  1. navbar 내용을 잘라내어 따로 nav.html 파일을 만든다.
  2. navbar가 필요한 ejs 파일에 가서 이런 문법을 사용한다.
<%- include('nav.html') %>

HTML에서 PUT 요청하기

  • 이전에, 서버에 요청하는 방법은 GET, POST, PUT, DELETE라고 했는데, 수정 시에는 PUT을 사용한다고 했고, 이것만 아직 다루지 않았다.

그러나, form 태그에서는 method란에 GET과 POST만 가능하기 때문에,
PUT 요청을 하기 위해서는 DELETE처럼 Ajax를 사용하거나 / method-override라는 라이브러리를 활용하면 된다.

method-overrid라는 라이브러리를 사용하기 위해서는

  1. 터미널에 npm install method-override 입력하여 설치
  2. 서버 JS파일에 아래 코드 추가 (라이브러리 및 미들웨어 등록)
<script>
// HTML에서 PUT/DELETE 요청을 위한 method-override 등록
const methodOverrice = require("method-override");
app.use(methodOverrice("_method"));
</script>
  1. 이제 html의 form 태그에서 PUT요청을 사용할 수 있다! (아래처럼)
<form action="/add?_method=PUT" method="POST">
  <input>
</form>

++) 그렇다면, edit 페이지를 위한 코드는 대략 이렇게 된다.

<script>
// /edit?_method=PUT 이라는 api로 put 요청이 오면,
app.put("/edit", function (req, res) {
  db.collection("post").updateOne(
    // 해당 request의 name이 id인 값을 _id로 가진 데이터를 찾고
    { _id: parseInt(req.body.id) },
    // 제목과 날짜를 다음 같이 $set한다.
    { $set: { 제목: req.body.title, 날짜: req.body.date } },
    function (err, result) {
      console.log("터미널에 표시 : ToDo 수정완료");
      // response에 /list라는 api로 redirect하도록 한다 :)
      res.redirect("/list");
    }
  );
});
</script>

회원인증

회원인증 방법론 간단 요약 설명

  1. session-based : 로그인을 하면, 서버에서 쿠키를 발행. 그 쿠키에는 유저의 유니크한 세션아이디가 적혀있음. 서버(세션 스토어)에도 해당 유저가 로그인을 했다는 기록(세션아이디)이 남아있음. 만약 유저가 마이페이지 등의 로그인이 필요한 시도를 하면, 같이 날라온 쿠키 데이터에 들어있는 세션 아이디를 서버의 세션 데이터에서 찾아 허용해주는 방식.

특징 : 세션을 서버에 다 저장함. 장점이자 단점

? 쿠키 : 브라우저에 저장할 수 있는 긴 문자열

  1. Token-based (JWT, json web token) : 로그인을 하면, JSON Web Token을 발행해서 브라우저에게 전송 및 쿠키나 local storage에 저장. 만약 유저가 마이페이지 등의 로그인이 필요한 시도를 할 때, 유저는 웹토큰을 헤더라는 곳에 함께 전송, 서버에서는 해당 토큰이 유효한지 확인함.

특징 : 세션을 서버에 저장할 필요가 없음.

? 토큰 : 암호화된 긴 문자열. 유통기한이 있는 열쇠라고 생각하면 됨

  1. Open Authentication (OAuth) : 다른 사이트의 프로필 정보를 가져오는 것. 유저가 로그인 시 해당 계정의 정보를 사이트에 제공 동의하는지 확인하고, 정보를 받아와서 계정을 만들거나 세션을 만들거나 JWT를 발행하는 등을 진행.

특징 : 별도의 id, pw가 필요없고 유저가 정보입력을 하지 않아도 동의를 통해 불러올 수가 있음. 단점은 연동되는 타 사이트가 사라지게 된다면... ToT

Node.js 로그인 구현 - 세션 방식

  1. 터미널에 npm install passport passport-local express-session 입력해서 설치
  2. 서버 JS 파일 상단에 아래 코드 입력
<script>
// Session 방식 로그인 기능 구현을 위한 라이브러리 연결
// req와 res사이의 미들웨어로 등록하기
const passport = require("passport");
const LocalStrategy = require("passport-local");
const session = require("express-session");
app.use(session({ secret: "비밀코드", resave: true, saveUninitialized: false }));
app.use(passport.initialize());
app.use(passport.session());
</script>

? 미들웨어 : request와 response 사이에 실행시키는 코드들. 요청이 적법한지 검사하는 기능들을 보통 많이 담는다.

아래 코드의 의미는 "/login 요청과 응답(res) 사이에 passport 라이브러리가 제공하는 Local 방식의 인증 과정을 거치고, 실패할 시에는 /fail로 이동하고 성공하면 홈페이지(/)로 이동하자" 이다.

<script>
app.post('/login', passport.authenticate('local', {failureRedirect : '/fail'}), function(req, res){
  res.redirect('/')
});
</script>

그렇다면 local 방식으로 어떻게 아이디와 비밀번호를 검사하는지 정의하는데,

<script>
passport.use(
  new LocalStrategy(
    {
      usernameField: "id", // form의 name이 id 인 것이 username
      passwordField: "pw", // form의 name이 pw 인 것이 password
      session: true, // session을 저장할 것인지
      passReqToCallback: false, // id/pw 외에 다른 정보 검증 시
    },
    function (inputID, inputPW, done) {
      db.collection("login").findOne({ id: inputID }, function (err, result) {
        if (err) return done(err);

        // done 문법 (서버에러, 성공시 사용자 DB, 에러메세지)
        if (!result)
          return done(null, false, { message: "존재하지 않는 아이디 입니다." });
        // 현재 암호화가 전혀 되어있지 않은 상태이기에 추후 변경 필요
        if (inputPW == result.pw) {
          return done(null, result);
        } else {
          return done(null, false, { message: "비밀번호가 일치하지 않습니다." });
        }
      });
    }
  )
);
</script>

대략적인 프로세스는 다음과 같다.
1. 로그인 페이지 제작 / 라우팅
2. 로그인 요청시 아이디/비번 검증 미들웨어 실행시키기
3. 아이디/비번 검증하는 세부 코드
4. 아이디/비번을 DB와 비교
5. 일치한다면 세션아이디를 발급 및 쿠키로 전송

세션아이디 발급 및 쿠키로 전송

아래 코드의 의미는 무엇일까?

<script>
// id를 이용해서 세션을 저장시키는 코드(로그인 성공 시)
passport.serializeUser(function (user, done) {
  done(null, user.id);
});

// 이 세션 데이터를 가진 사람을 DB에서 찾는 코드.
// 하단 코드의 '아이디'는 윗 코드의 user.id이다.
passport.deserializeUser(function (아이디, done) {
  // DB에서 user.id로 유저를 찾은 뒤에, 유저 정보를 {}안에 넣음\
  db.collection("login").findOne({ id: 아이디 }, function (err, result) {
    done(null, result);
  });
});
</script>

passport의 serializerUser는 세션데이터를 만들고, 세션아이드를 쿠키로 만들어서 사용자의 브라우저로 보내주는 역할을 한다.

passport의 deserializerUser는 세션아이디에 숨겨져있던 유저의 아이디와 일치하는 로그인 정보를 찾아서, 그 결과를 반환해주는 역할을 한다. 그렇게 된다면 로그인 정보(id, pw, _id)가 req.user 부분에 꽂히게 된다.

회원가입 기능과 암호화

회원가입 기능은 우선 다음과 같이 구현해보았다.

<script>
app.post("/register", (req, res) => {
  db.collection("login")
    .find({ id: req.body.id })
    .toArray((err, result) => {
      if (err) {
        return console.log(err);
      } else if (result.length === 0) {
        db.collection("login").insertOne(
          { id: req.body.id, pw: req.body.pw },
          (err, result) => {
            res.redirect("/");
          }
        );
      } else {
        res.send("이미 존재하는 아이디입니다.");
      }
    });
});
</script>

해석하자면, /register라는 api로 POST요청이 들어오면, login이라는 collection에서 입력된 id를 찾아서 Array형태로 반환한다.

  1. 만약 에러가 뜨지 않고, 검색된 결과Array의 길이가 0이라면, 기존에 없던 id인 것이기 때문에 가입을 진행한다.
  2. 만약 검색된 결과Array의 길이가 0이 아니라면, 메세지를 띄운다.

하지만, 비밀번호를 저장할 때, 암호화하지 않고 바로 pw : req.body.pw로 login collection에 저장한다는 문제점이 있다.

암호화, crypto 라이브러리

암호화하는 crypto라는 라이브러리를 사용해서 진행해보자.
구글에 있는 다른 분들의 설명을 참고해서 진행해 보았다.

<script>
crypto.randomBytes(64, (err, buf) => {
    crypto.pbkdf2(req.body.pw, buf.toString("base64"), 
    100000, 64, "sha512", (err, key) => {
        let encodeBUF = buf.toString("base64");
        let encondePW = key.toString("base64");
    });
});
</script>

대략 이런 코드가 들어가는데, 단방향 암호화를 사용해보았다.
순수하게 crypto만을 사용해서 암호화를 하였을 경우, 같은 비밀번호에 대해서 같은 암호화된 비밀번호 결과가 나오기 때문에, salt를 추가한다.

? salt : 말 그대로 소금을 치는 건데, 기존에 문자열에 salt를 붙여서 새로운 문자열을 만드는 것.

위의 코드에서는 10만번 salt를 하는데, 이렇게 해도 속도는 얼마 걸리지 않으며, 99999번째 salt와 10만번째는 완전히 다르다. 횟수도 10만처럼 깔끔한 숫자가 아니라 규칙없는 숫자면 더욱 보안에 좋다고 한다.

로그인 기능 수정

로그인을 할 경우에는,
1. 해당 아이디와 일치하는 아이디가 없는 경우 : 존재하지 않는 아이디입니다.
2. 해당 아이디와 일치하는 아이디가 있는 경우 : 유저 DB에서 salt(buf)를 가져와서, 입력된 PW를 암호화한 후 유저 DB의 암호화된 PW와 비교한다!

위의 Local 방식 검사의 일부를 이렇게 수정하면된다.

<script>
function (inputID, inputPW, done) {
      //console.log(입력한아이디, 입력한비번);
      db.collection("login").findOne({ id: inputID }, function (err, result) {
        if (err) return done(err);

        // done 문법 (서버에러, 성공시 사용자 DB, 에러메세지)
        if (!result) return done(null, false, { message: "존재하지 않는 아이디 입니다." });
        // buf 참조해서 암호화 및 비교진행
        crypto.pbkdf2(inputPW, result.buf, 100000, 64, "sha512", (err, key) => {
          let newPW = key.toString("base64");
          console.log("newPW : ", newPW);
          if (newPW == result.pw) {
            return done(null, result);
          } else {
            return done(null, false, { message: "비밀번호가 일치하지 않습니다." });
          }
        });
      });
    }
</script>

.env 파일 설정

포트번호나, DB 접속 문자열, 위의 crypto에서 salt의 횟수 등등
컴퓨터가 변경되면 바뀌어야하는 코드, 내 id와 pw같은 환경에 따라 가변적인 변수 데이터들을
보통 "환경변수"라고 부른다. (environment variable)

그래서 보통 개발자들은 미래에 대비해서 이러한 환경변수나 민감한 정보들을 .env파일에 저장하여 사용한다. 그래서 그 방법은?

  1. npm install dotenv
  2. 서버 JS파일 상단에 require('dotenv').config() 입력
  3. .env파일에 환경변수 입력, 변수이름은 보통 대문자로 표기
ex)
PORT=8080
DB_URL="mongodb+srv://codingapple1@저쩌구"
  1. 서버 JS파일에서 사용시에는 process.env.변수명 으로 사용한다.

굳이 .env파일이 아니더라도, Google이나 Naver나 AWS 등을 이용해서 서버를 발행할 때 비슷한 세팅을 할 가능성이 높다. 그러니 디테일보다는 개념과 필요한 이유를 기억하도록 하자:)

profile
HYU DataScience, ML Engineer - 산업기능요원(4급)

0개의 댓글