일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
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 |
- XSS
- 쿠키
- Error based sql injection
- csrf
- lord of sqli
- lord of sql injection
- Reflected Xss
- 게시판 만들기
- CTF
- JS
- sql injection
- cors
- 모의해킹
- Cross Site Request Forgery
- cookie 탈취
- sql injection point
- Los
- blind sql injection
- php
- JWT
- Python
- css
- MySQL
- 웹개발
- 로그인페이지
- 과제
- 세션
- union sql injection
- 로그인
- file upload
- Today
- Total
Almon Dev
게시판 만들기 #8 (마이페이지 수정) 본문
마이페이지 수정 기능
마이페이지 생성에서 만들었던 마이페이지에 수정기능이 빠져있어 추가했습니다.
마이페이지 수정 버튼
// login_successful.php
// 수정 전
<button class="mypageset-btn">수정하기</button>
// 수정 후
<button class="mypageset-btn" onclick="location.href='/mypage_update.php'">수정하기</button>
마이페이지의 수정 버튼을 클릭했을 때 mypage_update.php로 리다이렉트 하도록 변경했습니다.
mypage_update.php
<?php
require_once("jwt_auth.php");
require_once("mysql.php");
if ($token = Jwt_auth::auth()) {
$user_id = $token->sub;
$sql = "select profile_img_path, nickname, email, password, name from users where user_id='$user_id'";
try {
$result = runSQL($sql)->fetch_array();
$db_pass = $result['password'];
$nickname = $result['nickname'];
$email = $result['email'];
$img_path = $result['profile_img_path'] ?? "almond-profile.jpg";
$name = $result['name'];
} catch(Exception $e) {
// sql 실패
die("계정 정보를 확인할 수 없습니다.")
}
}else {
header("Location: login2.php");
exit;
}
?>
<!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/login_successful.css">
<link rel="stylesheet" href="/css/mypage_update.css">
<script src="./script/mypage_update.js"></script>
<script src="./script/chang_img.js" defer></script>
</head>
<body>
<container class="profile-container">
<form action="" class="mypage-form" enctype="multipart/form-data">
<p class="mypage-row update-mypage mypage profile">
<label for="profile_img">프로필 : </label>
<input type="file" id="profile_img_input" name="profile_img" style="display: none;" accept=".jpg, .png, .gif, .jpeg">
<img src="/upload/profile_img/<?=$img_path ?>" class="profile_img" id="profile_img" onclick="profileImgUpload()">
</p>
<p class="mypage-row update-mypage mypage name">
<label for="nickname">이름 : </label>
<input type="text" id="name" class="mypage update name" name="name" value=<?=$name?> required >
</p>
<p class="mypage-row update-mypage mypage nickname">
<label for="nickname">닉네임 : </label>
<input type="text" id="nickname" class="mypage update nickname" name="nickname" value=<?=$nickname?> required >
</p>
<p class="mypage-row update-mypage mypage email">
<label for="email">이메일 : </label>
<input type="email" id="email" class="mypage update email" name="email" value=<?=$email?> required>
</p>
<p class="mypage-row update-mypage mypage pass-btn">
<label for="pass">비밀번호 : </label>
<button id="pass" onclick="updatePass(event)">수정</button class="mypage update pass-btn">
</p>
<p class="mypage-row update-mypage mypage">
<button class="mypageset-btn on" onclick="updateSubmit(event)">수정완료</button>
</p>
</form>
<p class="fail">실패</p>
</container>
</body>
</html>
jwt token을 이용해서 유저를 식별한 뒤 해당 유저의 정보를 가져와 input태그에 넣어서 출력하는 코드입니다.
<?php
require_once("jwt_auth.php");
require_once("mysql.php");
if ($token = Jwt_auth::auth()) {
$user_id = $token->sub;
Jwt_auth의 auth 메서드를 이용해서 토큰을 추출해서 $token에 저장합니다.
$user_id에 jwt token의 sub에 저장된 user_id를 저장합니다.
JWT에 대한 정리 : https://almon.tistory.com/21#toc5
$sql = "select profile_img_path, nickname, email, password, name from users where user_id='$user_id'";
try {
$result = runSQL($sql)->fetch_array();
$db_pass = $result['password'];
$nickname = $result['nickname'];
$email = $result['email'];
$img_path = $result['profile_img_path'] ?? "almond-profile.jpg";
$name = $result['name'];
} catch(Exception $e) {
// sql 실패
die("계정 정보를 확인할 수 없습니다.")
}
토큰에서 가져온 user_id를 이용해서 db에서 정보를 가져오고 각 변수에 저장합니다.
try 함수를 사용해서 에러가 발생하는 경우를 처리합니다.
<p class="mypage-row update-mypage mypage profile">
<label for="profile_img">프로필 : </label>
<input type="file" id="profile_img_input" name="profile_img" style="display: none;" accept=".jpg, .png, .gif, .jpeg">
<img src="/upload/profile_img/<?=$img_path ?>" class="profile_img" id="profile_img" onclick="profileImgUpload()">
</p>
function profileImgUpload() {
const uploadTag = document.querySelector('#profile_img_input');
uploadTag.click();
}
input 태그의 file 타입을 이용해서 파일을 업로드할 수 있는 태그를 생성합니다.
img 태그의 onclick 핸들러를 이용해서 이미지를 클릭했을 때 input 태그를 클릭하는 JS를 실행합니다.
style="display:none" : 화면에 input 태그를 숨깁니다.
accept=".jpg .jpeg .png .gif" : accept를 이용해서 파일 확장자를 제한합니다.
<p class="mypage-row update-mypage mypage name">
<label for="nickname">이름 : </label>
<input type="text" id="name" class="mypage update name" name="name" value=<?=$name?> required >
</p>
<p class="mypage-row update-mypage mypage nickname">
<label for="nickname">닉네임 : </label>
<input type="text" id="nickname" class="mypage update nickname" name="nickname" value=<?=$nickname?> required >
</p>
<p class="mypage-row update-mypage mypage email">
<label for="email">이메일 : </label>
<input type="email" id="email" class="mypage update email" name="email" value=<?=$email?> required>
</p>
<p class="mypage-row update-mypage mypage pass-btn">
<label for="pass">비밀번호 : </label>
<button id="pass" onclick="updatePass(event)">수정</button class="mypage update pass-btn">
</p>
<p class="mypage-row update-mypage mypage">
<button class="mypageset-btn on" onclick="updateSubmit(event)">수정완료</button>
</p>
<?=변수명 ?>을 이용해서 db에서 가져온 정보를 input태그의 value에 추가합니다.
function updatePass(e) {
e.preventDefault();
const passBtnRow = document.querySelector('.mypage-row.pass-btn');
const emailRow = document.querySelector('.mypage-row.email');
const container = document.querySelector('.profile-container');
const passTag = `
<p class="mypage-row update-mypage mypage pass">
<label for="pass">비밀번호 : </label>
<input type="password" class="mypage update pass" name="password" id="pass"/>
</p>
<p class="mypage-row update-mypage mypage pass-check">
<label for="pass-check">비밀번호 확인 : </label>
<input type="password" class="mypage update pass-check" name="password-check" id="pass-check"/>
</p>
`;
passBtnRow.remove();
emailRow.insertAdjacentHTML('afterend', passTag);
container.classList.add('pass-update');
}
비밀번호 수정 버튼을 클릭하면 updatePass 함수가 실행됩니다.
e.preventDefault();
이벤트가 발생했을 때 기본 동작을 하지 않도록 합니다.
ex) submit 이벤트시 리다이렉트를 하지 않도록 합니다.
const passBtnRow = document.querySelector('.mypage-row.pass-btn');
const emailRow = document.querySelector('.mypage-row.email');
const container = document.querySelector('.profile-container');
비밀번호 수정 버튼 태그와 이메일 수정 태그를 가져옵니다.
컨테이너도 크기 수정을 위해 가져옵니다.
const passTag = `
<p class="mypage-row update-mypage mypage pass">
<label for="pass">비밀번호 : </label>
<input type="password" class="mypage update pass" name="password" id="pass"/>
</p>
<p class="mypage-row update-mypage mypage pass-check">
<label for="pass-check">비밀번호 확인 : </label>
<input type="password" class="mypage update pass-check" name="password-check" id="pass-check"/>
</p>
`;
비밀번호를 입력할 태그를 생성하고 passTag에 저장합니다.
passBtnRow.remove();
비밀번호 수정 버튼 태그를 제거합니다.
emailRow.insertAdjacentHTML('afterend', passTag);
이메일 태그의 뒷부분에 생성한 비밀번호를 입력할 input 태그들을 삽입합니다.
container.classList.add('pass-update');
컨테이너 태그에 pass-update 클래스를 추가합니다.
pass-update 클래스는 height를 600px로 키워줍니다.
const imgInput = document.querySelector('#profile_img_input');
const imgTag = document.querySelector('#profile_img');
imgInput.addEventListener('change', (e) => {
const imgFile = e.target.files[0];
if (imgFile) {
const fileReader = new FileReader();
fileReader.onload = (e) => {
imgTag.src = e.target.result;
};
fileReader.readAsDataURL(imgFile);
}
});
input 태그에서 수정할 프로필 사진을 등록했을 때 img 태그의 사진이 변경되는 js 코드입니다.
const imgInput = document.querySelector('#profile_img_input');
const imgTag = document.querySelector('#profile_img');
이미지 파일을 선택하는 input 태그와 가져온 이미지를 출력할 img 태그를 가져옵니다.
imgInput.addEventListener('change', (e) => {
change 이벤트가 발생할 경우 함수를 실행합니다.
change 이벤트는 태그에 변화가 생겼을 때 발생합니다.
ex) value 변경, 파일 선택 (같은 파일 X)
const imgFile = e.target.files[0];
e.target인 input 태그로부터 선택한 파일을 가져와 imgFile에 저장합니다.
if (imgFile) {
const fileReader = new FileReader();
imgFile을 성공적으로 가져왔다면 FileReader 객체를 생성합니다.
FileReader 객체는 Data URL형식으로 파일을 가져올 수 있습니다.
이걸 img 태그의 src에 삽입할 것입니다.
fileReader.onload = (e) => {
imgTag.src = e.target.result;
};
fileReader가 로드가 되면 img 태그의 src에 읽어온 파일을 삽입합니다.
onload 역시 addEventListener처럼 등록을 해두면 이벤트 발생 시 콜백 함수가 실행됩니다.
fileReader.readAsDataURL(imgFile);
FileReader를 이용해 DataURL 타입으로 선택한 파일을 읽어옵니다.
그리고 모두 읽어오면 결과가 img 태그의 src로 삽입됩니다.
Data URL
Data URL은 데이터를 텍스트로 인코딩한 것입니다. 별도로 파일을 요청할 필요 없이 파일을 처리할 수 있습니다.
Data URL 구조
data:[<mediatype>][;base64],<data>
[<mediatype>] : MIME 타입입니다. ex) jpeg 파일 => 'image/jpeg'
[;base64] : 이 부분이 있으면 base64로 인코딩 되었음을 의미합니다. => text일 경우 base64를 안 쓰기도 합니다.
<data> : 실제 데이터
function updateSubmit(e) {
e.preventDefault();
const updateForm = document.querySelector('.mypage-form');
const formData = new FormData(updateForm);
fetch('/mypage_update_proc.php', {
method: 'POST',
body: formData,
})
// .then((res) => res.text())
// .then((res) => console.log(res));
.then((res) => res.json())
.then((res) => {
const fail = document.querySelector('.fail');
if (res.result) {
if (res.password) {
location.href = 'logout.php';
} else {
fail.classList.remove('on');
alert('회원정보가 수정되었습니다.');
location.href = 'login_successful.php';
}
} else {
fail.classList.add('on');
}
});
}
수정완료 버튼을 클릭하면 updateSubmit() 함수가 실행됩니다.
이 함수는 form 태그의 데이터를 mypage_update_proc.php에 전송하고 결과를 처리하는 역할입니다.
function updateSubmit(e) {
e.preventDefault();
const updateForm = document.querySelector('.mypage-form');
const formData = new FormData(updateForm);
submit 버튼을 클릭 시 기본 동작을 막고 form의 데이터를 가져옵니다.
FormData는 multipart/form-data MIME 타입이기 때문에 파일 전송에 사용할 수 있습니다.
fetch('/mypage_update_proc.php', {
method: 'POST',
body: formData,
})
mypage_update_proc.php에 formData를 포함한 POST 요청을 보냅니다.
.then((res) => res.json())
응답이 오면 json을 파싱해 javascript 객체로 변환합니다.
.then((res) => {
const fail = document.querySelector('.fail');
if (res.result) {
~~~
} else {
fail.classList.add('on');
}
응답에 담긴 결과가 실패일 경우 실패 메시지를 출력합니다.
fail 클래스를 가진 태그에 on 클래스를 추가하면 화면에 출력됩니다.
if (res.result) {
if (res.password) {
location.href = 'logout.php';
} else {
fail.classList.remove('on');
alert('회원정보가 수정되었습니다.');
location.href = 'login_successful.php';
}
}
응답이 성공일 경우 비밀번호를 변경했다면 로그아웃을 시키고, 그렇지 않은 경우 성공 메시지를 띄우고 마이페이지로 돌아갑니다.
mypage_update_proc.php
<?php
require_once("jwt_auth.php");
require_once("mysql.php");
$result = false;
$pass_result = false;
// GET 메서드로 요청을 보내면 모두 null이 되어버림 (수정필요)
if(isset($_POST['name']) or isset($_POST['email']) or isset($_POST['password']) or isset($_FILES['profile_img'])) {
if($token = Jwt_auth::auth()) {
$user_id = $token->sub;
$name = $_POST['name'] ?? null;
$password = $_POST['password'] ?? null;
$nickname = $_POST['nickname'] ?? null;
$email = $_POST['email'] ?? null;
$password_filtered = preg_replace("/\s+/", "", $password);
$file = $_FILES['profile_img'];
// var_dump($file);
$upload_dir = './upload/profile_img/';
$extension = ['jpg', 'png', 'gif', 'jpeg'];
$sql = "update users set";
$img_result = false;
$pass_result = false;
if($name != null) {
$sql = $sql . " name='$name'";
}
if ($nickname != null) {
$sql = $sql . ", nickname='$nickname'";
}
if ($email != null) {
$sql = $sql . ", email='$email'";
}
if($password != null && $password === $password_filtered) {
$pass_hash = password_hash($password, PASSWORD_ARGON2ID);
$sql = $sql . ", password='$pass_hash'";
$pass_result = true;
}
if($file['error'] == UPLOAD_ERR_OK) {
$file_name = basename($file['name']);
$path_info = pathinfo($file_name);
$file_extension = strtolower($path_info['extension']);
if (!in_array($file_extension, $extension)) {
die("파일 확장자를 확인해 주세요. \n'jpg', 'jpeg', 'png', 'gif'만 사용이 가능합니다.");
}
$sql = $sql . ", profile_img_path='$file_name'";
$upload_path = $upload_dir . $file_name;
if(move_uploaded_file($file['tmp_name'], $upload_path)) {
$img_result = true;
}
}
$sql = $sql . " where user_id='$user_id'";
// echo "$sql";
$result = runSQL($sql);
}else {
header("Location: login2.php");
}
}else {
echo('요청을 확인해주세요');
header("Location: login2.php");
exit;
}
echo json_encode(["result" => $result, "password" => $pass_result, "img" => $img_result]);
입력받은 값이 있는지 검사한 후에 sql문에 포함시켜 db를 변경하는 코드입니다.
if(isset($_POST['name']) or isset($_POST['email']) or isset($_POST['password']) or isset($_FILES['profile_img'])) {
if($token = Jwt_auth::auth()) {
이메일, 이름, 비밀번호, 프로필 사진 중 하나라도 변경점이 있는지 확인하고 토큰까지 확인한 뒤에 if문을 실행합니다.
검사하는 과정이 없을 때 GET 메서드로 요청을 보내면 변수 선언 부분에서 모두 null로 처리돼서
이름, 이메일, 사진이 null로 변경되는 점을 방지하기 위해서 추가했습니다.
$user_id = $token->sub;
$name = $_POST['name'] ?? null;
$password = $_POST['password'] ?? null;
$nickname = $_POST['nickname'] ?? null;
$email = $_POST['email'] ?? null;
$password_filtered = preg_replace("/\s+/", "", $password);
각 정보들을 변수에 저장합니다. 비밀번호의 경우 정규식으로 공백을 검사한 뒤 삭제한 변수를 생성합니다.
삼항 연산자를 이용해서 POST 요청이 없는 값을 null로 처리합니다.
나중에 if문을 통해서 sql문에 추가할지 말지를 결정하기 위해서입니다.
$file = $_FILES['profile_img'];
// var_dump($file);
$upload_dir = './upload/profile_img/';
$extension = ['jpg', 'png', 'gif', 'jpeg'];
업로드한 프로필 사진을 $file에 저장합니다. 그리고 업로드 파일을 저장할 경로와 허용할 확장자를 각 변수에 저장합니다.
확장자를 검사하는 이유는 파일 업로드 취약점을 방지하기 위합니다.
php와 같은 서버 실행 파일을 삽입하는 공격을 막기 위해 jpg, png, gif, jpeg 같은 사진 파일만 업로드할 수 있도록 화이트 리스트 기반 필터링을 진행합니다.
$sql = "update users set";
$img_result = false;
$pass_result = false;
if($name != null) {
$sql = $sql . " name='$name'";
}
if ($nickname != null) {
$sql = $sql . ", nickname='$nickname'";
}
if ($email != null) {
$sql = $sql . ", email='$email'";
}
if($password != null && $password === $password_filtered) {
$pass_hash = password_hash($password, PASSWORD_ARGON2ID);
$sql = $sql . ", password='$pass_hash'";
$pass_result = true;
}
$sql에 update문의 기본 형식을 작성하고 if문을 통해 정보를 변경하는 경우에만 update문을 추가합니다. 비밀번호의 경우 공백이 포함되지 않은 것 역시 검사를 하고 해시화를 거쳐 sql문에 추가합니다.
$password === $password_filtered에서 ===를 사용하는 이유는 ==의 경우 숫자로 이루어진 문자열을 숫자로 변환해서 비교하기 때문입니다. 예를 들어 "1234"와 "1234 "처럼 공백이 포함되었음에도 같은 값으로 취급하여 참이 반환됩니다.
타입 변환
== 의 경우 비교를 할 때 문자열이 숫자로 이루어진 경우 숫자로 변경해서 비교합니다.
ex)
"1234" == '1234 '
'1234 '중 1234만 숫자로 변경
1234 == 1234 true
if($file['error'] == UPLOAD_ERR_OK) {
$file_name = basename($file['name']);
$file ['error'] == UPLOAD_ERR_OK의 경우 정상적으로 업로드된 경우만 true를 반환합니다.
정상적으로 업로드된 경우 파일의 이름을 $file_name에 삽입합니다.
$path_info = pathinfo($file_name);
pathinfo 함수를 이용해 파일의 경로, 확장자, 이름을 분리합니다.
$file_extension = strtolower($path_info['extension']);
파일의 확장자를 소문자로 변환해서 $file_extension 변수에 저장합니다.
소문자로 변환하는 이유는 PHp, PhP처럼 대문자와 소문자를 섞어 우회하는 경우를 방지하기 위함입니다.
여기서는 jpg 등 이미지 파일만 통과시키기 때문에 의미는 없지만 소문자로 변환해서 검사하면 Jpg PnG 등의 파일도 검사를 통과합니다.
if (!in_array($file_extension, $extension)) {
die("파일 확장자를 확인해 주세요. \n'jpg', 'jpeg', 'png', 'gif'만 사용이 가능합니다.");
}
파일 확장자 검사를 실패하면 실패 메시지를 출력합니다.
$sql = $sql . ", profile_img_path='$file_name'";
$upload_path = $upload_dir . $file_name;
확장자 검사를 통과하면 sql문에 프로필 파일 이름을 수정하는 구문을 추가하고, 프로필 사진을 업로드할 경로를 $upload_path에 설정합니다.
if(move_uploaded_file($file['tmp_name'], $upload_path)) {
$img_result = true;
}
move_uploaded_file 함수를 이용해 임시 폴더에 저장되었던 프로필 사진을 우리가 설정한 경로로 이동시킵니다.
그리고 이동에 성공하면 $img_result를 true로 설정합니다.
$sql = $sql . " where user_id='$user_id'";
// echo "$sql";
$result = runSQL($sql);
모든 업데이트문을 완성하면 $sql에 유저를 식별하는 where문을 추가하고 update문을 실행합니다.
echo json_encode(["result" => $result, "password" => $pass_result, "img" => $img_result]);
echo를 이용해 js의 updateSubmit 함수로 결과를 담아 응답을 보냅니다.