한걸음부터 다시 시작하기 프로젝트!
드디어 잔잔하고 소소한 프로젝트가 하나 끝났다.
https://github.com/silver-liq9118/php_website_basic
프로젝트요약
PHP, Apache, my SQL을 사용해 사용자 회원가입 부터 게시판을 볼수 있는 사이트를 구축한다.
<데이터>
- 모든 한국어 데이터 회원정보 및 게시글 콘텐츠는 데이터베이스에 저장된다.
- 파일은 파일데이터베이스에 따로기록되며 파일서버(localhost 물리path)에 저장되며 데이터베이스에는 경로만 저장한다.
<기능설계>
- 서비스접근제한
서비스를 가입하고 로그인한 사람만(세션유지) 모든 게시판기능에 접근할 수 있다. - 중복로그인제한
로그인되어 있는 사람이 로그인페이지에 접근시 다시 로그인을 받지 않고 다시 게시판 서비스를 이용하거나 로그아웃을 해서다시 접근해야한다. - 파일중복제한및 약간의 데이터보안
게시글 작성 시 파일은 파일데이터베이스에 따로 기록되며 실제파일은 따로 저장한다.
게시글 작성자가 올린 파일명과 실제 서버에 저장된 파일이름(유니크 난수)을 달리하여 사용자에게는 노출시키지 않도록 한다. - 웹 페이지 에러메세지를 노출하지 않고 메인페이지로 돌아가게 한다.
<보안점>
- 입력값검사
사용자 입력값은 모두 검증되어야한다. SQL인젝션이나 XSS에 노출되어있다. - 콘텐츠검사
콘텐츠검사가 따로 되지 않으므로 CSRF에 노출되어있다. - 파일검사
파일검사가 되지 않으므로 웹셸, 자바스크립트악용 취약점이 있다. - 파일패스 물리주소
파일패스가 인코딩되지않고 노출되고있다.
<해결방법>
- CSP통해 아파치 스크립트 원본을 제한한다.
- 시큐어 코딩을 통한 SQL, script 입력을 제한한다.
[주요기능]
- 회원가입
- 로그인
- 게시판보기, 파일다운로드하기
- 게시판읽기
- 게시판글찾기(제목)
- 게시판쓰기
- 게시판파일업로드하기
기스택요약
- 프로젝트 시간 : 약 3일
- 사용언어 : PHP, HTML, SQL
- 데이터베이스 : mySQL
- 서버 : Apache
- 사용툴 및 환경 : 챗 GPT, 휴먼지능(본인), VsCode, XAMPP
매일매일 삽질을 안하면 두드러기가나는 병에 걸렸다.
오늘의 삽질은 패스 인코딩을 하느라 애를먹었고 하드인코딩으로 넘기기로 했다.
또한 꼭꼭꼭 서버파일이름에는 _ underscore을 사용하지말자..
수정사항
Join.php / Database (users)
일단 몇가지 수정사항이 있어서 수정했다.
게시판에는 id(이메일)을 노출 시키지 않고 user을 노출시키는게 나을 것 같다고 판단했고,
유니크한 username이 없다면 혼동이 올것 같아 username을 유니크로 바꾸기로 했다.
username을 유니크로 받아오면서 기존의 회원가입 페이지에서 username도 중복확인 프로세스가 필요해졌다.
ID 와 username은 각각 중복확인이 되어야하며 그 어느것도 중복이 있는 항목이 있다면 회원가입을 할 수 없다.
그리고 사소한버그.. 캐시문제로 회원가입을 했다가 뒤로가기를 누르면 중복아이디가 생성될 수 있는점을 발견했다..
그래서 데이터베이스 email과 name을 유니크로 수정하고 php코드에서도 캐시를 지우도록 설정했다.
Board.php, Board_page.php
내 서비스에는 가입하고 로그인한 유저만 접근 가능하도록 설계했지만
board.php (기능)에는 비로그인유저가 접근 불가했지만 Board_page.php 페이지에 직접 접속은 가능하고 에러메세지도 출력됐다. 이점을 수정했다.
추가한기능
게시판 보기
쓰기만하면 관리자만 내용을 볼 수 있다. 게시판을 각각눌러서 컨텐츠를 볼 수 있도록했다.
각각 게시글마다 HTML을 생성하지 않고 동적으로 페이지를 보여줘야한다..
동적이라는 말은 사람을 참 힘들게 한다..
board_content_page.php (페이지)
이내용은 뒤에 파일 보기 및 읽기랑 중복되는부분이다.
사용자가 누른 게시글을보여주며 파일이 있다면 다운로드버튼을 활성화 시켜 다운받을 수 있게한다.
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>게시글 보기</title>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.3/css/all.min.css">
<style>
body {
font-family: Arial, sans-serif;
background-color: #f3f3f3;
padding: 20px;
}
h1 {
color: #333;
text-align: center;
}
.title{
font-family: Arial, sans-serif;
color: #666666;
font-size: 19px;
}
.container {
max-width: 500px;
margin: 0 auto;
background-color: #fff;
padding: 20px;
border-radius: 5px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.content {
margin-bottom: 20px;
}
.content h2 {
font-size: 24px;
margin-bottom: 10px;
}
.content p {
font-size: 16px;
line-height: 1.5;
}
.button-group {
display: flex;
justify-content: center;
margin-top: 20px;
}
.button-group button {
background-color: #007bff;
color: #fff;
border: none;
padding: 10px 20px;
border-radius: 3px;
cursor: pointer;
transition: background-color 0.3s;
font-family: Arial, sans-serif;
font-size: 16px;
}
.button-group button:hover {
background-color: #0056b3;
}
.container hr {
border: none;
border-top: 1px solid #ccc;
margin: 20px 0;
}
.info {
font-family: Arial, sans-serif;
font-size: 15px;
}
.download-link {
display: inline-block;
background-color: #007bff;
color: #fff;
padding: 8px 16px;
border-radius: 4px;
text-decoration: none;
transition: background-color 0.3s;
}
.download-link:hover {
background-color: #0056b3;
}
</style>
</head>
<body>
<h1>📄 게시글 보기 📄</h1>
<div class="container">
<div class="title">
<p><?php echo nl2br($title); ?><p>
</div>
<div class="info">
<p><i class="fas fa-user" style="color: #66666;"></i> <?php echo nl2br($author); ?><br>작성일 : <?php echo nl2br($created_at); ?> 수정일: <?php echo nl2br($updated_at); ?> </p></div>
<hr>
<div class="container">
<div class="content">
<p><?php echo nl2br($content); ?></p> </div>
</div>
<!--파일 가져오기-->
<?php
if (mysqli_num_rows($file_result)>0) {
// 파일 확장자에 따라 다운로드 링크 제공
// $allowedExtensions = array('pdf', 'doc', 'docx', 'xls', 'xlsx', 'txt'); // 다운로드 가능한 파일 확장자 목록
//$fileExtension = pathinfo($filename_user, PATHINFO_EXTENSION); // 파일 확장자 추출
//$fileName = pathinfo($filename_user, PATHINFO_FILENAME); // 확장자를 제외한 파일 이름 추출
//경로 인코딩이 도저히 안돼서절대경로로 변경
//echo "$filepath";
echo '<div class="button-group"><a href="' . $filepath . '" download class="download-link">';
echo '파일 다운로드</div></a>';
}
?>
<div class="button-group">
<button onclick="goToBoardPage()">뒤로 가기</button>
</div>
</div>
<script>
function goToBoardPage() {
window.location.href = "board.php";
}
</script>
</body>
</html>
뒤로가기를 누르면 다시 게시판 페이지로 돌아온다.
get_board_content.php (기능)
컨텐츠를 동적으로 가져온다...
동적... 구현이 다시 날 너무 힘들게했다..
기능도 파일 업로드코드랑 동시에 있다.
콘텐츠를 데이터 베이스에서 받아와 타이틀 및 생성날짜 등을 받아오고 출력한다.
<?php
include "db_conn.php";
if (session_status() === PHP_SESSION_NONE) {
session_start();
}
$postId = $_GET['id'];
$sql = "SELECT content,title,author, created_at , updated_at FROM posts WHERE id=$postId";
$result = $conn->query($sql);
if ($result) {
$row = $result->fetch_assoc();
$content = $row['content'];
$title = $row['title'];
$author = $row['author'];
$created_at = $row['created_at'];
$updated_at = $row['updated_at'];
$file_row = "";
$filename = "";
$filename_user = "";
$filepath = "";
$sql_file = "SELECT filename_user, filepath, filename FROM files WHERE post_id = $postId";
$file_result = $conn->query($sql_file);
if (mysqli_num_rows($file_result)>0) {
$file_row = $file_result->fetch_assoc();
$filename = $file_row['filename'];
$filename_user = $file_row['filename_user'];
$filepath = $file_row['filepath'];
return include "board_content_page.php";
}
else {
return include "board_content_page.php";
}
}
else {
return include "access_failed.html";
}
?>
게시판 검색
get_board_search.php (기능)
<?php
include "db_conn.php";
if (session_status() === PHP_SESSION_NONE) {
session_start();
}
if(isset($_SESSION['email']) && isset($_SESSION['username'])){
$email = $_SESSION['email'];
$username =$_SESSION['username'];
// 세션에서 유저 이름 가져오기
}
else {
$email = "";
$username ="";
}
try
{
if (!empty($email)) {
//로그인 했을 때
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$find_title =$_POST['find_title'];
}
else {
$find_title="";
}
// 게시판 검색 목록 불러오기
$sql = "SELECT id, title, author, created_at FROM posts WHERE title LIKE '%$find_title%'";
$result = $conn->query($sql);
if ($result && mysqli_num_rows($result) > 0) {
$rows = array(); // 결과 행을 저장할 배열
while ($row = $result->fetch_assoc()) {
$rows[] = $row; // 각 행을 배열에 추가
}
include "board_page.php";
}
else {
#검색값이 없을 때
$id=0;
$rows = 0;
$row=0;
include "board_page.php";
}}
else {
// 로그인 을 안했을 때
include "access_failed.html"; }
}
catch ( Exception $e ) {
// 오류 발생 시
include "access_failed.html";
}
?>
역시 로그인한 사람만 사용할 수 있고 제목 키워드를 통해서 게시물을 검색한다.
포함으로 검색된다. %키워드%
만약 검색한 파일이 없다면
게시글을 찾을 수 없다는 경고문을 보여주고 강제로 게시판 페이지로 다시 이동시키게 했다.
검색값이 없었을때 각변수 $id, $row등의 값을 0으로초기화 시키고 board.php 에서 불러오게 했다.
Board_page.php
if ($rows === 0) {
echo "<h3 class='not-link'>🔍 해당 게시글을 찾을 수 없습니다. 😓</h3>";
echo "<h4 class='not-link'>3초후 게시판으로 이동됩니다.</h3>";
echo "<div id='countdown'></div>";
echo "<script>";
echo "var countdownElement = document.getElementById('countdown');";
echo "var countdownTime = 3000;";
echo "function updateCountdown() {";
echo " countdownElement.textContent = (countdownTime / 1000);";
echo " countdownTime -= 1000;";
echo " if (countdownTime >= 0) {";
echo " setTimeout(updateCountdown, 1000);"; // 1초(1000ms)마다 업데이트
echo " } else {";
echo " window.location.href = 'board.php';";
echo " }";
echo "}";
echo "setTimeout(updateCountdown, 1000);"; // 카운트다운 시작
echo "</script>";
echo "<br>";
exit;
}
파일업로드/다운로드
기본적으로 파일은 posts 데이터베이스가 아닌 file이라는 데이터베이스를 추가해서 새로 경로만 받아왔다.
나름 안전성을 고려하고 데이터베이스 용량을 고려했다.
Database - files
file_id는 파일의 고유 id 이고 post_id와 연결되도록 해서 각 게시물의 파일을 알게 하게끔 외래키를 두었다.
또한 유저가 입력한 filename_user와 진짜 파일이름 filename 을 분리하여 다운로드하는 사용자는 저장된 파일이름을 모르게끔 보안을 조금더 높여보았다.
이러면 filename이 겹쳐도 중복쓰기가 불가능해진다.
파일업로드는 기본적으로 write_post_page.php 에서 기능을 추가했다.
파일 업로드
write_post_page.php (페이지)
<div class="button-group">
<label for="file"></label>
<!--<input type="file" accept=".doc,.docx"> 특정파일 허용 화이트리스트-->
<input type="file" id="file" name="file">
<input type="submit" value="작성하기">
<input type="button" value="뒤로가기" onclick="goToBoardPage()">
</div>
이 항목은 required로 설정되지 않았기때문에 사용자가 파일을 업로드 하고싶지 않으면 안해도 게시글을 작성할 수 있다.
특정파일만 허용하려고 했으나 일단 주석처리 하였다.
Write.php (기능)
if ($result_post) {
// 게시글 추가에 성공한 경우
$postId = $conn->insert_id;
// 다시 postId를 가져옴
if ($postId) {
// 파일을 추가하고 게시판 페이지로 돌아감
$uploadedFileName = $_FILES['file']['name'];
$uploadedFileExt = pathinfo($uploadedFileName, PATHINFO_EXTENSION); // 파일 확장자 추출
$uniqueFileName = uniqid() . '.' . $uploadedFileExt; // 고유한 파일 이름 생성
$uploadDirectory = 'C:\xampp\htdocs\somefile\PHPweb\file\\'; //인코딩 변경불가로 인해 하드코딩
$uploadedFilePath = 'http://localhost/somefile/PHPweb/file/' . $uniqueFileName; // //인코딩 변경불가로 인해 하드코딩
$fileDestination = $uploadDirectory . $uniqueFileName;
move_uploaded_file($_FILES['file']['tmp_name'], $fileDestination);
$sql_file = "INSERT INTO files (filename, filename_user, filepath, post_id) VALUES ('$uniqueFileName', '$uploadedFileName', '$uploadedFilePath', $postId)";
$result_file = $conn->query($sql_file);
}
게시글 추가에 성공한 경우만 방금 추가된 post_id를 file데이터베이스에 저장하고 연결될수 있도록하고
파일의 정보들을 각각 컬럼들에 저장했다.
파일은 데이터베이스에 저장되지않고 경로만 저장되며 진짜파일은 $fileDestination (파일서버)로 이동된다.
인코딩 디코딩 문제로 상대경로로 지정했다.
파일보기
get_board_content (기능)
나를힘들지 않게하는 기능이.. 없지만 기능을 추가할 수록 챌린지 지만 힘내서.. 했다
$file_row = "";
$filename = "";
$filename_user = "";
$filepath = "";
$sql_file = "SELECT filename_user, filepath, filename FROM files WHERE post_id = $postId";
$file_result = $conn->query($sql_file);
if (mysqli_num_rows($file_result)>0) {
$file_row = $file_result->fetch_assoc();
$filename = $file_row['filename'];
$filename_user = $file_row['filename_user'];
$filepath = $file_row['filepath'];
return include "board_content_page.php";
}
else {
return include "board_content_page.php";
}
파일의 각변수들을 ""로 초기화하고 만약 파일이 있으면 파일의 내용들을 다시 Board_content_page.php (페이지) 로 전송하게 했다.
파일이 없다면 유저에게 표시되지 않는다.
if (mysqli_num_rows($file_result)>0) {
// 파일 확장자에 따라 다운로드 링크 제공
// $allowedExtensions = array('pdf', 'doc', 'docx', 'xls', 'xlsx', 'txt'); // 다운로드 가능한 파일 확장자 목록
//$fileExtension = pathinfo($filename_user, PATHINFO_EXTENSION); // 파일 확장자 추출
//$fileName = pathinfo($filename_user, PATHINFO_FILENAME); // 확장자를 제외한 파일 이름 추출
//경로 인코딩이 도저히 안돼서절대경로로 변경
//echo "$filepath";
echo '<div class="button-group"><a href="' . $filepath . '" download="' . $filename . '" class="download-link">';
echo '파일 다운로드</div></a>';
}
파일 다운로드를 누르면 상대경로에서 각 파일이 게시글을 쓴사람 이름으로 다운받아 지지만,
실제로는 유니크한 파일이 다운받아지게 된다.
'사이드 프로젝트 도전기 > [PHP] 게시판 구축' 카테고리의 다른 글
[Project] *사용자 사이트 구축* / PHP / 3. 게시판 메인 페이지 및 쓰기 (0) | 2023.06.13 |
---|---|
[Project] *사용자 사이트 구축* / PHP / 2. 회원가입 (0) | 2023.06.10 |
[Project] *사용자 사이트 구축* / PHP / 1. 환경 구축 부터 로그인까지 (0) | 2023.06.10 |