이번 2주차 과제는 회원가입 기능과 로그인 기능이 추가되어 있는 페이지를 만드는 것이다.
우선 회원가입을 먼저 만들어 보려고 한다.
개발
우선 웹의 헤더 기능을 만들어 여러 페이지에 적용시키기 위해 헤더 부분을 만들겠다.
기본적인 뼈대는 아래 코드처럼 작성하였고 추가로 로그인/로그아웃, 마이페이지/로그아웃 버튼은 로그인 유무에 따라 다르게 보여야 하기 때문에 세션을 추가하여 조건을 설정해줬다.
<?php
session_start(); // 세션 시작
?>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="canonical" href="https://getbootstrap.kr/docs/5.1/examples/navbar-static/">
<title>Document</title>
<style>
.header{
margin-bottom: 20px;
height: 60px;
}
.signup{
margin-left: 10px;
}
.d-flex{
height: 37px;
}
.search_btn{
border-color: #70a2ee;
}
</style>
</head>
<body>
<div class="header">
<nav class="navbar navbar-expand-md" style="background-color: #e3f2fd; height: 100%; ">
<div class="container-fluid">
<a class="navbar-brand" href="index.php">Home</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarCollapse" aria-controls="navbarCollapse" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarCollapse">
<ul class="navbar-nav me-auto mb-2 mb-md-0">
<?php if (isset($_SESSION['user_id'])): ?>
<!-- 로그인된 경우 -->
<li class="nav-item" id="community">
<a class="nav-link active" aria-current="page" href="community.php">커뮤니티</a>
</li>
<?php endif; ?>
</ul>
<form class="d-flex">
<input class="form-control me-2" type="search" placeholder="Search" aria-label="Search">
<button class="search_btn btn btn-outline-primary btn-block" type="submit">Search</button>
</form>
<ul class="navbar-nav mb-2 mb-md-0">
<?php if (isset($_SESSION['user_id'])): ?>
<!-- 로그인된 경우 -->
<li class="nav-item">
<a class="nav-link" href="mypage.php">마이페이지</a>
</li>
<li class="nav-item">
<a class="nav-link" href="logout.php">로그아웃</a>
</li>
<?php else: ?>
<!-- 로그인하지 않은 경우 -->
<li class="signup">
<a class="nav-link" href="signup.php">회원가입</a>
</li>
<li class="login">
<a class="nav-link" href="login.php">로그인</a>
</li>
<?php endif; ?>
</ul>
</div>
</div>
</nav>
</div>
</body>
</html>
그 다음 회원가입 뼈대를 만들었다.
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<title>회원가입</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css">
<style>
.check-btn {
width: 120px;
background-color: #70a2ee;
border-color: #c2d7f7;
}
</style>
</head>
<body class="bg-light">
<?php include 'header.php'; ?> <!-- 헤더 포함 -->
<div class="container">
<div class="row justify-content-center" style="height: 100vh;">
<div class="col-md-4 my-auto">
<h2>회원가입</h2>
<form id="signupForm" action="signup_proc.php" method="POST">
<input type="hidden" name="csrf_token" value="<?php echo $_SESSION['csrf_token']; ?>">
<div class="mb-3">
<label for="email" class="form-label">이메일</label>
<input type="email" name="email" class="form-control" id="email" placeholder="도메인까지 작성해주세요." required>
</div>
<div class="mb-3">
<label for="password" class="form-label">비밀번호</label>
<input type="password" name="password" class="form-control" id="login_pw" required>
</div>
<div class="mb-3">
<label for="login_pw_check" class="form-label">비밀번호 확인</label>
<input type="password" class="form-control" id="login_pw_check" required>
</div>
<div class="mb-3">
<label for="name" class="form-label">이름</label>
<input type="text" name="name" class="form-control" id="name" required>
</div>
<div class="mb-3">
<label for="nickname" class="form-label">닉네임</label>
<input type="text" name="nickname" class="form-control" id="nickname" required>
</div>
<div class="mb-3">
<label for="birth" class="form-label">생년월일</label>
<input type="date" name="birth" class="form-control" id="birth" required>
</div>
<div class="mb-3">
<label for="phone_number" class="form-label">전화번호</label>
<input type="text" name="phone_number" class="form-control" id="phone_number" required>
</div>
<button type="submit" class="btn btn-primary">Submit</button>
</form>
</div>
</div>
</div>
<script src="https://code.jquery.com/jquery-3.5.1.min.js"></script>
<script src="signup_proc.js"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
</body>
</html>
header.php 파일을 불러오는 방법 다양하게 있는데 JavaScript의 load함수를 활용하여 불러올까 생각하였지만 페이지가 완전히 로드되기 전에 헤더가 나타나지 않을 수 있기 때문에 php의 include 함수를 사용하였다.
그 후 회원가입의 유효성검사를 위해 signup_proc.js 파일을 추가하였다.
$(document).ready(function() {
// 이메일 유효성 검사
$('#email').on('blur', function() {
const email = $(this).val();
const emailPattern = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/;
if (!emailPattern.test(email)) {
alert('유효한 이메일 주소를 입력해주세요.');
$(this).val('').focus(); // 이메일 필드 지우고 포커스
} else {
// 이메일 중복 확인
$.ajax({
url: 'check_email.php', // 이메일 중복 확인을 위한 PHP 파일
type: 'POST',
data: { email: email },
dataType: 'json',
success: function(response) {
emailCheckInProgress = false; // 이메일 검사 완료
if (response.exists) {
alert('이미 사용 중인 이메일입니다.');
$('#email').val('').focus(); // 이메일 필드 지우고 포커스
} else {
alert('사용 가능한 이메일입니다.');
}
},
error: function() {
alert('서버 오류가 발생했습니다. 다시 시도해주세요.');
emailCheckInProgress = false; // 이메일 검사 완료
}
});
}
});
// 폼 제출 시 유효성 검사
$('#signupForm').on('submit', function(e) {
e.preventDefault(); // 기본 제출 동작 방지
const password = $('#login_pw').val();
const passwordCheck = $('#login_pw_check').val();
const name = $('#name').val();
const nickname = $('#nickname').val();
const birth = $('#birth').val();
const phoneNumber = $('#phone_number').val();
// 비밀번호 유효성 검사
if (password.length < 8) {
alert('비밀번호는 최소 8자 이상이어야 합니다.');
$('#password').val('').focus();
return;
}
// 비밀번호 확인
if (password !== passwordCheck) {
alert('비밀번호가 일치하지 않습니다.');
$('#login_pw_check').val('').focus();
return;
}
// 이름 유효성 검사
if (!name.trim()) {
alert('이름을 입력해주세요.');
$('#name').val('').focus();
return;
}
// 닉네임 유효성 검사
if (!nickname.trim()) {
alert('닉네임을 입력해주세요.');
$('#nickname').val('').focus();
return;
}
// 생년월일 유효성 검사
if (!birth) {
alert('생년월일을 입력해주세요.');
$('#birth').val('').focus();
return;
}
// 전화번호 유효성 검사
const phonePattern = /^\d{10,11}$/; // 10자리 또는 11자리 숫자
if (!phonePattern.test(phoneNumber)) {
alert('유효한 전화번호를 입력해주세요. (10~11자리 숫자)');
$('#phone_number').val('').focus();
return;
}
// 모든 유효성 검사를 통과한 경우 폼 제출
this.submit();
});
});
해당 코드는 기본적으로 php에 사용되어 한 파일로 묶고 필요 시 각 파일에 require_once로 불러올 것이다.
<?php
session_start(); // 세션 시작
// PHP 에러 확인
error_reporting(E_ALL);
ini_set('display_errors', '1');
// 데이터베이스 연결 함수
function getConnection() {
$conn = mysqli_connect(DB_SERVER, DB_USERNAME, DB_PASSWORD, DB_NAME);
// 연결 실패 시 에러 메시지 출력
if (!$conn) {
die("Connection failed: " . mysqli_connect_error());
}
return $conn; // 연결 성공 시 연결 객체 반환
}
$conn = getConnection(); // 데이터베이스 연결 함수 호출
?>
이메일 중복 확인은 GET 방식으로도 가능하지만, GET 방식은 URL에 데이터가 노출되어 보안상 문제가 발생할 수 있다. 따라서 POST 방식을 사용하여 서버와 통신을 하여 보안성을 높였다.
<?php
require_once 'db.php'; // 데이터베이스 연결 함수 파일 추가
$email = filter_input(INPUT_POST, 'email', FILTER_SANITIZE_EMAIL);
// 이메일 형식 검증
if ($email && filter_var($email, FILTER_VALIDATE_EMAIL)) {
$sql = "SELECT COUNT(*) FROM login WHERE email = ?";
$stmt = $conn->prepare($sql);
if ($stmt === false) {
// Prepare 실패 시 처리
error_log("Prepare failed: " . $conn->error);
echo json_encode(['error' => '데이터베이스 오류 발생']);
exit();
}
$stmt->bind_param("s", $email);
$stmt->execute();
// 쿼리 실행 후 결과를 가져옵니다.
$stmt->bind_result($count);
$stmt->fetch();
// 이메일 존재 여부에 따른 JSON 응답
echo json_encode(['exists' => $count > 0]);
$stmt->close();
} else {
echo json_encode(['error' => '유효하지 않은 이메일 형식입니다.']);
}
$conn->close();
?>
그 다음 회원가입 로직을 만들었다.
<?php
require_once 'db.php'; // 데이터베이스 연결 함수 파일 추가
// CSRF 검증
if (empty($_POST['csrf_token']) || $_POST['csrf_token'] !== $_SESSION['csrf_token']) {
die('CSRF token validation failed.');// CSRF 토큰 검증 실패 시 스크립트 종료
}
// CSRF 토큰 갱신
$_SESSION['csrf_token'] = bin2hex(random_bytes(32));// 새로운 CSRF 토큰 생성
// POST 데이터 가져오기 및 검증
$email = filter_input(INPUT_POST, 'email', FILTER_SANITIZE_EMAIL);
$password = $_POST['password'];
$name = trim($_POST['name']);
$nickname = trim($_POST['nickname']);
$birth = $_POST['birth'];
$phone_number = $_POST['phone_number'];
// 사용자 정보 삽입
$sql_user = "INSERT INTO user (`name`, nickname, birth, phone_number) VALUES (?, ?, ?, ?)";
$stmt_user = $conn->prepare($sql_user);
$stmt_user->bind_param("ssss", $name, $nickname, $birth, $phone_number);
if (!$stmt_user->execute()) {
$_SESSION['error_message'] = "회원 가입 중 오류가 발생했습니다.";
header("Location: signup.php");
exit();
}
// 새로 생성된 user ID 가져오기
$user_id = $stmt_user->insert_id;
// 로그인 정보 삽입
$password = password_hash($password, PASSWORD_BCRYPT); // 비밀번호 해싱
$sql_login = "INSERT INTO login (email, `password`, user_id) VALUES (?, ?, ?)";
$stmt_login = $conn->prepare($sql_login);
$stmt_login->bind_param("ssi", $email, $password, $user_id);
if (!$stmt_login->execute()) {
$_SESSION['error_message'] = "로그인 정보 저장 중 오류가 발생했습니다.";
// 사용자 정보를 삽입한 후 실패 시, 삽입된 사용자 정보 삭제
$stmt_user->close(); // 사용자 정보 삽입을 위한 준비문 종료
$conn->close(); // 연결 종료
header("Location: signup.php");
exit();
}
// 성공 메시지 설정
$_SESSION['signup_success'] = true;
// 성공 페이지로 이동
header("Location: signup_message.php");
exit(); // 스크립트 종료
// 연결 종료
$stmt_user->close();
$stmt_login->close();
$conn->close();
?>
사용자 정보를 user 테이블과 login 테이블에 각각 저장하는 방식을 사용하고 있는데 비밀번호를 별도의 테이블로 관리하여 보안성을 높이고, 로그인 정보만 수정할 수 있어 유지보수가 용이하다는 장점이 있어 각각 테이블에 저장을 하였다.
그리고 회원가입 뼈대에 CSRF 토큰 생성과 오류 메시지 출력 코드를 작성하였다.
<?php
session_start(); // 세션 시작
// CSRF 토큰이 설정되어 있지 않다면 새로 생성
if (empty($_SESSION['csrf_token'])) {
$_SESSION['csrf_token'] = bin2hex(random_bytes(32)); // CSRF 토큰 생성
}
// 오류 메시지가 설정되어 있다면 출력 후 삭제
if (isset($_SESSION['error_message'])) {
echo '<div class="alert alert-danger">' . $_SESSION['error_message'] . '</div>'; // 오류 메시지 출력
unset($_SESSION['error_message']); // 메시지를 출력 후 세션에서 삭제
}
?>
마지막으로 회원가입 성공시 나오는 성공 페이지를 만들었다.
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<title>회원가입</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css">
<style>
.login_load_btn {
width: 100%;
background-color: #70a2ee;
border-color: #c2d7f7;
}
</style>
</head>
<body class="bg-light">
<?php include 'header.php'; ?> <!-- 헤더 포함 -->
<div class="container">
<div class="row justify-content-center" style="height: 100vh;">
<div class="col-md-4 my-auto">
<?php
session_start(); // 세션 시작
if (isset($_SESSION['signup_success'])) {
echo '<h3 style="text-align: center; margin-bottom: 30px;">회원가입을 성공하였습니다.</h3>';
unset($_SESSION['signup_success']); // 세션 삭제
} else {
echo '<script>alert("잘못된 접근입니다."); window.location.href = "signup.php";</script>';
exit();
}
?>
<button type="button" class="login_load_btn btn btn-primary btn-block" onClick="location.href='login.php'">로그인하기</button>
</div>
</div>
</div>
<script src="https://code.jquery.com/jquery-3.5.1.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
</body>
</html>
결과
- 회원가입 창
- 이메일 존재 시
- 이메일 존재하지 않을 시
- 비밀번호 유효성 확인
- 비밀번호 확인 유효성 검사
- 회원가입 성공
후기
첫째, Java로 개발했을 때보다 PHP로 개발하는 것이 더 쉬웠다.
Java로 개발할 때는 각 데이터베이스에 저장할 데이터를 JavaScript로 분리하여 서버로 전송하고, 서버 측에서 다시 처리해야 했지만, PHP에서는 전체 데이터를 한 번에 불러와 같은 파일 안에서 직접 데이터베이스에 삽입할 수 있어 개발이 훨씬 간편했다. 또한, PHP 코드가 Java에 비해 간결한 편이어서 웹 개발을 더 쉽게 할 수 있는 장점이 있었다. 추후에 PHP의 단점에 대해서도 정리해봐야겠다.
둘째, CSRF(Cross-Site Request Forgery)란 웹 애플리케이션에서 발생할 수 있는 보안 취약점 중 하나이다.
CSRF 공격은 사용자가 인증된 상태에서 의도치 않게 원하지 않는 요청을 보내도록 유도하는 공격이다. 이러한 공격을 방어하기 위한 방법 중 하나는 CSRF 토큰을 생성하는 것인데 이와 관련하여 추가적으로 학습하고 정리해야겠다.
'모의해킹 > 모의해킹 스터디' 카테고리의 다른 글
모의해킹 스터디 3주차 정리 (0) | 2024.10.31 |
---|---|
모의해킹 스터디 2주차 과제(2) - 로그인 (0) | 2024.10.28 |
모의해킹 스터디 2주차 과제 - Mini Mission (0) | 2024.10.26 |
모의해킹 스터디 2주차 정리 (0) | 2024.10.24 |
모의해킹 스터디 1주차 과제 (0) | 2024.10.21 |