실전 프로젝트 5단계 - 안전한 회원가입과 로그인 시스템
게시판을 만들 줄 안다면 이제 회원 시스템도 만들 수 있습니다. 결국 회원가입은 'DB에 사용자 정보를 INSERT 하는 것'이고, 로그인은 'DB에서 정보를 SELECT 해와서 비밀번호를 대조하는 것'에 불과하니까요.
하지만 여기에 '세션(Session)'과 '보안(Security)'이라는 양념이 강력하게 들어가야 합니다. 회원 시스템은 사이트의 심장이자 보안의 최전선입니다.
1. 데이터베이스(DB) 설계
회원 정보를 담을 users 테이블을 만듭니다.
CREATE TABLE users (
id INT UNSIGNED NOT NULL AUTO_INCREMENT,
email VARCHAR(100) NOT NULL UNIQUE, -- 이메일을 아이디로 씁니다. 중복 가입 방지!
password VARCHAR(255) NOT NULL, -- 암호화된 비밀번호가 들어갈 넉넉한 공간
name VARCHAR(50) NOT NULL,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
2. 공통 DB 접속 파일 (db.php)
이전에 게시판에서 만들었던 getPdo() 함수 방식을 그대로 재사용합니다.
<?php
declare(strict_types=1);
function getPdo(): PDO {
static $pdo = null;
if ($pdo === null) {
$dsn = 'mysql:host=localhost;dbname=mydb;charset=utf8mb4';
$pdo = new PDO($dsn, '아이디', '비밀번호', [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
PDO::ATTR_EMULATE_PREPARES => false,
]);
}
return $pdo;
}
3. 회원가입 (register.php)
사용자의 이메일과 비밀번호를 받아서 깐깐하게 검사한 뒤 DB에 저장합니다.
<?php
declare(strict_types=1);
session_start();
require 'db.php';
$errors = []; // 발생한 에러 메시지들을 모아둘 빈 바구니
// 폼이 전송되었을 때만 처리합니다.
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$name = trim($_POST['name'] ?? '');
$email = trim($_POST['email'] ?? '');
$password = trim($_POST['password'] ?? '');
$confirm = trim($_POST['confirm'] ?? '');
// 1. 입력값 깐깐하게 검증하기
if ($name === '') {
$errors[] = '이름을 입력해주세요.';
}
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
$errors[] = '올바른 이메일 형식이 아닙니다.';
}
if (mb_strlen($password) < 8) {
$errors[] = '비밀번호는 보안을 위해 8자 이상이어야 합니다.';
}
if ($password !== $confirm) {
$errors[] = '비밀번호 확인이 일치하지 않습니다.';
}
// 2. 이메일 중복 가입 확인
if (empty($errors)) {
$pdo = getPdo();
$stmt = $pdo->prepare("SELECT id FROM users WHERE email = :email");
$stmt->execute([':email' => $email]);
if ($stmt->fetch()) {
$errors[] = '이미 가입된 이메일 주소입니다.';
}
}
// 3. 에러가 하나도 없다면 DB에 안전하게 저장
if (empty($errors)) {
$pdo->prepare(
"INSERT INTO users (name, email, password) VALUES (:name, :email, :password)"
)->execute([
':name' => $name,
':email' => $email,
// [핵심 보안] 사용자의 비밀번호는 무조건 해싱(단방향 암호화)해서 저장합니다!
':password' => password_hash($password, PASSWORD_DEFAULT),
]);
header('Location: login.php?registered=1');
exit;
}
}
?>
<!DOCTYPE html>
<html lang="ko">
<head><meta charset="UTF-8"><title>회원가입</title></head>
<body>
<h2>회원가입</h2>
<!-- 에러가 있다면 붉은 글씨로 경고해줍니다 -->
<?php foreach ($errors as $error): ?>
<p style="color:red;">* <?= htmlspecialchars($error, ENT_QUOTES, 'UTF-8') ?></p>
<?php endforeach; ?>
<form method="post">
<!-- 사용자가 입력하다가 에러가 났을 때, 기존 입력값을 날리지 않고 다시 채워주는 센스! -->
<p>이름: <input type="text" name="name" value="<?= htmlspecialchars($_POST['name'] ?? '', ENT_QUOTES, 'UTF-8') ?>" required></p>
<p>이메일(아이디): <input type="email" name="email" value="<?= htmlspecialchars($_POST['email'] ?? '', ENT_QUOTES, 'UTF-8') ?>" required></p>
<p>비밀번호: <input type="password" name="password" required> <small>(8자 이상)</small></p>
<p>비밀번호 확인: <input type="password" name="confirm" required></p>
<p><input type="submit" value=" 가입하기 "></p>
</form>
<p><a href="login.php">이미 계정이 있으신가요? 로그인</a></p>
</body>
</html>
비밀번호 암호화(
password_hash)의 중요성
과거에 많이 쓰던md5()나sha1()은 이제 해커들의 1초 컷 장난감이 되었습니다. 최신 PHP가 권장하는 가장 강력한 알고리즘을 알아서 선택해 주는PASSWORD_DEFAULT옵션을 무조건 사용하세요.
4. 로그인 (login.php)
입력한 아이디와 비밀번호가 맞는지 확인하고, 맞으면 서버의 '금고(세션)'에 로그인 기록을 남깁니다.
<?php
declare(strict_types=1);
session_start();
require 'db.php';
// 이미 로그인된 상태라면 굳이 여기 있을 필요가 없으므로 메인으로 보냅니다.
if (isset($_SESSION['user_id'])) {
header('Location: index.php');
exit;
}
$error = '';
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$email = trim($_POST['email'] ?? '');
$password = trim($_POST['password'] ?? '');
if ($email === '' || $password === '') {
$error = '이메일과 비밀번호를 모두 입력해주세요.';
} else {
$pdo = getPdo();
$stmt = $pdo->prepare("SELECT id, name, password FROM users WHERE email = :email");
$stmt->execute([':email' => $email]);
$user = $stmt->fetch();
// 1. 아이디가 DB에 존재하고,
// 2. 폼에서 입력한 비밀번호와 DB의 암호화된 비밀번호가 서로 짝이 맞는다면?
if ($user && password_verify($password, $user['password'])) {
// [핵심 보안] 세션 고정(Session Fixation) 공격을 막기 위해 로그인 성공 즉시 열쇠표(세션 ID)를 갈아치웁니다.
session_regenerate_id(true);
// 세션에 내 정보를 담습니다. (이로써 로그인이 완료됩니다)
$_SESSION['user_id'] = $user['id'];
$_SESSION['user_name'] = $user['name'];
header('Location: index.php');
exit;
} else {
// "아이디가 틀렸습니다" 혹은 "비번이 틀렸습니다"라고 구체적으로 알려주지 마세요. 해커에게 힌트가 됩니다.
$error = '이메일 또는 비밀번호가 올바르지 않습니다.';
}
}
}
?>
<!DOCTYPE html>
<html lang="ko">
<head><meta charset="UTF-8"><title>로그인</title></head>
<body>
<h2>로그인</h2>
<?php if (isset($_GET['registered'])): ?>
<p style="color:green;">회원가입이 완료되었습니다. 로그인해 주세요!</p>
<?php endif; ?>
<?php if ($error !== ''): ?>
<p style="color:red;"><?= htmlspecialchars($error, ENT_QUOTES, 'UTF-8') ?></p>
<?php endif; ?>
<form method="post">
<p>이메일: <input type="email" name="email" required></p>
<p>비밀번호: <input type="password" name="password" required></p>
<p><input type="submit" value=" 로그인 "></p>
</form>
<p><a href="register.php">회원가입</a></p>
</body>
</html>
5. 출입증 검사소 (auth.php) 와 메인 페이지 (index.php)
회원 전용 게시판이나 마이페이지 등 로그인한 사람만 볼 수 있는 페이지에는 최상단에 항상 출입증 검사소(auth.php) 코드를 세워두어야 합니다.
[auth.php - 출입증 검사]
<?php
declare(strict_types=1);
session_start();
// 세션 금고에 'user_id' 정보가 없다면? (로그인을 안 했다면)
if (!isset($_SESSION['user_id'])) {
// 가차 없이 로그인 페이지로 쫓아냅니다.
header('Location: login.php');
exit;
}
[index.php - 메인 페이지]
<?php
// 메인 페이지 맨 위에 경비원을 세워둡니다. 로그인 안 한 사람은 이 아래 코드를 절대 볼 수 없습니다.
require 'auth.php';
?>
<!DOCTYPE html>
<html lang="ko">
<head><meta charset="UTF-8"><title>환영합니다</title></head>
<body>
<h2>안녕하세요, <?= htmlspecialchars($_SESSION['user_name'], ENT_QUOTES, 'UTF-8') ?>님!</h2>
<p>로그인에 성공하셨군요. 무사히 회원 전용 구역에 들어오셨습니다.</p>
<a href="logout.php">[로그아웃]</a>
</body>
</html>
6. 로그아웃 (logout.php)
로그아웃은 단순히 버튼을 누른다고 끝나는 것이 아니라, 서버와 내 PC에 연결된 세션의 끈을 아주 완벽하고 산산조각 내버려야 합니다.
<?php
declare(strict_types=1);
session_start();
// 1. 서버 금고(세션 배열) 안의 내 데이터를 싹 비웁니다.
$_SESSION = [];
// 2. 내 브라우저에 남아있는 열쇠표(세션 쿠키)마저 빼앗아서 파기합니다.
if (ini_get('session.use_cookies')) {
$params = session_get_cookie_params();
setcookie(
session_name(), '', time() - 42000,
$params['path'], $params['domain'],
$params['secure'], $params['httponly']
);
}
// 3. 서버에 존재하는 세션 파일 자체를 물리적으로 파괴합니다.
session_destroy();
header('Location: login.php');
exit;
마치며: 보안 체크리스트
오늘 만든 로그인 시스템에는 웹 보안의 핵심 3대장이 모두 들어갔습니다.
- SQL 인젝션 방어: PDO Prepared Statement 사용
- 비밀번호 유출 방어:
password_hash()로 암호화 저장- 세션 하이재킹(탈취) 방어: 로그인 성공 즉시
session_regenerate_id()호출회원 시스템은 겉보기엔 단순해 보이지만 이처럼 뒤에서 치열한 보안 로직이 돌아가야 합니다. 이 원칙들을 철저하게 지켜서 안전하고 튼튼한 웹 서비스를 만드시길 바랍니다!