사이드 프로젝트 도전기

[Project] *사용자 사이트 구축* / PHP / 4. 게시판 읽기 / 게시판 검색 /게시판 업로드& 다운로드 (Done)

Developer D 2023. 6. 14. 17:38

한걸음부터 다시 시작하기 프로젝트!

드디어 잔잔하고 소소한 프로젝트가 하나 끝났다.

https://github.com/silver-liq9118/php_website_basic

 

GitHub - silver-liq9118/php_website_basic: PHP Web site basic, just login and sign up

PHP Web site basic, just login and sign up. Contribute to silver-liq9118/php_website_basic development by creating an account on GitHub.

github.com


프로젝트요약

PHP, Apache, my SQL을 사용해 사용자 회원가입 부터 게시판을 볼수 있는 사이트를 구축한다.

<데이터>

  1. 모든 한국어 데이터 회원정보 및 게시글 콘텐츠는 데이터베이스에 저장된다.
  2. 파일은 파일데이터베이스에 따로기록되며 파일서버(localhost 물리path)에 저장되며 데이터베이스에는 경로만 저장한다.

<기능설계>

  1. 서비스접근제한
    서비스를 가입하고 로그인한 사람만(세션유지) 모든 게시판기능에 접근할 수 있다.
  2. 중복로그인제한
    로그인되어 있는 사람이 로그인페이지에 접근시 다시 로그인을 받지 않고 다시 게시판 서비스를 이용하거나 로그아웃을 해서다시 접근해야한다.
  3. 파일중복제한및 약간의 데이터보안
    게시글 작성 시 파일은 파일데이터베이스에 따로 기록되며 실제파일은 따로 저장한다.
    게시글 작성자가 올린 파일명과 실제 서버에 저장된 파일이름(유니크 난수)을 달리하여 사용자에게는 노출시키지 않도록 한다.
  4. 웹 페이지 에러메세지를 노출하지 않고 메인페이지로 돌아가게 한다.

 <보안점>

  1. 입력값검사
    사용자 입력값은 모두 검증되어야한다. SQL인젝션이나 XSS에 노출되어있다.
  2. 콘텐츠검사
    콘텐츠검사가 따로 되지 않으므로 CSRF에 노출되어있다.
  3. 파일검사
    파일검사가 되지 않으므로 웹셸, 자바스크립트악용 취약점이 있다.
  4. 파일패스 물리주소
    파일패스가 인코딩되지않고 노출되고있다.

<해결방법>

  1. CSP통해 아파치 스크립트 원본을 제한한다.
  2. 시큐어 코딩을 통한 SQL, script 입력을 제한한다.

[주요기능]

  1. 회원가입
  2. 로그인
  3. 게시판보기, 파일다운로드하기
  4. 게시판읽기
  5. 게시판글찾기(제목)
  6. 게시판쓰기
  7. 게시판파일업로드하기

기스택요약

  • 프로젝트 시간 : 약 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>&nbsp;&nbsp;<?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'>&#128269; 해당 게시글을 찾을 수 없습니다. &#128531;</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>';
        }

파일 다운로드를 누르면 상대경로에서 각 파일이 게시글을 쓴사람 이름으로 다운받아 지지만,

실제로는 유니크한 파일이 다운받아지게 된다.

 

 

 

반응형