일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | ||||||
2 | 3 | 4 | 5 | 6 | 7 | 8 |
9 | 10 | 11 | 12 | 13 | 14 | 15 |
16 | 17 | 18 | 19 | 20 | 21 | 22 |
23 | 24 | 25 | 26 | 27 | 28 |
- 쿠키
- Los
- MySQL
- 로그인
- lord of sql injection
- CTF
- 게시판 만들기
- Cross Site Request Forgery
- union sql injection
- lord of sqli
- php
- 과제
- Reflected Xss
- blind sql injection
- Error based sql injection
- Python
- 웹개발
- cookie 탈취
- 세션
- 로그인페이지
- sql injection point
- csrf
- sql injection
- 모의해킹
- XSS
- cors
- css
- file upload
- JS
- JWT
- Today
- Total
Almon Dev
모의해킹 공부 정리 3일차 (회원가입 페이지) 본문
오늘 한 일
오늘은 회원가입 기능을 추가했습니다.
회원가입 페이지인 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으로 응답합니다.
'모의해킹 > 웹 개발' 카테고리의 다른 글
모의해킹 공부 정리 7일차 (mysql 적용) (0) | 2024.10.23 |
---|---|
모의해킹 공부 정리 4,5,6 일차 (mysql 설정) (3) | 2024.10.22 |
모의해킹 공부 정리 2일차 (과제) (2) | 2024.10.18 |
모의해킹 공부 정리 2일차 (과제) (5) | 2024.10.18 |
모의해킹 공부 정리 1일차(과제) (2) | 2024.10.17 |