지금까지 쭉 실습으로 로그인 페이지를 다루고 있다. 이번 포스트는 로그인의 과정에 대해 중점적으로 다룬다.
로그인이란?
사용자의 계정을 이용하기 위해 운영체제, 웹 사이트 등에서 사용자 인증을 거치고 접속하는 것
간단히 말해 접근을 시도하는 사람이 정말 본인이 맞는지 확인하는 작업을 말한다. 이를 위해 ID와 PW를 넘겨 식별과 인증의 절차를 거친다.
식별과 인증
식별 (Identification): 사용자가 누구인지 확인하는 것
→ 자신을 인식할 수 있도록 제공하는 정보로, 사용자 아이디(사용자 이름, 이메일 주소, 학번) 등이 있다. 식별 정보는 타인에게 노출되어도 상관 없는 정보이다.
인증 (Authentication): 사용자가 제공한 정보가 진짜인지를 확인하여 사용자를 인증하는 것 (로그인)
→ 사용자가 제공한 정보가 실제로 그 사용자의 것인지 확인하는 정보로, 사용자 비밀번호(생체 인식 정보) 등이 있다. 인증 정보 (혹은 번호)는 노출되면 위험한 정보이다.
cf. 고유식별번호 역시 노출되면 위험한 정보이다. 해당 정보만을 통해 특정 인물을 유추할 수 있다.
예) 주민등록번호, 운전면허번호, 여권 등
결국 간단히 말해 식별 정보는 ID이고 인증 정보는 PW이므로, 로그인을 할 때 ID와 PW를 받는다는 것으로 해석할 수 있다. 이런 로그인에는 다양한 로직이 있다. 그 중 네 가지 케이스에 대해 다뤄보고자 한다.
1. 식별/인증 동시
식별과 인증을 동시에 한다는 것은 사용자가 입력한 ID와 PW를 동시에 확인하는 방식이라고 할 수 있다.
데이터베이스에서 이를 확인할 때 작동하는 query는
SELECT * FROM users WHERE username = ? AND password = ?
이 될 것이다.
<?php
try {
$user_id = $_POST['user_id'];
$user_pass = $_POST['user_pass'];
// 식별 + 인증
$sql = "SELECT * FROM member WHERE id = :user_id AND pass = :user_pass";
$stmt = $pdo->prepare($sql);
$stmt->bindParam(':user_id', $user_id);
$stmt->bindParam(':user_pass', $user_pass);
$stmt->execute();
if ($stmt->rowCount() > 0) {
echo "로그인 성공!";
} else {
echo "로그인 실패!";
}
}
?>
- 장점: 간단, 직관적
- 단점: 비밀번호가 평문으로 저장된 경우 보안에 취약
2. 식별/인증 분리
식별과 인증 과정이 분리되었다는 것은 사용자의 ID로 존재 여부 파악 후 PW 검증을 거치는 방식이라고 할 수 있다.
데이터베이스에서 이를 확인할 때 작동하는 query는 SELECT * FROM users WHERE username = ?
와 SELECT * FROM users WHERE username = ? AND password = ?
일 것이다
<?php
try {
$user_id = $_POST['user_id'];
// 식별: 사용자 ID로 비밀번호 조회
$sql = "SELECT pass FROM member WHERE id = :user_id";
$stmt = $pdo->prepare($sql);
$stmt->bindParam(':user_id', $user_id);
$stmt->execute();
// 인증: 비밀번호 비교
$db_pass = $stmt->fetchColumn();
$user_pass = $_POST['user_pass'];
if ($db_pass === $user_pass) {
echo "로그인 성공!";
} else {
echo "로그인 실패!";
}
}
?>
- 장점: 비밀번호 입력 전 피드백 제공
- 단점: 두 번의 데이터베이스 접근을 필요로 함
데이터베이스에 암호를 평문으로 저장 및 관리하는 방식은 두 가지 로직의 공통점이다. 즉, 보안 측면에서는 좋지 않은 방식이다. 그렇다면 암호화 기법을 적용하는 것이 좋겠다.
암호화 기법에는 다양한 기법이 있지만, 그 중 해시(Hash)와 암호화(Encryption) 두 가지가 있다.
- 해시(Hash): 단방향 암호화 기법
- 암호화(Encryption): 양방향 암호화 기법
단방향과 양방향이라 함은, 간단히 복호화 여부라고 볼 수 있다. 암호화는 평문 암호화 + 암호문 복호화 두 가지의 기능을 한다. 그렇기에 데이터베이스에서 비밀번호는 Hash로 보관하는 것이 안전하다. 데이터베이스가 유출되어도 복호화가 어려워 비밀번호 정보를 보호할 수 있다. 좀 더 깊게 알아보자.
해시(Hash)란?
평문의 비밀번호를 해시 함수(해시 알고리즘)을 사용하여 고정된 길이의 문자열로 변경하는 암호화 기법이다.
해시에는 몇 가지 특징이 있다.
- 해시 알고리즘은 특정 입력 값에 대해 항상 같은 해시 값을 리턴
: 인증 절차에 응용 가능
- 해시된 값은 항상 고정된 길이의 값
: 입력 길이는 서로 달라도 해시 결과값의 길이는 동일할 수 있음
- 해시 알고리즘마다 결과값의 길이는 다름
- 해시 알고리즘은 다양하며, 이미 보안이 뚫린 해시 함수 또한 존재 (예) MD5, SHA-1, HAS-180, RIPEMD-160, HAVAL 등
: 단순히 해시를 적용했다고 완벽히 안전한 것은 아니며, 사용 권고되는 해시 함수가 존재
: SHA-512, SHA-256 권장
추가로, 해시 적용한 것에 보안을 더 강화하기 위해서 솔팅(salting) 또는 반복적인 해시 사용 등의 시도를 할 수 있다. 해시된 비밀번호가 유출되는 경우, 공격자가 브루트 포스(모든 경우의 수를 함수로 돌려 전부 시도하는 공격)나 사전 공격(dictionary attack), 레인보우 테이블 공격(공격자가 미리 계산된 해시 값과 평문 비밀번호의 매핑을 저장한 테이블) 등을 시도할 수도 있다. 이에 대해 대비하기 위한 추가 대책이다.
- 솔팅(salting): 개별 사용자의 비밀번호에 고유한 솔트를 추가하여 해시화 하는 것
- 솔트(salt): 무작위 문자열, 동일한 비밀번호라도 서로 다른 해시값을 생성하게 하여 레인보우 테이블 공격 방지
- 사용자가 같은 비밀번호를 여러 서비스에서 재사용는 경우, 하나의 사이트에서 유출된 비밀번호가 다른 서비스에서도 유출될 수 있는데, 위와 같은 문제 또한 보완할 수 있음
- 솔트를 넣는 방법 또한 다양함 (예) 비밀번호 양쪽에 대입하고 해시 적용, 맨앞 또는 맨끝에 대입하고 해시 적용 후 다시 솔트 넣어 해시 적용 등
- 반복적인 해시 사용: 함수를 여러 번 돌려서 해시 값을 생성하여 브루트 포스를 어렵게 함
해싱된 평문 비밀번호를 데이터베이스에 저장하는 코드의 예시는 다음과 같다.
password = 'mypassword' # 평문 비밀번호
hashed_password = hashlib.sha512(password.encode()).hexdigest() # hashing (SHA-512)
cursor.execute('INSERT OR IGNORE INTO users (username, password) VALUES (?, ?)', (username, hashed_password))
그럼 위 두 가지 로직에 해시 함수가 적용된 케이스를 살펴보자.
3. 식별/인증 동시 with Hash
1번 로직과 동일하지만, 사용자에게 입력받은 비밀번호에 대해 해싱을 한 후 데이터베이스의 해싱 비밀번호와 일치하는지 확인하는 점에서 차이가 있다.
예제 코드는 다음과 같다.
<?php
try {
$user_id = $_POST['user_id'];
$user_pass = $_POST['user_pass'];
$hashed_pass = hash('sha512', $user_pass); // 비밀번호를 SHA-512 해시로 변환
$sql = "SELECT * FROM member WHERE id = :user_id AND pass = :user_pass";
$stmt = $pdo->prepare($sql);
$stmt->bindParam(':user_id', $user_id);
$stmt->bindParam(':user_pass', $hashed_pass);
$stmt->execute();
if ($stmt->rowCount() > 0) { // $db_pass_hash === $hashed_pass
echo "로그인 성공!";
} else {
echo "로그인 실패!";
}
}
?>
4. 식별/인증 분리 with Hash
2번 로직과 동일하지만, 사용자에게 입력받은 비밀번호에 대해 해싱을 한 후 데이터베이스의 해싱 비밀번호와 일치하는지 확인하는 점에서 차이가 있다.
<?php
try {
$user_id = $_POST['user_id'];
// 식별: 사용자 ID로 비밀번호 해시 조회
$sql = "SELECT pass FROM member WHERE id = :user_id";
$stmt = $pdo->prepare($sql);
$stmt->bindParam(':user_id', $user_id);
$stmt->execute();
// 인증: 비밀번호 해시 비교
$db_pass_hash = $stmt->fetchColumn(); // 비밀번호 해시 가져옴
$user_pass = $_POST['user_pass'];
// 입력된 비밀번호를 SHA-512 해시로 변환
$hashed_user_pass = hash('sha512', $user_pass);
if ($db_pass_hash === $hashed_user_pass) {
echo "로그인 성공!";
} else {
echo "로그인 실패!";
}
}
?>
위와 같이 로그인 로직에 대해 살펴보았다.
마무리하며 이전 실습에서 내가 처음에 짰던 로그인 코드의 로직은 어떤지 살펴보겠다.
function login($username, $password) {
global $db_conn; // 전역 변수로 DB 연결 사용
// SQL query
$sql = $db_conn->prepare("SELECT * FROM user WHERE username = ? AND pw = ?"); // 쿼리
$sql->bind_param("ss", $username, $password); // 파라미터 바인딩
$sql->execute(); // 쿼리 실행
$result = $sql->get_result(); // 결과 저장
// 결과가 존재하면 로그인 성공
if ($result->num_rows > 0) // 쿼리 결과가 하나 이상의 레코드를 반환하는지를 확인
{
return true;
}
return false;
}
// POST 요청 처리
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$username = isset($_POST['id']) ? $_POST['id'] : ''; // isset()으로 키 존재 여부 확인
$password = isset($_POST['pass']) ? $_POST['pass'] : ''; // isset()으로 키 존재 여부 확인
if (login($username, $password)) {
$_SESSION['login_success'] = true; // 로그인 성공 상태 저장
$_SESSION['username'] = $username; // 사용자 이름 저장
} else {
$_SESSION['login_success'] = false; // 로그인 실패 상태 저장
$_SESSION['message'] = "Wrong ID or password!"; // 실패 메시지 저장
}
header("Location: login.php"); // 로그인 페이지로 리다이렉트
exit();
}
login_proc.php 파일의 일부이다.
사용자가 입력한 ID와 PW를 데이터베이스의 평문 데이터와 직접 비교하고 있으므로 해싱이 적용되지 않은 식별/인증 동시 로직이다. 위 과정의 학습을 통해 1,2주차의 코드가 보안에 취약한 상태임을 확인할 수 있다.
'모의해킹 > 웹해킹스터디' 카테고리의 다른 글
쿠키(Cookie)와 세션(Session) (0) | 2024.11.27 |
---|---|
[실습] 4개 로직 반영한 로그인 페이지 만들기 (0) | 2024.11.27 |
[실습] 회원가입 페이지 만들기 + DB연동 (0) | 2024.11.27 |
데이터베이스와 SQL (3) | 2024.11.27 |
[실습] 로그인 페이지 만들기 + CSS (0) | 2024.11.27 |