마이페이지 만들기

비밀번호변경 기능이 있는 마이페이지를 만들어보자

기본적인 틀은 회원가입의 틀을 가져와 사용한다.

 

CSRF 공격을 방지하기위해 비밀번호 변경 시 현재 비밀번호를 입력하도록 구성한다.


mypage.php

    <?php
      // 사용자 정보를 받아와 화면에 그린다.
      $user = getUserInfo();
       // post로 전달 받았다면 비밀번호 로직츨 처리한다.
        if ($_SERVER['REQUEST_METHOD'] == 'POST') {
          // 비밀번호 변경
          $is_currentPw = isset($_POST['currentPw']) && $_POST['currentPw'];
          $is_newPw = isset($_POST['newPw']) && $_POST['newPw'];
          $is_submit = isset($_POST['submit']);
          
		  // 현재 비밀번호를 입력 받아서 맞는지 확인.
          $checkPw = checkCurrentPw($_POST['currentPw']);
          
          // 현재 비밀번호를 입력받고, 새 비밀번호를 입력받고, 현재 비밀번호와 입력받은 현재 비밀번호가 같고
          // 새로 입력받은 비밀번호로 update가 성공한다면
          if ($is_currentPw && $is_newPw && $is_submit 
          && $checkPw  && updatePassword($_POST['newPw']) ) {
              // alert을 띄우고 현재 로그인 페이지로 이동
              echo "<script>alert('비밀번호 변경 완료.');
              location.href='/login.php'
              </script>";
          }
        } 
    ?>

 

// 입력 태그
<div class="container">
  <main>
    <div class="py-5 text-center">
      <img class="d-block mx-auto mb-4" src="../assets/images/normaltic_logo.png" alt="" width="72" height="57">
      <h2>마이 페이지</h2>
      <!-- <p class="lead">하기의 항목을 기입해주세요.</p> -->
    </div>

    <div class="row g-5">
      
      <div class="">
        <!-- <h4 class="mb-3">Billing address</h4> -->
        <form class="needs-validation" novalidate method="POST">
          <div class="row g-3">
            
            <div class="">
              <label for="아이디" class="form-label">아이디</label>
              <input readonly type="text" value=
              <?php
                echo '"'.$user['id'].'"';
                ?>
              class="form-control" name="id" >
              <div class="">
                
              </div>
            </div>
          
            <div class="">
              <label for="이름" class="form-label">이름</label>
              <input readonly type="text" value=
              <?php
                echo '"'.$user['name'].'"';
                ?>
              name="name" class="form-control" id="username" >
              <div class="">
              </div>
            </div>

            <div class="">
              <label for="현재 비밀번호" class="form-label">현재 비밀번호</label>
              <input type="password" name="currentPw" class="form-control" id="username" required>
              <?php
                if(isset($checkPw)) {
                    echo '비밀번호가 잘못되었습니다.';
                    echo "<br>";
                }
                ?>
              </div>
            </div>

            <div class="">
              <label for="변경할 비밀번호" class="form-label">변경할 비밀번호</label>
              <input type="password" name="newPw" class="form-control" id="username" required>
              <div class="">
              </div>
            </div>

            <input type=text vlaue="submit" style="display:none;" name="submit"/>

          <button class="w-100 btn btn-primary btn-lg" type="submit">비밀번호 변경</button>
        </form>
      </div>
    </div>
  </main>

 

mypage_function.php

<?php
ini_set('display_errors', 1);
require '/app/lib/db_connection.php';

function getDbConn() {
  if (!isset($db_conn)) {
    $db_conn = mysqli_connect(DB_SERVER, DB_USERNAME, DB_PASSWORD, DB_NAME);
    $db_conn -> set_charset('utf8');
  }
  return $db_conn;
}

function getUserInfo() {
  $db_conn = getDbConn();
  $token = $_COOKIE['REFRESH_TOKEN'];
  $sql = 'SELECT USER_ID as id, USER_NM  as name FROM user
  where REFRESH_TOKEN = ?';
  $stmtSQL = $db_conn->prepare($sql);
  $stmtSQL->bind_param("s",$token);
  $stmtSQL->execute();
  $result = $stmtSQL->get_result(); 
  $user = mysqli_fetch_array($result);
  // var_dump($user);
  return $user;
}

function updatePassword($newPw) {
  $db_conn = getDbConn();
  $token = $_COOKIE['REFRESH_TOKEN'];
  $newPw = hash("sha256", $newPw);

  $sql = "update user set PW = ? where REFRESH_TOKEN = ?";
  $stmtSQL = $db_conn->prepare($sql);
  $stmtSQL->bind_param('ss', $newPw,$token);
  $result = $stmtSQL->execute();
 
  return $result;
}

// CSRF공격을 방지하기위해 현재 비밀번호를 입력받고 확인하는 절차를 추가한다.
function checkCurrentPw($currentPw) {
  $db_conn = getDbConn();
  $token = $_COOKIE['REFRESH_TOKEN'];
  $currentPw = hash("sha256", $currentPw);

  $sql = "SELECT count(USER_ID) as count FROM user where REFRESH_TOKEN = ? and PW = ?";

  $stmtSQL = $db_conn->prepare($sql);
  $stmtSQL->bind_param("ss",$token, $currentPw);
  $stmtSQL->execute();
  $result = $stmtSQL->get_result(); 
  $user = mysqli_fetch_array($result);
  return $user['count'];
}

?>

 

html_entities

html_entities는 php의 함수로 각종 태그들을 html Entity로 변환하는 함수이다. 이를 통해 XSS를 방지 할 수 있다.


 

writer.php(게시물 등록)

<?php
  $isetTitle = isset($_POST['title']) && strlen($_POST['title']) > 0;
  $issetContent = isset($_POST['content']) && strlen($_POST['content']) > 0;
  $isSubmit = isset($_POST['is_submit']) && strlen($_POST['is_submit']) > 0;
 
  if ($_SERVER['REQUEST_METHOD'] == 'POST') {
    
    if ($isetTitle && $issetContent && $isSubmit) {

      // 저장하기
      $title = $_POST['title'];
      $content = $_POST['content'];
      echo "before:".$content.'<br>';
      // 게시물 등록 SQL로 데이터를 전달하기 전 
      // htmlentities함수를 사용하여 태그를 변환한다.
      $title = htmlentities($title);
      $content = htmlentities($content);
      // echo $title.'<br>';
      // echo "after:".$content.'<br>';
      $result = insert_tbl_board($title, $content);
      if($result) {
        echo "<script>alert('작성 완료.');
                location.href='/board/list.php'
                </script>";
                exit;
      }

    }
  }

?>

 

글쓰기에 script를 삽입 후 내용을 확인해보자

summernote의 태그는 &lt인데 내가 작성한 글은 lt;와 같은 방식으로 다르게 저장된다... 이유는 모르겠다...

아시는 분은 댓글 부탁드립니다.

script와 alert을 적용
변환된 태그들이 저장되었다.

 

조회 화면에서 decode를 하여 화면에 뿌리게되면.

list.php

<tbody>
          <?php
			// 입력받은 페이지
            $page = isset($_GET["page"])? $_GET["page"] : 1;
            // 입력받은 검색타입
            $searchType = isset($_GET["searchType"])? $_GET["searchType"] : '';
            //입력받은 검색키워드
            $searchValue = isset($_GET["searchValue"])? $_GET["searchValue"] : '';
            // 한 페이지당 보여줄 개수
            $itemPerPage = 10;
            
			// limit 0, 10은 0개를 패스하고 10개까지 보여준다.
            // 페이지가 2라면 10 개를 패스하고 10개를 보여줘야하기에 limit 10, 10이 되어야한다.
            $boardList = getBoardList(($page-1) * $itemPerPage, $itemPerPage, $searchType, $searchValue);

            while ($board =  mysqli_fetch_array($boardList)) 
            {
              $idx = $board['idx'];
              // htmlentitiy 디코드
              $title = html_entity_decode($board['title']);
              $content = html_entity_decode($board['content']);

              $regUser = $board['regUser'];
              $regTime = $board['regTime'];

              // var_dump($content);
              echo "<tr onclick='moveDetail({$idx})'>";
              echo "<th  class='idx' scope='row'>{$idx}</td>";
              echo "<td class='title' >{$title}</td>";
              echo "<td class='content'>{$content}</td>";
              echo "<td class='reg_user'>{$regUser}</td>";
              echo "<td class='reg_time'>{$regTime}</td>";
              echo "</tr>";
            }
            
          ?>

        </tbody>

스크립트가 삽입되어도 동작하지 않는다.....

Summernote

섬머노트는 "이지윅 에디터"로 우리들이 사용하는 게시판등에서 글꼴, bold 혹은 이미지 삽입 등을 사용할 때 사용하는 기능들을 추가해주는 라이브러리이다.

https://summernote.org/

 

티스토리의 이지윅

이와같은 기능을 간편하게 사용할 수 있는 라이브러리 중 하나로 Summernote이다.

 

오늘은 섬머노트를 사용하여 이미지 삽입, 및 css 적용을 진행한다.


writer.php(게시물 등록)

필요한 라이브러리를 import 받는다 이는 css, jquery 등이 포함된다.

....
// 관련 라이브러리를 cdn으로 import
<!-- include libraries(jQuery, bootstrap) -->
<link href="https://stackpath.bootstrapcdn.com/bootstrap/3.4.1/css/bootstrap.min.css" rel="stylesheet">
<script src="https://code.jquery.com/jquery-3.5.1.min.js"></script>
<script src="https://stackpath.bootstrapcdn.com/bootstrap/3.4.1/js/bootstrap.min.js"></script>

<!-- include summernote css/js -->
<link href="https://cdn.jsdelivr.net/npm/summernote@0.8.18/dist/summernote.min.css" rel="stylesheet">
<script src="https://cdn.jsdelivr.net/npm/summernote@0.8.18/dist/summernote.min.js"></script>
<div style="width: 100%; height:100%">
....

 

섬머노트가 적용될 태그 하위에 섬머노트 태그를 작성

<div style="width: 80%; height:80%; margin: 50px"> 
    <form method="POST" id="write_form">
      <div style="height: 20%;"> 
        <div class="form-floating mb-3">
          <input type="text" name="title" class="form-control" id="floatingInput" placeholder="name@example.com"
          value="<?php
              if ($isSubmit) {
                echo $_POST['title'];
              }
            ?>"/>
          <label for="floatingInput">제목</label>
        </div>
      </div>
  
  
        <div >
          <div class="form-floating">
          // 섬머노트를 textarea 태그로 생성하였으며 이는 데이터를 전송할 form을 사용하기 위함
          // div도 가능하며 id를 "summernote"로 주면 된다.
          <textarea id="summernote" name='content'></textarea >
        </div>
        
          <input hidden/ value="submit" name="is_submit">
        

        <div style="margin-top: 20px;">

          <button onclick="formSubmit()" style="margin-left: 20px" type="button" class="btn btn-primary" >작성</button>
          <span>
            <?php
              if($isSubmit)
              echo "제목, 내용을 확인해주세요";
            ?>
          </span>
        </div>

    </form>

  </div>

 

 

섬머노트의 틀은 적용이 된 모습을 확인 할 수 있다.

그러나 이미지 삽입의 경우 동작하지만 저장되지는 않을 것 이다. 이는 이미지 삽입의 동작 원리를 알아야한다.

 

1. 이미지 삽입

2. 이미지가 서버에 올라가 특정 경로에 저장된다.

3. 이미지가 저장된 경로와 저장된 파일명을 화면으로 반환한다.

4. 경로와 파일명으로 하면에 img태그를 상성한다.

5. 저장버튼을 눌러 저장한다.

6. 저장 내용은 이미지 태그를 포함한 내용이 저장된다.

위 과정을 위해 이미지 업로드 기능을 만들어야한다.

 

writer.php (섬머노트의 업로드 동작을 위한 스크립트)

<script>
    $(document).ready(function() {
    // 높이, 언어, 폰트 등 다양한 설정을 할 수 있는 섬머노트의 옵션설정
        var $summernote = $('#summernote').summernote({
          lang: 'ko-KR',
          height: 400,
          // 콜백 함수 특정 기능들을 할 때 수행할 함수를 지정한다.
          callbacks: {
          // 이미지 업로드를 하면 아래의 함수가 동작한다.
            onImageUpload: function(files) {
              if(files.length > 4) {
                alert('파일은 3개 까지만 등록 할 수 있습니다.')
                return
              }
              for(var i = 0; i < files.length; i++){
              // 각각의 파일들을 sendFile이라는 함수의 인자로 전달한다.
                sendFile($summernote, files[i])
              }
            }
          }
        });
    });

    function sendFile(el, file) {
      console.log('111')
      console.log(el)
    var formData = new FormData();
    formData.append("file", file);
    // file 데이터를 ajax를 통해 uplload_file.php로 Post방식으로 전달한다.
    $.ajax({
        url: '/board/upload_file.php',
        data: formData,
        cache: false,
        contentType: false,
        processData: false,
        type: 'POST',
        success: function (data) {
            if(data==-1){
                alert('용량이 너무크거나 이미지 파일이 아닙니다.');
                return;
            }else{
                console.log(window.location.href+data)
                // 반환 받은 이미지 경로를 통해 summernote의 내부에 img 태그를 생성한다.
                el.summernote('insertImage', data, function ($image) {
                    $image.attr('src', data);
                    $image.attr('class', 'childImg');
                });
                var imgUrl=$("#imgUrl").val();
                if(imgUrl){
                    imgUrl=imgUrl+",";
                }
                $("#imgUrl").val(imgUrl+data);
            }
        }
    });
  }

  function formSubmit() {
    var form = document.getElementById('write_form')
    form.submit()
  }
  </script>

 

upload_file.php (파일 업로드 작업을하는 서버코드)

<?php
// ini_set('display_errors', 1);
// POST 방식일 때만 동작
  if ($_SERVER['REQUEST_METHOD'] == 'POST') {
    if($_FILES['file']['size']>10240000){//10메가
      echo "-1";
      exit;
    }

    $ext = substr(strrchr($_FILES['file']['name'],"."),1);
    $ext = strtolower($ext);

    if ($ext != "jpg" and $ext != "png" and $ext != "jpeg" and $ext != "gif")
    {
      echo "-1";
      exit;
    }

	// 이름의 중복이 있을 수 있기때문에 rand함수를 사용하여 임의의 문자열로 만든다.
    $name = "mp_".$now3.substr(rand(),0,4);
    $filename = $name.'.'.$ext;
    $date = date("Y-m-d",time());
    $dir ='../files/'.$date;
    
    //files의 경로에 오늘 날짜로 된 디렉토리가 없다면 디렉토리를 생성한다.
    // 이는 관리를 수월하게 하기위함일 뿐 없어도 문제는 없다.
    if (!file_exists($dir)) {
      mkdir($dir, 0777, true);
    }
    
    $destination = $dir.'/'.$filename;
    $location =  $_FILES["file"]["tmp_name"];
    // 전달받은 file을 files/오늘날짜/파일명. 확장자 형식으로 저장한다.
    move_uploaded_file($location,$destination);
    
    //이미지 파일의 경로를 반환한다.
    echo '/files/'.$date.'/'.$filename;
  }
?>

 

이미지를 업로드하면 "/files/오늘날짜/파일명.확장자" 형식으로 파일이 저장 되는 것을 확인한다.

 

png 파일 생성 확인

 

summernote 태그 내부에도 이미지가 삽입된 것을 확인

summernote에서 html 태그를 확인 할 수도 있다 "</>" 버튼을 눌러보자

이미지 삽입이 된 것을 확인
이미지 태그에 반환 받은 경로가 들어있는 것을 확인

 

다음에는 XSS방지를위한 html_entites를 적용하여 해당 내용을 저장하는 기능을 추가해보자.

 

Preparedstatement

프리페어드 스테이트먼트(prepared statement), 파라미터라이즈드 스테이트먼트(parameterized statement)는 데이터베이스 관리 시스템(DBMS)에서 동일하거나 비슷한 데이터베이스 문을 높은 효율성으로 반복적으로 실행하기 위해 사용되는 기능이다. 일반적으로 쿼리나 업데이트와 같은 SQL 문과 함께 사용되는 프리페어드 스테이트먼트는 템플릿의 형태를 취하며, 그 템플릿 안으로 특정한 상수값이 매 실행 때마다 대체된다. -위키백과-


Preparedstatement는 보안적 측면에서 SQL Injection을 예방할 수 있는 방법이다. SQL문을 미리 변환하기 때문에 문자 ', " <, > (, ) 와같은 SQL에 사용되는 특수문자들이 SQL 구문이 아닌 문자열로 인식 할 수 있게 해준다.

 

로그인과 게시판 조회에 Preparedstatement를 적용해보자.

login_function.php

<?php
    require_once 'db_connection.php';
   
   ...

    //로그인로직
    function login($id, $pw) {
        global $db_conn;
        $pw = hash("sha256", $pw);
		
        // 바인딩 SQL 구문에 ?를 넣는다.
        $stmtSQL =$db_conn ->prepare("SELECT COUNT(USER_ID) as count 
        FROM user WHERE USER_ID = ? and PW = ?");
        
        // 바인딩할 값을 지정 한다, integer일 경우 i, 문자열의 경우 s 
        // 아래의 예시는 아이디와 비밀번호 모두 문자열이기때문에 첫 인자를 ss로 넣어준다.
        // pw가 integer라면 "si"가 된다.
        $stmtSQL->bind_param("ss", $id, $pw);
        $stmtSQL->execute();
        $result = $stmtSQL->get_result(); 

        $row = mysqli_fetch_array($result);
        var_dump($row);
        
        return $row['count'];
    }
...
?>

 

 

위의 쿼리는 select로 결과값을 가져올 수 있지만 insert, update등은 결과 값이 없기에 성공여부를 가져오기위해 아래와 같은 방법으로 확인한다.

 function register($id, $name, $pw) {
        global $db_conn;
        
        $pw = hash("sha256", $pw);
        // echo "pw : {$pw}";

        $sql = "INSERT INTO user (USER_ID, USER_NM, PW) VALUE(
            '{$id}'
            , '{$name}'
            , '{$pw}'
        )";
        $stmtSQL =$db_conn -> prepare("INSERT INTO user (USER_ID, USER_NM, PW) 
        VALUE( ?, ?, ?)
        ");
        $stmtSQL->bind_param("sss", $id, $name ,$pw);
        
        // excute의 결과는 성공여부에 대한 boolean 값이다.
        $result = $stmtSQL->execute();
        var_dump($result);
       
        return $result;
    }

 

board_function.php(변경 전)

function getBoardList($currentPage, $itemPerPage, $searchType, $searchValue)  {
  $db_conn = getDbConn();
  $token = $_COOKIE['REFRESH_TOKEN'];

  $sql = "SELECT IDX AS idx
  , TITLE as title
  , CONTENT as content
  , FIRST_REG_USER as regUser
  , FIRST_REG_TIME as regTime
   FROM tbl_board ";
  echo "searchValue is {$searchValue}";
  if(!$searchValue=='' && $searchType == 'title') {
    $sql = $sql. "WHERE TITLE like'%{$searchValue}%' ";
  }

  $order = "ORDER BY IDX DESC limit {$currentPage},{$itemPerPage}";

  $sql = $sql.$order;

  echo $sql;
  return mysqli_query($db_conn, $sql);
}

 

board_function.php(변경 후)

function getBoardList($currentPage, $itemPerPage, $searchType, $searchValue)  {
  $db_conn = getDbConn();
  $token = $_COOKIE['REFRESH_TOKEN'];


  $sql = "SELECT IDX AS idx
  , TITLE as title
  , CONTENT as content
  , FIRST_REG_USER as regUser
  , FIRST_REG_TIME as regTime
   FROM tbl_board ";
  
  // 검색입력값이 있다면 적용
  if(!$searchValue == '' && $searchType == 'title') {
    $sql = $sql. "WHERE TITLE like ? ";
  }
  $order = "ORDER BY IDX DESC limit ?,?";
  $sql = $sql.$order;
  $stmtSQL = $db_conn->prepare($sql);
  
  // limit에 들어가는 값은 integer이기 때문에 i
  if(!$searchValue == '' && $searchType == 'title') {
    $stmtSQL->bind_param("sii", $searchValue, $currentPage, $itemPerPage);
  } else {
    $stmtSQL->bind_param("ii",$currentPage, $itemPerPage);
  }
  $stmtSQL->execute();
  $result = $stmtSQL->get_result(); 

  echo $sql;
  return  $result;
}

+ Recent posts