My Boundary As Much As I Experienced

express로 회원가입 API 만들기 (+ Hashing 적용하기, 접근 권한 적용하기) 본문

BackEnd/Node.js

express로 회원가입 API 만들기 (+ Hashing 적용하기, 접근 권한 적용하기)

Bumang 2024. 8. 11. 11:30

express로 회원가입 API를 만들었다.

코딩애플 선생님이 너가 알아서 만들어보라고 해서 알아서 만들어봤다.

 

signup.ejs파일

로그인이나 글작성 UI랑 똑같은거 가져와서 api만 바꿔줬다.

fetch api로 찔러보고 성공하면 alert창으로 성공 메시지를 띄워준다.

<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
    <link rel="stylesheet" href="/main.css" />
  </head>
  <body>
    <!-- <%- include('nav.ejs') %> -->

    <form class="form-box" action="/signup" method="POST" id="itemForm">
      <h4>회원가입</h4>
      <!-- username과 password는 passport 라이브러리가 강제하는 name임 -->
      <input name="username" id="username" />
      <input name="password" type="password" id="password" />
      <button type="submit">전송</button>
    </form>
  </body>
  <script>
    document.addEventListener("submit", async (e) => {
      e.preventDefault();

      const username = document.getElementById("username");
      const password = document.getElementById("password");

      console.log(username.value, password.value, "???");

      try {
        const response = await fetch("/signup", {
          method: "POST",
          headers: {
            "Content-Type": "application/json",
          },
          body: JSON.stringify({
            username: username.value,
            password: password.value,
          }),
        });

        username.value = "";
        password.value = "";

        if (response.ok) {
          const data = await response.json(); // 또는 response.text(), response.blob(), etc.
          alert(data);
          return data;
        } else {
          throw new Error(
            "Network response was not ok: " + response.statusText
          );
        }
      } catch (error) {
        console.log(
          "There has been a problem with your fetch operation:",
          error
        );
      }
      return;
    });
  </script>
</html>

 

 

signup get, post API

get요청 시 페이지만 단순히 던져준다.

errors 객체에다가 에러가 생길때마다 하나씩 담아주고

errors가 0일 때만 성공 플로우를 타게한다.

 

사실 아무렇게나 해도 다 성공 플로우를 타게 했는데

지피티킁한테 보완할거 있냐고 하니깐 예외처리 코드들을 보충해주었다.

app.get("/signup", (req, res) => {
  res.render("signup.ejs");
});

app.post("/signup", async (req, res) => {
  const { username, password } = req.body;

  let errors = [];

  if (!username || !password) {
    errors.push({ msg: "Please enter all fields" });
  }

  if (password.length < 6) {
    errors.push({ msg: "Password must be at least 6 characters" });
  }

  const col = db.collection("user");

  if (errors.length > 0) {
    res.status(400).json({ errors });
  } else {
    const doc = await col.findOne({ username }).then((user) => {
      if (user) {
        res.status(400).json({ error: [{ msg: "Email already exists" }] });
      } else {
        col.insertOne({ username, password });
        res.status(200).json("sign up succeeded");
      }
    });
  }
});

 

 

 

+ 보충 내용

 

1. (Hashing 적용하기)

 

해싱이란?

원본 데이터를 고정된 길이의 고유한 값으로 변환하는 기법이다.

고정된 크기란, 해시 함수가 아무리 큰 입력 데이터를 받더라도

항상 일정한 길이의 해시 값을 반환한다는 것을 의미한다.

그냥 쉽게 말해, 어떤 문자를 이상한 암호화된 문자열로 치환해주는거다. (그러나 언제나 균일한.)

 

Salt란?

해커들이 사람들이 많이 쓰는 비번 목록을 만들어놓고(qwer1234, password같은거)

이걸 해싱했을 때 어떤 값이 나오는지 족보를 만들어둔게 있다. 이걸 무용지물로 만들기 위해서

랜덤 문자열을 몇개 유저비번에 넣고 해싱을 하는 것이다.

 

대표적인 해싱 알고리즘은 아래와 같은데,

md5와 SHA1은 옛날에 쓰다가 보안 허점 발견돼서 더이상 안 쓰이는 애들이다.

아래 나머지 5개 중에 하나 사용하도록 하자.

 

Bcrypt 사용법

설치:

npm install bcrypt

 

해시 메소드를 사용하여 간편하게 해싱을 할 수 있다.

아래는 "문자"를 해싱하는 코드인데 2번째 파라미터는 복잡도라고 한다.

const bcrypt = require("bcrypt")

// 첫 번째 파라미터: 해싱할 문자
// 두 번째 파라미터: 꼬아줄 정도. 보통 10 정도로 함(50ms 정도 걸림). 15면 1초(1000ms) 정도 걸림
await bcrypt.hash("문자", 10)

 

이걸 적용해서 DB에 해시된 패스워드값을 저장시켜줘야 되는데,

그렇다면 회원가입 post API에서 아래처럼 바꿔줄 수 있겠다.

app.post("/signup", async (req, res) => {
  const { username, password } = req.body;

  let errors = [];

  if (!username || !password) {
    errors.push({ msg: "Please enter all fields" });
  }

  if (password.length < 6) {
    errors.push({ msg: "Password must be at least 6 characters" });
  }

  const hashed = await bcrypt.hash(password, 10); // 해싱

  const col = db.collection("user");

  if (errors.length > 0) {
    res.status(400).json({ errors });
  } else {
    const doc = await col.findOne({ username }).then((user) => {
      if (user) {
        res.status(400).json({ error: [{ msg: "Email already exists" }] });
      } else {
        col.insertOne({ username, password: hashed }); // 해싱된 값을 제공
        res.status(200).json("sign up succeeded");
      }
    });
  }
});

 

 

그리고 passport.use로 로그인 시 입력한 패스워드가 db에 있는 해시된 값과 똑같은지 비교해야된다.

이때 bcrypt.compare를 통해 유저가 입력한 값이 해시됐을 때 db에 있는 값과 일치할지 비교할 수 있다.

 

(password === req.password)를 bcrypt.compare(password, result.password)로 바꿨다.

// 권한 확인하는 코드
passport.use(
  new LocalStrategy(async (userId, password, cb) => {
    let result = await db.collection("user").findOne({ username: userId });
    if (!result) {
      // cb (
      //   에러여부: {...} | null,
      //   인증여부: boolean, 에러 시 false
      //   추가메시지: { message: ... }
      // )
      return cb(null, false, { message: "아이디 DB에 없음" });
    }
    // .compare(원래스트링, 해시된 문자)
    const compared = await bcrypt.compare(password, result.password);

    if (compared) {
      return cb(null, result);
    } else {
      return cb(null, false, { message: "비번불일치" });
    }
  })
);

 

 

 

 

2. '비밀번호란'과 '비밀번호 확인'란이 같아야 백엔드 요청하기

비밀번호와 비밀번호 확인란이 같은지는 굳이 서버에 확인 요청 안 해도 될거 같다.

유저 확인용에 불과하기 때문에.. 그냥 프론트 쪽에서 예외처리로 한 번 확인해줬다.

  <script>
    document.addEventListener("submit", async (e) => {
      e.preventDefault();

      const usernameInput = document.getElementById("username");
      const passwordInput = document.getElementById("password");
      const verificationInput = document.getElementById("verification");

      const username = usernameInput.value.trim();
      const password = passwordInput.value.trim();
      const verification = verificationInput.value.trim();
      
      if (!username) {
        alert("아이디를 입력해주세요");
        return;
      } else if (!password) {
        alert("비밀번호를 입력해주세요");
        return;
      } else if (!verification) {
        alert("비밀번호 확인을 입력해주세요");
        return;
      } else if (password !== verification) {
        alert("비밀번호와 비밀번호 확인란의 값이 일치하지 않습니다.");
        return;
      }
      // ...

 

3. 로그인한 사람만 글 작성하게 해주기

passport.js를 써준 덕분에 req에서 user를 뽑아 쓸 수 있게 되었다.

그래서 req.user.username이 있는 경우에는 redirect 변수를 false로 해주고

없는 경우에는 redirect를 true로 해줬다.

app.get("/write", (req, res) => {
  if (!req.user?.username) {
    res.render("write.ejs", { redirect: true });
  }
  res.render("write.ejs", { redirect: false });
});

 

그리고 write.ejs파일에는 redirect가 true일 시 로그인이 필요하다는 alert창을 띄워주고

로그인 페이지로 리다이렉팅 시켜줬다.

  <body>
    <!-- <%- include('nav.ejs') %> -->

    <form class="form-box" action="/write" method="POST" id="itemForm">
      <h4>글쓰기</h4>
      <input name="title" id="title" />
      <input name="content" id="content" />
      <button type="submit">전송</button>
    </form>

    <script>
      <% if (redirect) { %>
          alert('로그인이 필요합니다.');
          window.location.href = '/login'; // 메인 페이지로 리다이렉트
      <% } %>
    </script>
  </body>