Normaltic 모의해킹 취업반 스터디 8기 - 2주차 과제(회원가입 페이지 만들기)

containerxox·2025년 4월 22일
post-thumbnail

🚩 2주차 과제 - 회원가입 페이지 만들기(+기능구현)

  • 회원가입 페이지에서 ID, Password, Username, Email를 입력하여 회원가입하게 만들기!
  • DB와 연동 시켜서 회원가입 시, 테이블의 레코드가 추가되도록 기능 구현!

☑️ Front-end

❶ login.html

💡 login.html에 sign-in코드와 sign-up코드를 같이 적음!

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Login Page</title>
    <script src="https://kit.fontawesome.com/242c7614be.js" crossorigin="anonymous"></script>
    <link rel="stylesheet" href="./css/login.css">
</head>
<body>
    <div class="login">
        <div class="login__forms">
            <!-- login form -->
            <form action="./php/login.php" method="POST" class="login__register block" id="login-in">
                <h1 class="login__title">Sign In</h1>
                <div class="login__box">
                    <i class="fas fa-user login__icon"></i>
                    <input type="text" placeholder="Username" class="login__input" name="username" required>
                </div>
                <div class="login__box">
                    <i class="fas fa-lock login__icon"></i>
                    <input type="password" placeholder="Password" class="login__input" name="password" required>
                </div>
                <a href="#" class="login__forgot">Forgot Password?</a>
                <button type="submit" class="login__button">Sign In</button>
                <div>
                    <span class="login__account login__account--account">Don't Have an Account?</span>
                    <span class="login__signin login__signin--signup" id="sign-up">Sign Up</span>
                </div>
            </form>

            <!-- create account form -->
            <form action="./php/signUp.php" method="POST" class="login__create none" id="login-up">
                <h1 class="login__title">Create Account</h1>
                <div class="login__box">
                    <i class="fas fa-user login__icon"></i>
                    <input type="text" placeholder="Username" class="login__input" name="username" required>
                </div>
                <div class="login__box">
                    <i class="fa-solid fa-at login__icon"></i>
                    <input type="email" placeholder="Email" class="login__input" name="email" required>
                </div>
                <div class="login__box">
                    <i class="fas fa-lock login__icon"></i>
                    <input type="password" placeholder="Password" class="login__input" name="password" required>
                </div>
                <button type="submit" class="login__button">Sign Up</button>
                <div>
                    <span class="login__account login__account--account">Already have an Account?</span>
                    <span class="login__signup login__signup--signup" id="sign-in">Sign In</span>
                </div>
                <div class="login__social">
                    <a href="#" class="login__social--icon"><i class="fab fa-facebook-f"></i></a>
                    <a href="#" class="login__social--icon"><i class="fab fa-x-twitter"></i></a>
                    <a href="#" class="login__social--icon"><i class="fab fa-google"></i></a>
                    <a href="#" class="login__social--icon"><i class="fab fa-github"></i></a>
                </div>
            </form>
        </div>
    </div>

    <script src="./js/login.js"></script>
</body>
</html>



❷ login.scss

// 1. 변수 정의
$color-primary: #4AD395;
$color-primary-hover: #65bf97;
$color-dark: #23004d;
$color-light: #a49eac;
$color-bg: #f2f2f2;
$font: 'Open Sans', sans-serif;
$font-size-normal: 0.938rem;
$font-size-small: 0.813rem;
$font-size-big: 1.5rem;


*, ::before, ::after {
  box-sizing: border-box;
}

body {
  margin: 0;
  padding: 0;
  font-family: $font;
  font-size: $font-size-normal;
  color: $color-dark;
}

h1 {
  margin: 0;
}

a {
  text-decoration: none;
}

// 로그인 레이아웃웃
.login {
  display: grid;
  place-items: center;
  height: 100vh;
  padding: 1.5rem;

  &__forms {
    width: 100%;
    max-width: 348px;
    background-color: $color-bg;
    padding: 2rem 1rem;
    border-radius: 1rem;
    text-align: center;
    box-shadow: 0 8px 20px rgba($color-dark, 0.2);
    animation: animateLogin 0.4s;
  }

  &__title {
    font-size: $font-size-big;
    margin-bottom: 2rem;
  }

  &__box {
    display: grid;
    grid-template-columns: max-content 1fr;
    column-gap: 0.5rem;
    padding: 1.125rem 1rem;
    background-color: #fff;
    margin-top: 1rem;
    border-radius: 0.5rem;
  }

  &__icon {
    font-size: $font-size-big;
    color: $color-primary;
  }

  &__input {
    border: none;
    outline: none;
    font-size: $font-size-normal;
    font-weight: 700;
    color: $color-dark;
    width: 100%;

    &::placeholder {
      color: $color-light;
    }
  }

  &__forgot {
    display: block;
    width: max-content;
    margin-left: auto;
    margin-top: 0.5rem;
    font-size: $font-size-small;
    font-weight: 600;
    color: $color-light;
  }

  &__button {
    display: block;
    padding: 1rem;
    margin: 2rem 0;
    background-color: $color-primary;
    color: #fff;
    font-weight: 600;
    text-align: center;
    border-radius: 0.5rem;
    transition: 0.3s;

    &:hover {
      background-color: $color-primary-hover;
    }
  }

  &__account,
  &__signin,
  &__signup {
    font-weight: 600;
    font-size: $font-size-small;

    &--account {
      color: $color-dark;
    }

    &--signup {
      color: $color-primary;
      cursor: pointer;
    }
  }

  &__social {
    margin-top: 2rem;

    &--icon {
      font-size: $font-size-big;
      color: $color-dark;
      margin: 0 1rem;
    }
  }
}


.block {
  display: block;
}

.none {
  display: none;
}


@keyframes animateLogin {
  0% {
    transform: scale(1);
  }
  50% {
    transform: scale(1.1);
  }
  100% {
    transform: scale(1);
  }
}



❸ login.css

*, ::before, ::after {
  box-sizing: border-box;
}

body {
  margin: 0;
  padding: 0;
  font-family: "Open Sans", sans-serif;
  font-size: 0.938rem;
  color: #23004d;
}

h1 {
  margin: 0;
}

a {
  text-decoration: none;
}

.login {
  display: grid;
  place-items: center;
  height: 100vh;
  padding: 1.5rem;
}
.login__forms {
  width: 100%;
  max-width: 348px;
  background-color: #f2f2f2;
  padding: 2rem 1rem;
  border-radius: 1rem;
  text-align: center;
  box-shadow: 0 8px 20px rgba(35, 0, 77, 0.2);
  animation: animateLogin 0.4s;
}
.login__title {
  font-size: 1.5rem;
  margin-bottom: 2rem;
}
.login__box {
  display: grid;
  grid-template-columns: max-content 1fr;
  -moz-column-gap: 0.5rem;
       column-gap: 0.5rem;
  padding: 1.125rem 1rem;
  background-color: #fff;
  margin-top: 1rem;
  border-radius: 0.5rem;
}
.login__icon {
  font-size: 1.5rem;
  color: #4AD395;
}
.login__input {
  border: none;
  outline: none;
  font-size: 0.938rem;
  font-weight: 700;
  color: #23004d;
  width: 100%;
}
.login__input::-moz-placeholder {
  color: #a49eac;
}
.login__input::placeholder {
  color: #a49eac;
}
.login__forgot {
  display: block;
  width: -moz-max-content;
  width: max-content;
  margin-left: auto;
  margin-top: 0.5rem;
  font-size: 0.813rem;
  font-weight: 600;
  color: #a49eac;
}
.login__button {
  display: block;
  padding: 1rem;
  margin: 2rem 0;
  width: 100%;
  background-color: #4AD395;
  color: #fff;
  font-weight: 600;
  text-align: center;
  border-radius: 0.5rem;
  border:none;
  transition: 0.3s;
}
.login__button:hover {
  background-color: #65bf97;
}
.login__account, .login__signin, .login__signup {
  font-weight: 600;
  font-size: 0.813rem;
}
.login__account--account, .login__signin--account, .login__signup--account {
  color: #23004d;
}
.login__account--signup, .login__signin--signup, .login__signup--signup {
  color: #4AD395;
  cursor: pointer;
}
.login__social {
  margin-top: 2rem;
}
.login__social--icon {
  font-size: 1.5rem;
  color: #23004d;
  margin: 0 1rem;
}

.block {
  display: block;
}

.none {
  display: none;
}

@keyframes animateLogin {
  0% {
    transform: scale(1);
  }
  50% {
    transform: scale(1.1);
  }
  100% {
    transform: scale(1);
  }
}

➡️ 회원가입 페이지




☑️ Back-end

❶ Database 부분

① Database 생성

mywebsite라는 이름의 Database 생성



② Table 생성

user_info라는 테이블 생성



③ 테이블 구조 설계

  • idx, username, pw, email 컬럼을 생성 (총 4개)
  • idxPRIMARY KEY로 설정, AUTO_INCREMENT로 설정
    → 중복 허용 X, NULL도 X, (PRIMARY KEY는 한 테이블에 하나만 설정 가능)
  • usernameemailUNIQUE로 설정하되, NOT NULL 추가!
    → 중복 허용 X, NULL도 X (UNIQUE는 한 테이블에 여러개 설정 가능)

💡 PRIMARY KEYUNIQUE 차이 정리

💡 UNIQUE vs UNIQUE NOT NULL vs PRIMARY KEY

💡 pw 칼럼을 UNIQUE로 설정하지 않은 이유

  • 비밀번호는 중복 될 수 있다!
    ↳ 유저가 다르면 같은 비밀번호라도 문제 X
    ↳ 현실에서는 여러 사람이 같은 비밀번호 사용할 수 있다.
  • 비밀번호는 비교하거나 검색 대상이 아니다!
    ↳ 비밀번호는 조회, 검색, 참조, JOIN 등의 대상이 X
    ↳ 단순히 입력한 비밀번호와 DB에 저장된 해시값의 일치 여부만 확인할 뿐!
  • 보안상 더 위험해질 가능성 有
    ↳ 비밀번호를 UNIQUE로 하면 DB 중복 확인을 위한 인덱스가 생성된다.
    ↳ 만약, 해시 처리 없이 비밀번호를 저장하거나, 쿼리 오류 메세지가 노출되면,
    해커가 무차별 대입(brute-force)하면서 중복 여부로 비밀번호를 추정할 수 있다.
    (예: insert가 실패하면 "이 비번 누가 쓰고 있나보군!"하고 추측)



❷ PHP 부분

①-1. signUp.php 생성 ( 일반 SQL문 버전 )

  • 회원가입 성공 시
    login.html로 이동

    → 레코드 추가됨!

  • DB서버 접속 실패 시DB Connect Fail!이라는 문구와 함께 에러 원인을 출력.
    ↳ 일부러 DB_PASSWORD를 틀리게 작성해서 DB접속 실패하게 함.
    ↳ 에러 원인: Access denied for user 'admin'@'localhost' (using password: YES)가 출력됨.

  • 회원가입 실패 시회원가입에 실패하였습니다. 다시 시도 해주세요 라는 문구와 함께 에러 원인을 출력
    ↳ 일부러 inser 쿼리 $sql = "insert into user_inf0o values (NULL,'$username', '$hashed_pw', '$email')";에서 테이블 명을 user_inf0o으로 틀리게 작성함.
    ↳ 에러 원인: Table 'mywebsite.user_inf0o' doesn't exist가 출력됨.


<?php
//1. DB 연결하기
define('DB_SERVER', 'localhost');
define('DB_USERNAME','admin');
define('DB_PASSWORD','student1234');
define('DB_NAME','mywebsite');
$db_conn = mysqli_connect(DB_SERVER,DB_USERNAME,DB_PASSWORD,DB_NAME);

//2. DB 접속 확인하기(접속 실패시, 해당문자열 표시됨)
if(!$db_conn){
        echo "DB Connect Fail!<br>";
        echo "에러 원인: " . mysqli_connect_error();
        exit();
}

//3. login.html로부터 회원가입 폼의 각 데이터를 받아오기
$username = $_POST['username'];
$email = $_POST['email'];
$pw = $_POST['password'];

// (추가) username, email 중복 체크
$check_sql = "select * from user_info where username = '$username' or email ='$email'";
$check_result = mysqli_query($db_conn, $check_sql);

if(mysqli_num_rows($check_result)>0){
        echo "<script>alert('이미 사용 중인 username 또는 email입니다.'); history.back();</script>";        exit();
}


//4. 비밀번호는 해시 처리하기
$hashed_pw = password_hash($pw, PASSWORD_DEFAULT);

//5. INSERT 쿼리 작성
$sql = "insert into user_info values (NULL,'$username', '$hashed_pw', '$email')";

//6. 쿼리 실행
$result = mysqli_query($db_conn, $sql);


if($result){
        //회원가입 성공시, 로그인 페이지로 이동함.
        echo "<script>window.location.href='/project-folder/login.html';</script>";
}
else{ 
        //회원가입 실패시, 해당 문자열 출력후,
        //mysqli_error()함수를 통해 에러의 원인을 출력
        echo "회원가입에 실패하였습니다. 다시 시도 해주세요.<br>";
        echo "에러 원인: " . mysqli_error($db_conn);
}

//7. DB서버 연결 종료
mysqli_close($db_conn);
?>




💡password_hash()함수란?

  • 사용자가 입력한 비밀번호를 안전하게 암호화(해시) 해주는 PHP 내장 함수
  • password_hash(평문, 해시 알고리즘);

    salt란?
    : 해킹 방지를 위해 비밀번호에 추가로 붙이는 무작위 문자열

    • 솔트는 매번 다르게 생성되며, 내부에 자동으로 포함됨
    • 그래서 같은 평문 비번 1234라도 붙는 솔트가 다르니까 결과도 매번 다른 암호화 결과가 나온다!
      // 첫 번째
      password_hash("1234", PASSWORD_DEFAULT);
      // → $2y$10$GkO... (랜덤 솔트 포함)

      // 두 번째
      password_hash("1234", PASSWORD_DEFAULT);
      // → $2y$10$LtZ... (솔트가 다르니 해시도 다름)

💡PASSWORD_DEFAULT란?

  • PHP에서 비밀번호를 암호화할 때 사용하는 내장 상수
  • 현재 PHP버전에서 권장되는 가장 강력한 해시 알고리즘을 자동으로 선택해줌.
  • 예시) $hashed_pw = password_hash($pw, PASSWORD_DEFAULT);
  • 예를 들어 암호화된 비밀번호가 $2y$10$uHKo76euvpjW3n.t3aYZ7uR0mPvtsGpP2RhI7OBs4w6Wr0rZXRtwe :
    → DB는 이 해시값을 저장하고, 로그인할 때는 password_verify(입력값, 저장된 해시)로 비교한다!



①-2. signUp2.php 생성 ( Prepared Statement 버전 )

  • 회원가입 성공 시
    login.html로 이동

    → 레코드 추가됨!

    🎯코드 전체 흐름

    1. mysqli_connect()DB에 접속
      + (추가). username과 email 중복 체크
    2. password_hash()비밀번호 해시
    3. mysqli_prepare()쿼리 준비
    4. mysqli_stmt_bind_param()값 바인딩
    5. mysqli_stmt_execute()쿼리 실행
    6. mysqli_stmt_close()쿼리 종료
    7. mysqli_close()DB 연결 종료
<?php
// 1. DB 연결하기
define('DB_SERVER', 'localhost');
define('DB_USERNAME', 'admin');
define('DB_PASSWORD', 'student1234');
define('DB_NAME', 'mywebsite');
$db_conn = mysqli_connect(DB_SERVER, DB_USERNAME, DB_PASSWORD, DB_NAME);

// 2. DB접속 확인하기 (DB접속 실패 시, 해당 문구 출력됨)
if(!$db_conn){
        echo "DB Connect Fail!<br>";
        echo mysqli_connect_error();
}

// 3.login.html로부터 회원가입 폼의 각 데이터를 받아오기
$username = $_POST['username'];
$email = $_POST['email'];
$pw = $_POST['password'];

// (추가) username, email 중복 체크
$check_sql = "select * from user_info where username = ? or email = ?";
$check_stmt = mysqli_prepare($db_conn, $check_sql);
mysqli_stmt_bind_param($check_stmt, "ss", $username, $email);
mysqli_stmt_execute($check_stmt);
$check_result = mysqli_stmt_get_result($check_stmt);

if(mysqli_num_rows($check_result)>0){
        echo "<script>alert('이미 사용중인 username 또는 email입니다.');history.back();</script>";
        exit();
}
mysqli_stmt_close($check_stmt);



// 4. 비밀번호 해시 처리하기
$hashed_pw = password_hash($pw, PASSWORD_DEFAULT);

// Prepared Statement 사용!
// 5. sql쿼리 틀을 먼저 DB에 전달하여 준비하기
$sql = "insert into user_info values (NULL, ?, ?, ?)";
$stmt = mysqli_prepare($db_conn, $sql);

if($stmt){
        // 6. 값 바인딩하기
        // (값만 따로 DB에 전송해서 바인딩함)
        // sss -> 문자열(값) 3개
        mysqli_stmt_bind_param($stmt, "sss", $username, $hashed_pw, $email);

        //7. 바인딩 마친 sql쿼리를 실제로 실행하기
        if(mysqli_stmt_execute($stmt)){
                // sql쿼리 실행완(회원가입 성공) -> 로그인페이지로 이동됨
                echo "<script>window.location.href='/project-folder/login.html';</script>";
        }
        else{
                //sql쿼리 실행실패(회원가입 실패)
                echo "회원가입에 실패하였습니다. 다시 시도 해주세요.<br>";
                echo "에러 원인 : ". mysqli_stmt_error($stmt);
        }

        // 8. sql쿼리 종료
        mysqli_stmt_close($stmt);
}
else{
        //쿼리 준비 실패 시 출력됨.
        echo "쿼리 준비 실패: " . mysqli_error($db_conn);
}

// 9. DB 연결 종료
mysqli_close($db_conn);

?>




💡Prepared Statement란?

SQL Injection 공격을 방지하기 위한 방법 중 하나로,
SQL쿼리를 미리 준비(prepare)해두고,
사용자 입력값은 나중에 따로 바인딩(bind)해서 실행하는 방식!

쉽게 말해,
👉 SQL 틀만 먼저 보내고,

// 1. 쿼리 틀 먼저 DB에 전달
$sql = "INSERT INTO users (id, pw) VALUES (?, ?)";
$stmt = mysqli_prepare($conn, $sql);

👉 나중에 실제 데이터(값)는 따로 보냄

// 2. 값만 따로 DB에 전송해서 바인딩
mysqli_stmt_bind_param($stmt, "ss", $id, $pw);
mysqli_stmt_execute($stmt);

  • 예시
$stmt = $conn->prepare("INSERT INTO users (id, pw) VALUES (?, ?)");
$stmt->bind_param("ss", $id, $pw);

➡️?: 입력값 자리(placeholder)
➡️ ss: 문자열(String) 2라는 의미
👉 이 방식은 입력 값을 SQL코드가 아닌 "값"으로만 처리"하기 때문에 SQL이 의도한 대로 실행됨!

💡mysqli_prepare()함수 란?

예시) $stmt = mysqli_prepare($conn, $sql);

  • DB 서버에 쿼리 문장을 먼저 보내서 준비시켜 놓음
  • ?로 자리(placeholder)를 만들어두고, 실제 값은 나중에 넣음

💡mysqli_stmt_bind_param()함수 란?

예시) mysqli_stmt_bind_param($stmt, "ssss", $id, $hashed_pw, $username, $email);

  • 준비된 쿼리에 실제 변수 값을 바인딩
  • ssss는 각각의 자료형을 의미함: (위의 경우는 4개의 문자열이라 ssss)
    s = string
    i = integer
    d = double
    b = blob

💡mysqli_stmt_execute()함수 란?

예시) mysqli_stmt_execute($stmt);

  • 앞서 준비하고 바인딩한 쿼리를 실제로 실행

💡mysqli_stmt_error()함수 란?

예시) mysqli_stmt_error($stmt);

  • 쿼리 실행 중 문제가 생기면 자세한 오류 메시지 출력

💡mysqli_stmt_close()함수 란?

예시) mysqli_stmt_close($stmt);

  • Prepared Statement 자원을 해제 (메모리 절약)

☑️ 일반 SQL문 버전 vs. Prepared statement 버전

  • 일반 SQL문 버전

    $id = "' OR 1=1 -- ";
    그럼 SQL이 이렇게 된다.
    INSERT INTO users (id) VALUES ('' OR 1=1 -- ');
    ➡️ 문제가 발생 ! (SQL Injection 발생)
    ( '' OR 1=1을 보면 ''(빈 문자열)은 false고, 1=1은 true니까 OR에 의해 조건이 항상 True가 됨)
    ( -- 주석으로 인해 뒤의 문장들은 주석처리 되어버림)

  • Prepared statement 버전

    → 이렇게 하면 '문제, SQL Injection 걱정 없이 안전하게 처리됨!

$stmt = $conn->prepare("INSERT INTO users (id, pw, username, email) VALUES (?, ?, ?, ?)");
$stmt->bind_param("ssss", $id, $hashed_pw, $username, $email);
$stmt->execute();

☑️ Prepared statement의 장점

SQL에서 문자열을 감싸기 위해 '가 필요하다 ('문자열')
SQL 문법상 문자열은 작은따옴표(' ')로 감싸야 한다.
PHP에서는 변수 값을 SQL문에 삽입하기 위해 작은 따옴표를 사용한다.
근데 여기서 작은따옴표, 큰따옴표 등의 특수문자를 사용하여 직접 문자열을 만들면 보안에 취약해진다 (SQL Injection 위험)
이로 인한 해결책은 Prepared Statement를 사용하는 것이다.

① 보안
: 입력값이 SQL 코드로 해석되지 않음 → SQL Injection 방지
② 성능
: 쿼리 분석/파싱을 한 번만 하고, 값만 바꿔 빠르게 반복 실행이 가능!
③ 안정성
: ', ", %, _ 등 특수문자 자동으로 안전하게 처리(이스케이프)
문법 오류 방지!

0개의 댓글