フレームワークなしでPHPを用いてセッション認証機能を構築する(Ⅲ.開発<part2>)

本記事は、フレームワークなしでPHPを用いてセッション認証機能を構築する(Ⅲ.開発<part1>)の続きです。

Ⅲ.開発<part2>

  1. Authクラスの作成
  2. ロジックとルーティングを管理するコントローラーの作成
  3. 汎用関数をまとめたファイルの作成

1.Authクラスの作成

/classes/Auth.php

<?php require_once ( dirname(__FILE__) . '/../database/dbconnect.php'); 
require_once ( dirname(__FILE__) . '/../functions.php'); 
/** * Authクラス(認証関連のフィールドとメソッドの集まり) 
* ■フィールド 
* $email、$username、$password、$confirm_password、$varidate_err 
* * ■メソッド 
* ①ユーザー作成(createUser) 
* ②バリデーション(validate) 
* ③サインアップ(signUp) 
* ④サインイン(signIn) 
* ⑤Eメールでユーザー検索(searchUserBasedEmail) 
* ⑥セッションから認証状態のチェック(checkIsAuthenticated) 
* ⑦サインアウト(signOut) 
*/ 

class Auth 
{ 
  // オブジェクト指向について 
  // アクセス修飾子(private、protected、public)でカプセル化 
  private $email; 
  private $username; 
  private $password; 
  private $confirm_password; 
  private $varidate_err = []; 

  /** * コンストラクタ(インスタンス生成時に動的に値を渡して、フィールドに値をセットできる) 
  * マジックメソッド(特殊な状況下で実行されるメソッド) 
  * */ 

  public function __construct($email, $password, $username, $confirm_password) 
  { //初期化 $this->email = $email;
    $this->password = $password;
    $this->username = $username;
    $this->confirm_password = $confirm_password;
  }

    /**
     * phpDoc
     */

    /**
     * @return bool $hasCreated
     */

    // ①ユーザー作成(createUser)
    public function createUser() 
    {
      /**
       * ;DELETE FROM user--"などのsqlインジェクション対策
       * プレースホルダでSQL文の可変値を「$1」「$2」あるいは「?」などの特別な文字列で設定しておき、後から埋め込む
       */
      $sql = 'INSERT INTO users (name, email, password) VALUES (?, ?, ?)';
      $hasCreated = false;
      
      try {
          // PDOStatementオブジェクト->prepare
          $stmt = connectToDatabase()->prepare($sql);
          // executeはプリペアドステートメントを実行する。成功した場合に true を、失敗した場合に false を返す。
          $hasCreated = $stmt->execute([$this->username, $this->email, password_hash($this->password,PASSWORD_DEFAULT)]);  // パスワードはハッシュ化
              
      } catch(\Exception $e) {
        echo h($e);
      }
      return $hasCreated;
    }

     /**
     * @param string $authenticationOperation
     * @return array $varidate_err
     */

    /**
     * ②バリデーション(validate)
     */
    public function validate($authenticationOperation) 
    {
      if(empty($this->email)) {
        $this->varidate_err['email_blank'] = 'メールアドレスを入力してください。';
      }
      
      if ($authenticationOperation === "signin") {
        if(empty($this->password)) {
          $this->varidate_err['password_blank'] = 'パスワードを入力してください。';
        }

      } else {
        if(empty($this->username)) {
          $this->varidate_err['username_blank'] = 'ユーザ名を入力してください。';
        }
  
        // 正規表現(preg_match)
        if (!preg_match("/\A[a-z\d]{8,100}+\z/i", $this->password)) {
          $this->varidate_err['password_invalid'] = 'パスワードは英数字8文字以上100文字以下にしてください。';
        }
        
        if ($this->password !== $this->confirm_password) {
          $this->varidate_err['confirm_password_invalid'] = '確認用パスワードと異なっています。';
        }
      }
      
      return $this->varidate_err;
    }

     /**
     * @param bool $hasCreated
     * @param string $email
     * @return void
     */

    // ③サインアップ(signUp)
    public static function signUp($hasCreated, $email) // static
    {
      if(!$hasCreated) {
        $_SESSION['err']['user_registration_failure'] = 'ユーザー登録に失敗しました';
        header('Location: ../views/signup.php');
        exit();
      }

      /**
       * $thisは、自分自身のオブジェクトを指し、インスタンス化した際、クラス内のメンバ変数やメソッドにアクセスする際に使用
       * self:: は、自クラスを示します。クラス定数、static変数については、インスタンス化せずに使用します。そのため、$thisは使用せず、代わりにselfを使用します。
       */
      $newUser = self::searchUserBasedEmail($email);
      session_regenerate_id(true); // セッションハイジャック対策(「セッション」を窃取し、本人に成り代わって通信を行うというサイバー攻撃 = なりすまし)のため、必ずセッションIDを再生成する
      $_SESSION['signin_user'] = $newUser;
      unset($_SESSION['err']); // サインインできたら、セッションのエラーを消去する
      header('Location: ../views/mypage.php');
    }

    /**
     * ④サインイン(signIn)
     * @return void
     */
    public function signIn()
    {
      $target_user = $this->searchUserBasedEmail($this->email); // ユーザーをemailから検索して取得
      // emailが一致するユーザーがいなかったら早期リターン
      if (count($target_user) === 0) {
        $_SESSION['err']['email_invalid'] = 'メールアドレスが不正です。';
        header('Location: ../views/signin.php');
        exit();
      }
      
      // パスワードが不一致なら早期リターン(password_verifyはハッシュされたパスワードとベタがきのパスワードが正しいかチェックできる)
      if (!password_verify($this->password, $target_user['password'])) {
        header('Location: ../views/signin.php');
        exit();
      }
      
      // emailとパスワードが一致した場合はサインイン
      session_regenerate_id(true); // セッションハイジャック対策(「セッション」を窃取し、本人に成り代わって通信を行うというサイバー攻撃 = なりすまし)のため、必ずセッションIDを再生成する
      $_SESSION['signin_user'] = $target_user;
      unset($_SESSION['err']); // サインインできたら、セッションのエラーを消去する(unset = 変数や配列の特定の要素を削除)
      header('Location: ../views/mypage.php');
    }
    
    /**
     * @param string $email
     * @return array|bool $user|false
     */

    // ⑤Eメールでユーザー検索(searchUserBasedEmail)
    public static function searchUserBasedEmail($email)
    {
      $sql = 'SELECT * FROM users WHERE email = ?';

      try {
        $stmt = connectToDatabase()->prepare($sql);
        // execute(array|null)
        $stmt->execute([$email]);
        $user = $stmt->fetch(); // fetchメソッドでSQLの結果を返すことが可能
        return $user;
        
      } catch(\Exception $e) {
        echo h($e);
        return false;
      }
    }

    /**
     * @return bool $isAuthenticated
     */

    // ⑥セッションから認証状態のチェック(checkIsAuthenticated)
    public static function checkIsAuthenticated()
    {
      $isAuthenticated = false;
      
      // セッションにサインインユーザが入っていればtrue
      if (isset($_SESSION['signin_user'])) {
        $isAuthenticated = true;
      }
      return $isAuthenticated;
    }

  /**
   * @return void
   */

    // ⑦サインアウト(signOut)
    public static function signOut()
    {
      //セッションを破棄($_SESSIONはphpからセッションデータにアクセスするためのスーパーグローバル変数)
      $_SESSION = array();
      session_destroy();
      header('Location: ../views/signin.php');
    }
}

2.ロジックとルーティングを管理するコントローラーの作成

サインアップに関する処理

/controllers/signup_controller.php

<?php 

/** * サインアップ画面からマイページへのルーティングなどの処理 
* ①csrfトークンの照合 
* ②postされたusername、email、password、confirm_passwordのバリデーション 
* ③ユーザー登録処理と認証 */ session_start(); require_once(dirname(__FILE__) . '/../classes/Auth.php'); 

/** * ①csrfトークンの照合 
*/ if (!isset($_SESSION['csrf_token']) || filter_input(INPUT_POST, 'csrf_token') !== $_SESSION['csrf_token']) { 
  exit('不正なリクエスト'); 
} 
 
unset($_SESSION['csrf_token']); 

// ②postされたusername、email、password、confirm_passwordのバリデーション 
$auth = new Auth(filter_input(INPUT_POST, 'email'), filter_input(INPUT_POST, 'password'), filter_input(INPUT_POST, 'username'), filter_input(INPUT_POST, 'password_conf')); 
$signup_error = $auth->validate("signup");

if (count($signup_error) > 0) {
  $_SESSION['err'] = $signup_error;
  header('Location: ../views/signup.php');
  exit();
}

// ③ユーザー登録処理と認証
$auth = new Auth(filter_input(INPUT_POST, 'email'), filter_input(INPUT_POST, 'password'), filter_input(INPUT_POST, 'username'), $confirm_password = null);
$hasCreated = $auth->createUser();
Auth::signUp($hasCreated, filter_input(INPUT_POST, 'email'));

サインインに関する処理

/controllers/signin_controller.php

<?php /** * サインイン画面からマイページへのルーティングなどの処理 
* ①csrfトークンの照合 * ②postされたemailとpasswordのバリデーション 
* ③サインイン処理 
*/ 

// この位置で書くと、require_onceの読み込み先ファイルでもsessionが使用できる 
session_start(); require_once(dirname(__FILE__) . '/../classes/Auth.php'); 

/** * ①csrfトークンの照合 
* セッションにトークンがない、もしくはフォームで送信されたトークンと一致しない場合、処理を中止 
*/ if (!isset($_SESSION['csrf_token']) || filter_input(INPUT_POST, 'csrf_token') !== $_SESSION['csrf_token']) { exit('不正なリクエスト'); } 

unset($_SESSION['csrf_token']); // セッションの中のcsrf_tokenを削除(ワンタイム利用のため) フォーム画面以外からのセッションや、二重送信(フォーム再送信のconfirmからの送信)を対策 

/** * ②postされたemailとpasswordのバリデーション(メールアドレス、パスワードが未入力でないかチェックする) 
* Authクラスのインスタンス化(->)
*/
$auth = new Auth(filter_input(INPUT_POST, 'email'), filter_input(INPUT_POST, 'password'), $username = null, $confirm_password = null);
$signin_error = $auth->validate("signin");

// count
if (count($signin_error) > 0) {
    $_SESSION['err'] = $signin_error;
    header('Location: ../views/signin.php');
    exit();
}

// ③サインイン処理
$auth->signIn();

サインアウトに関する処理

/controllers/signout_controller.php

<?php

/**
* マイページ画面からのサインアウト処理
* ①サインアウト処理
* ②セッションを削除
*/

session_start();
require_once(dirname(__FILE__) . '/../classes/Auth.php');

// サインアウトボタンを押してサインアウトを実行しているか
if (!$signout = filter_input(INPUT_POST, 'signout')) {
  exit('不正なリクエストです。');
}

// ①サインアウト処理、②セッションを削除
Auth::signOut();

3.汎用関数をまとめたファイルの作成

/functions.php

<?php

/**
 * @param string $str 対象の文字列
 * @return string 処理された文字列
 */

 /**
* xss対策(エスケープ処理)
* ユーザーが入力した値などをechoで出力する際にはエスケープ処理が必ず必要(フォーム等で入力された悪意のあるスクリプトの実行を無効化できる)
* htmlspecialchars( 変換対象, 変換パターン, 文字コード ) 
* ENT_QUOTESは特殊文字のうちシングルクォーテーションとダブルクォーテーションも変換対象に含めるようになる
* < → &lt;
* > → &gt;
* " → &quot;
* laravelのbladeでは{{ name }}
*/
function h($str) {
  return htmlspecialchars($str, ENT_QUOTES, 'UTF-8');
}

/**
 * @return string $csrf_token
 */

 /**
  * csrfトークンはCSRF対策用のワンタイムトークン
  * csrfトークンはざっくりいうと、なりすましを防止する役割。基本的にPOSTリクエストには必須
  */
function generateCsrfToken() {
  $csrf_token = bin2hex(random_bytes(32));
  return $csrf_token;
}

今回は以上になります。

コメント

タイトルとURLをコピーしました