Almon Dev

모의해킹 공부 정리 3일차 (회원가입 페이지) 본문

모의해킹/웹 개발

모의해킹 공부 정리 3일차 (회원가입 페이지)

Almon 2024. 10. 19. 21:40

오늘 한 일

오늘은 회원가입 기능을 추가했습니다.
회원가입 페이지인 sign_up.php를 만들어서 
sign_up_proc.php에 폼 데이터를 POST로 전송한 뒤
users.txt에 id : password : nickname으로 추가하는 형태로 시작했습니다.

 

login2.php

<!DOCTYPE html>
<html lang="ko">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Almond Login</title>
    <link rel="stylesheet" href="/css/login2.css">
</head>
<body>
    <container class="login-container">
        <form class="login-form" action="login_proc2.php" method="post">
            <div class="logo-container">
                <a href="/login2.php"><img src="/imgs/almond.png" alt="logo" class="logo"></a>
            </div>
            <h2>Almond</h2>
            <input type="text" class="login-input" name="id" placeholder="아이디 또는 전화번호" required>
            <input type="password" class="login-input" name="passwd" placeholder="비밀번호" required>
            <?php
                $result = $_GET['result'];
                if($result == "failed") {
                    echo '<p class="login-failed">로그인에 실패하셨습니다.</p>';
                }
            ?>
            <button type="submit" class="login-submit">로그인</button>
        </form>
    </container>
    <div class="login-more">
        <a href="#">아이디 찾기</a>
        <a href="#">비밀번호 찾기</a>
        <a href="sign_up.php">회원가입</a>
    </div>
    <footer>
        <a href="https://www.flaticon.com/kr/free-icons/" title=" 아이콘" class="logo-source"> 아이콘 제작자: Freepik - Flaticon</a>
    </footer>
</body>
</html>

 

회원가입 페이지만 추가했습니다.

<div class="login-more">
   <a href="#">아이디 찾기</a>
   <a href="#">비밀번호 찾기</a>
   <a href="sign_up.php">회원가입</a>
</div>

 

login2.css

* {
    margin: 0;
    padding: 0;
    box-sizing: border-box;
    font-family: 'Arial', sans-serif;
}

html {
    height: 100%;
}

body {
    height: 100%;

    display: flex;
    flex-direction: column;
    justify-content: center;
    align-items: center;

    background-color: #f5e1c8;
}

a {
    text-decoration: none;
}

.login-container {
    margin-top: 7rem;
    padding: 2rem;
    width: 100%;
    max-width: 400px;

    border-radius: 8px;
    box-shadow: 0 4px 10px rgba(0, 0, 0, 0.1);

    background-color: #efdecd;
}

.login-form h2 {
    margin-bottom: 1.5rem;

    text-align: center;

    font-size: 1.5rem;
    font-weight: 800;
    color: #6e4e37; 
}

.logo-container {
    text-align: center;
}

.logo {
    margin-bottom: 1rem;

    width: 70%;
    text-align: center;
}

.login-input {
    width: 100%;
    padding: 0.75rem;
    margin-bottom: 1rem;

    border: 1px solid #d2b48c; 
    border-radius: 4px;
    transition: border-color 0.3s ease;

    font-size: 1rem;
    color: #6e4e37; 
}

.login-input:focus {
    border-color: #deb887;
    outline: none;
}

.login-submit {
    width: 100%;
    padding: 0.75rem;

    border: none;
    border-radius: 5px;
    transition: background-color 0.3s ease;
    cursor: pointer;
    
    background-color: #d2b48c;
    color: white;
    font-size: 1rem;
}

.login-submit:hover {
    background-color: #deb887;
}

.login-more {
    margin-top: 1rem;
    width: 100%;
    max-width: 300px;

    display: flex;
    text-align: center;
}

.login-more a {
    flex-grow: 1;
    
    color: #6e4e37;
    font-size: 0.8rem;
}

.login-more a+a {
    position: relative;
}

.login-more a+a::before {
    width: 1px;
    height: 10px;
    content: "";
    
    display: block;
    position: absolute;
    top: 55%;
    left: 0;
    transform: translateY(-50%);

    background-color: #d2b48c;
}


.login-failed {
    margin-bottom: 1rem;

    text-align: center;

    font-size: 1rem;
    font-weight: bold;
    color: #6e4e37;
}

footer {
    margin-top: 7rem;
}

.logo-source {
    font-size: 1rem;
    font-weight: bold;
    color: #6e4e37;
    transition: color 0.7s ease;
}

.logo-source:hover {
    color: #deb887;
}

 

추가된 부분은 .login-more 부분이고

이미지 크기를 줄이니 형태가 무너져서 컨테이너에 margin-top을 추가했습니다.

또한 로그인 실패 메세지는 위로, 아이콘 저작권 표시는 아래로 내려보냈습니다.

.login-more {
    margin-top: 1rem;
    width: 100%;
    max-width: 300px;

    display: flex;
    text-align: center;
}

.login-more a {
    flex-grow: 1;
    
    color: #6e4e37;
    font-size: 0.8rem;
}

.login-more a+a {
    position: relative;
}

.login-more a+a::before {
    width: 1px;
    height: 10px;
    content: "";
    
    display: block;
    position: absolute;
    top: 55%;
    left: 0;
    transform: translateY(-50%);

    background-color: #d2b48c;
}


 

sign_up.php

<!DOCTYPE html>
<html lang="ko">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>회원가입</title>
    <link rel="stylesheet" href="/css/sign_up.css">
</head>
<body>
    <container class="signup-container">
        <form action="sign_up_proc.php" method="post" class="signup-form">
            <div class="logo-container">
                <a href="/login2.php"><img src="/imgs/almond.png" alt="logo" class="logo"></a>
            </div>
            <h2>Almond</h2>
            <input type="text" name="id" placeholder="아이디" class="signup-input id-input" required>
            <input type="password" name="pass" placeholder="비밀번호" class="signup-input pass-input" required>
            <input type="text" name="nickname" placeholder="닉네임" class="signup-input nickname-input" required>
            <div class="agreement">
                <input type="checkbox" id="sign_up_agreement" required>
                <label for="sign_up_agreement">회원가입에 동의합니다.</label>
            </div>
            <button type="submit" class="signup-submit">회원가입</button>
        </form>
    </container>
</body>
</html>

 

아이디, 패스워드, 닉네임을 입력받아 sign_up_proc.php에 POST로 전달합니다.

sign_up.css

* {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
  font-family: 'Arial', sans-serif;
}

html {
  height: 100%;
}

body {
  height: 100%;

  display: flex;
  flex-direction: column;
  justify-content: center;
  align-items: center;

  background-color: #f5e1c8;
}

.signup-container {
  padding: 2rem;
  width: 100%;
  max-width: 400px;

  border-radius: 8px;
  box-shadow: 0 4px 10px rgba(0, 0, 0, 0.1);

  background-color: #efdecd;
}

.logo-container {
  text-align: center;
}

.logo {
  margin-bottom: 1rem;

  width: 70%;
  text-align: center;
}

.signup-form h2 {
  margin-bottom: 1.5rem;

  text-align: center;

  font-size: 1.5rem;
  font-weight: 800;
  color: #6e4e37;
}

.signup-input {
  width: 100%;
  padding: 0.75rem;
  margin-bottom: 1rem;

  border: 1px solid #d2b48c;
  border-radius: 4px;
  transition: border-color 0.3s ease;

  font-size: 1rem;
  color: #6e4e37;
}

.signup-input:focus {
  border-color: #deb887;
  outline: none;
}

.agreement {
  margin-bottom: 1rem;

  font-weight: bold;
  color: #6e4e37;
}

.signup-submit {
  width: 100%;
  padding: 0.75rem;

  border: none;
  border-radius: 5px;
  transition: background-color 0.3s ease;
  cursor: pointer;

  background-color: #d2b48c;
  color: white;
  font-size: 1rem;
}

.signup-submit:hover {
  background-color: #deb887;
}

 

 

sign_up_proc.php

<?php 
    $id = $_POST['id'];
    $pass = $_POST['pass'];
    $nickname = $_POST['nickname'];
    
    $users = fopen($_SERVER["DOCUMENT_ROOT"]."/storage/users.txt", "a");
    if($users) {
        fwrite($users, "$id:$pass:$nickname\n");
        fclose($users);
        header("Location:/login2.php");
    }else {
        header("Location:/login2.php");
    }
?>
fopen으로 users.txt파일을 쓰기 모드로 읽어옵니다.
'a'는 파일 끝 쓰기 모드로 포인터가 자동으로 파일의 끝으로 갑니다.
fwrite를 이용해 내용을 추가하면 마지막에 내용이 추가됩니다.
\n은 개행문자로 다음줄로 이동하게 됩니다.

 

본래는 이 정도로 끝내려 했으나 문제가 생겼습니다.
아이디와 닉네임이 계속 중복으로 회원가입되고 있습니다.

이 문제를 해결하기 위해서 fetch api를 이용해서
닉네임과 아이디의 중복 체크를 하고자 합니다.

 

duplicate_check.js

document.addEventListener('DOMContentLoaded', () => {
  const idInput = document.querySelector('.id-input');
  const nickInput = document.querySelector('.nickname-input');
  const className = 'duplication';
  const passInput = document.querySelector('.pass-input');
  const passCheckInput = document.querySelector('.pass-input-check');

  idInput.addEventListener('input', () => {
    const userId = idInput.value;

    fetch('/api/users/check_userId.php', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json; charset=utf-8',
      },
      body: JSON.stringify({
        id: userId,
      }),
    })
      .then((resJson) => resJson.json())
      .then((res) => {
        if (!res.pass) {
          // console.log(res.pass);
          idInput.classList.add(className);
        } else {
          // console.log(res.pass);
          idInput.classList.remove(className);
        }
      });
  });

  nickInput.addEventListener('input', () => {
    const userNick = nickInput.value;

    fetch('/api/users/check_userNick.php', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json; charset=utf-8',
      },
      body: JSON.stringify({
        nick: userNick,
      }),
    })
      // .then((resText) => resText.text())
      .then((resJson) => resJson.json())
      .then((res) => {
        if (!res.pass) {
          // console.log(res.pass);
          // console.log(res.nickname);
          // console.log(res);
          nickInput.classList.add(className);
        } else {
          // console.log(res.pass);
          nickInput.classList.remove(className);
        }
      });
  });

  passCheckInput.addEventListener('input', (event) => {
    const passwd = passInput.value;
    const passwdCheck = passCheckInput.value;

    if (passwd === passwdCheck) {
      passCheckInput.classList.remove(className);
    } else {
      passCheckInput.classList.add(className);
    }
  });
});
아... 정말 오래 걸렸습니다
기능 자체는 별것 아니지만 클라이언트와 서버 간의 데이터 전송에서
생각지 못한 오류가 계속 났습니다.

분명 JSON으로 보냈는데 JSON이 아니라는 에러들을 정말 많이 봤습니다.

Uncaught (in promise) SyntaxError: Unexpected end of JSON input

Uncaught (in promise) SyntaxError: Unexpected non-whitespace character after JSON at position 17 (line 1 column 18) Promise.then (익명) @ duplicate_check.js:43

서버 코드와 클라이언트 코드를 모두 고쳐가며 로그도 출력해 보면서 겨우 성공했습니다.

 

기능 자체는 별것 없습니다

document.addEventListener('DOMContentLoaded', () => {

DOMContentLoaded는 DOM 트리가 완전히 생성되었을 때를 의미합니다.

 

<script src="/script/duplicate_check.js" defer></script>

제가 sign_up.php 에 자바스크립트를 defer 옵션을 넣었는데 혹시라도 순서가 꼬이는 것을 방지하기 위합니다.

 

const idInput = document.querySelector('.id-input');
const nickInput = document.querySelector('.nickname-input');
const className = 'duplication';
const passInput = document.querySelector('.pass-input');
const passCheckInput = document.querySelector('.pass-input-check');

quertSelector를 이용해서 각 요소들을 받아왔습니다.

 

idInput.addEventListener('input', () => {
    const userId = idInput.value;

    fetch('/api/users/check_userId.php', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json; charset=utf-8',
      },
      body: JSON.stringify({
        id: userId,
      }),
    })
      .then((resJson) => resJson.json())
      .then((res) => {
        if (!res.pass) {
          // console.log(res.pass);
          idInput.classList.add(className);
        } else {
          // console.log(res.pass);
          idInput.classList.remove(className);
        }
      });
  });

 

idInput 태그에 input이라는 이벤트리스너를 생성했습니다.

input 이벤트는 요소에 값을 입력할 때 실시간으로 발생하기에 선택했습니다.

userId 변수에 아이디 입력창의 값을 실시간으로 넣고

fetch 함수를 통해 /api/users/check_userId.php로 json 데이터를 보냅니다.

json 데이터는 { '키' : '값' }의 형태를 하고 있으며

읽기 쉽고 용량이 적어 데이터 전송에 많이 사용됩니다.

fetch 함수는 Promise를 반환하는데 Promise는 작업의 결과는 알려주는 객체로
대기, 성공, 실패, 세 가지의 상태가 있습니다.

.then 메서드로 성공 시 일을 처리하고

.catch 메서드로 실패 시 에러를 처리합니다. 

 

.then((resJson) => resJson.json())
.then((res) => {
if (!res.pass) {
// console.log(res.pass);
idInput.classList.add(className);
} else {
// console.log(res.pass);
idInput.classList.remove(className);
}

서버(sign_up_proc.php)에서 받은 반환값(Promise)을 resJson.json()을 이용해서 json 형태로 변환합니다.

json으로 변환한 값을 res 매개변수에 넣고 성공과 실패를 if문으로 확인한 뒤

실패 시 idInput.classList.add(className);을 이용해 idInput태그에 클래스명을 추가해 줍니다.

 

  const className = 'duplication';
.duplication,
.duplication:focus {
  border: 3px solid #ff6b6b;
}

sign_up.css에 아이디가 중복일 경우 테두리 색이 진홍색으로 바뀌도록 css를 넣어줍니다.

 


자바스크립트에서 사용되는 () => {}는 Arrow Function으로
일반 함수를 조금 더 간편하게 쓰는 느낌입니다.
function sum(a, b) {
    return a+b;
}
sum = (a, b) => {
	return a+b;
}

위 두 개는 같은 함수입니다.


 

 

nickInput.addEventListener('input', () => {
    const userNick = nickInput.value;

    fetch('/api/users/check_userNick.php', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json; charset=utf-8',
      },
      body: JSON.stringify({
        nick: userNick,
      }),
    })
      // .then((resText) => resText.text())
      .then((resJson) => resJson.json())
      .then((res) => {
        if (!res.pass) {
          // console.log(res.pass);
          // console.log(res.nickname);
          // console.log(res);
          nickInput.classList.add(className);
        } else {
          // console.log(res.pass);
          nickInput.classList.remove(className);
        }
      });
  });

닉네임 역시 같은 방식으로 중복검사를 합니다.

 

passCheckInput.addEventListener('input', (event) => {
    const passwd = passInput.value;
    const passwdCheck = passCheckInput.value;

    if (passwd === passwdCheck) {
      passCheckInput.classList.remove(className);
    } else {
      passCheckInput.classList.add(className);
    }
  });

비밀번호는 서버를 거치지 않고 자바스크립트로 바로 검사합니다.

 

check_userId.php

<?php 
    $id = json_decode(file_get_contents("php://input"), true)['id'] ?? null;
    $users = fopen($_SERVER["DOCUMENT_ROOT"]."/storage/users.txt", 'r');
    $duplication = false;

    while($user = fgets($users)) {
        $user_id = explode(":", $user)[0];

        if($user_id == $id) {
            $duplication = true;
        }
    }
    fclose($users);
    header("Content-Type: apllication/json");

    // if($duplication) {
    //     echo json_encode(['pass' => false]);
    // } else {
    //     echo json_encode(['pass' => true]);
    // }

    echo json_encode(['pass' => !$duplication]);
?>

 

 

$id = json_decode(file_get_contents("php://input"), true)['id'] ?? null;

php://input 스트림에서 클라이언트에서 보낸 데이터를 읽어옵니다.

json_decode 메서드로 json을 php 배열로 만들어준 뒤에 'id' 값을 $id에 저장합니다

뒤에 ?? null은 값이 없을 경우 null을 저장한다는 의미입니다.

 

users.txt 파일을 열어서 루프를 돌며 아이디 중복을 확인합니다.

중복일 경우 $duplication을 true로 설정합니다.

 

echo json_encode(['pass' => !$duplication]);

$duplication 값을 배열로 만든 뒤 json으로 인코딩한 뒤 응답으로 보냅니다.


 

check_nick.php

<?php 
    $nickname = json_decode(file_get_contents("php://input"), true)['nick'] ?? null;
    $users = fopen($_SERVER["DOCUMENT_ROOT"]."/storage/users.txt", 'r');
    $duplication = false;
    
    while($user = fgets($users)) {
        $user_nick = trim(explode(":", $user)[2]);
        // echo "$user_nick";

        if($user_nick == $nickname) {
            $duplication = true;
        }
    }
    fclose($users);
    
    header("Content-Type: apllication/json");

    // echo json_encode(['nickname' => $nickname]);
    
    echo json_encode(['pass' => !$duplication]);
?>

아이디 중복확인과 거의 똑같지만 다른 점이 하나 있습니다.

 

$user_nick = trim(explode(":", $user)[2]);

이 부분인데 trim 메서드를 사용하고 있습니다.

users.txt 파일에 유저 정보를 id : password : nickname의 형태로 한 줄씩 저장했는데

nickname의 경우 눈에 보이지는 않지만 개행문자(\n)가 포함되어 있습니다.

그 부분을 제거해 주는 메서드입니다.

if ("Almond Dev" == "Almond Dev\n") 이 돼버려서 중복확인이 되지 않습니다.
이 부분에서 한참 헤매었습니다.

 


 

최종 수정

이렇게 중복검사는 완료되었지만, 여전히 한 가지의 문제가 있었습니다. 
중복검사에 실패하더라도 상관없이 회원가입이 완료된다는 점입니다.

그래서 회원가입 버튼을 눌렀을때 서버에서 아이디, 비밀번호, 패스워드 일치 검증을
거친 뒤에 회원가입이 가능하도록 변경하였습니다.

 

duplication.js

const submit = document.querySelector('.signup-form');

submit.addEventListener('submit', (event) => {
    event.preventDefault();

    const formData = new FormData(event.target);

    // for (const test of formData.entries()) {
    //   console.log(test);
    // }

    fetch('/sign_up_proc.php', {
      method: 'POST',
      body: formData,
    })
      // .then((restxt) => restxt.text())
      // .then((res) => console.log(res))
      .then((resJson) => resJson.json())
      .then((res) => {
        // console.log(res);

        if (res.result) {
          // 회원가입 성공
          alert('회원가입 성공');
          window.location.href = 'login2.php';
        } else {
          // 회원가입 실패
          if (res.id) {
            idInput.classList.add(className);
          } else if (res.pass) {
            passCheckInput.classList.add(className);
          } else if (res.nick) {
            nickInput.classList.add(className);
          }
        }
      });
  });

해당 부분을 추가했습니다.

const submit = document.querySelector('.signup-form');

quertSelector를 이용해 로그인 폼 요소를 submit에 저장합니다.

 

submit.addEventListener('submit', (event) => {
    event.preventDefault();

회원가입 버튼을 눌렀을때 아무런 반응을 하지 않도록 변경합니다.

 

const formData = new FormData(event.target);

    // for (const test of formData.entries()) {
    //   console.log(test);
    // }

    fetch('/sign_up_proc.php', {
      method: 'POST',
      body: formData,
    })

formData에 회원가입 폼의 모든 데이터를 넣고 POST로 sign_up_proc.php에 전송합니다.

 

      .then((resJson) => resJson.json())
      .then((res) => {
        // console.log(res);

        if (res.result) {
          // 회원가입 성공
          alert('회원가입 성공');
          window.location.href = 'login2.php';
        } else {
          // 회원가입 실패
          if (res.id) {
            idInput.classList.add(className);
          } else if (res.pass) {
            passCheckInput.classList.add(className);
          } else if (res.nick) {
            nickInput.classList.add(className);
          }

sign_up_proc.php에서 받아온 결과값을 처리합니다.

각각 아이디 패스워드 닉네임의 검증값을 가지고 검증에 실패한 요소에 클래스를 추가해

진홍색 테두리를 만들어줍니다.

모든 검증이 통과했다면 회원가입을 완료한뒤 login2.php로 이동시킵니다.

 

sign_up_proc.php

<?php 
    // $id = $_POST['id'];
    // $pass = $_POST['pass'];
    // $nickname = $_POST['nickname'];
    
    // $users = fopen($_SERVER["DOCUMENT_ROOT"]."/storage/users.txt", "a");
    // if($users) {
    //     fwrite($users, "$id:$pass:$nickname\n");
    //     fclose($users);
    //     header("Location:/login2.php");
    // }else {
    //     header("Location:/login2.php");
    // }
            
    // $req = json_decode(file_get_contents("php://input"), true);
    // $id = $req['id'];
    // $nick = $req['nickname'];
    // $pass = $req['pass'];
    // $passCheck = $req['passCheck'];
            
    header("Content-Type: apllication/json");

    $id = $_POST['id'];
    $nick = $_POST['nickname'];
    $pass = $_POST['pass'];
    $passCheck = $_POST['passCheck'];
            
    $id_duple = false;
    $nick_duple = false;
    $pass_duple = false;
    $result = false;
            
    $users = fopen($_SERVER['DOCUMENT_ROOT']."/storage/users.txt", "a+");
    rewind($users);
    while($user = fgets($users)) {
        $user_pars = explode(":", $user);
                
        $user_id = $user_pars[0];
        $user_nick = trim($user_pars[2]);
                
        if($id == $user_id) {
            $id_duple = true;
        }
                
        if($nick == $user_nick) {
            $nick_duple = true;
        }
        // echo "$id : $user_id,  $nick : $user_nick";
    }
            
    if($pass != $passCheck) {
        $pass_duple = true;
    }
            
    if (!$id_duple && !$pass_duple && !$nick_duple) {
        $result = true;
        fwrite($users, "$id:$pass:$nick\n");
    }
    fclose($users);

    echo json_encode(['result' => $result, 'id' => $id_duple, 'nick' => $nick_duple, 'pass' => $pass_duple]);         
?>

각 변수에 아이디, 닉네임, 비밀번호, 비밀번호 확인 값을 받아옵니다.

 

    header("Content-Type: apllication/json");

이건 HTTP 헤더값을 지정해주는 것으로 지금부터 보낼 응답닶은 json형식이라는 뜻입니다.

 

 $users = fopen($_SERVER['DOCUMENT_ROOT']."/storage/users.txt", "a+");
 rewind($users);

fopen의 a+는 읽기 및 쓰기모드입니다.

파일 포인터는 여전히 파일의 끝에서 시작하기에 rewind 메서드로 포인터를 처음으로 되돌립니다.

 

    while($user = fgets($users)) {
        $user_pars = explode(":", $user);
                
        $user_id = $user_pars[0];
        $user_nick = trim($user_pars[2]);
                
        if($id == $user_id) {
            $id_duple = true;
        }
                
        if($nick == $user_nick) {
            $nick_duple = true;
        }
        // echo "$id : $user_id,  $nick : $user_nick";
    }
            
    if($pass != $passCheck) {
        $pass_duple = true;
    }

반복문을 돌며 아이디와 닉네임 중복을 확인하고, 조건문으로 비밀번호를 검사합니다.

 

if (!$id_duple && !$pass_duple && !$nick_duple) {
    $result = true;
    fwrite($users, "$id:$pass:$nick\n");
}

모든 검사를 통과한 경우 $result를 true로 설정하고

users.txt파일에 새로운 아이디를 추가합니다.

 

fclose($users);
echo json_encode(['result' => $result, 'id' => $id_duple, 'nick' => $nick_duple, 'pass' => $pass_duple]);

users.txt 파일을 닫고, 회원가입, 아이디, 닉네임, 비밀번호의 결과값을 json으로 응답합니다.