sql >> データベース >  >> RDS >> Mysql

ユーザーアカウント管理、役割、権限、認証PHPおよびMySQL-パート2

    これは、ユーザーアカウント管理システム、認証、役割、権限に関するシリーズの第2部です。最初の部分はここにあります。

    データベース構成

    user-accountsというMySQLデータベースを作成します。次に、プロジェクトのルートフォルダ(user-accountsフォルダ)でファイルを作成し、config.phpという名前を付けます。このファイルは、データベース変数を構成し、アプリケーションを作成したMySQLデータベースに接続するために使用されます。

    config.php:

    <?php
    	session_start(); // start session
    	// connect to database
    	$conn = new mysqli("localhost", "root", "", "user-accounts");
    	// Check connection
    	if ($conn->connect_error) {
    	    die("Connection failed: " . $conn->connect_error);
    	}
      // define global constants
    	define ('ROOT_PATH', realpath(dirname(__FILE__))); // path to the root folder
    	define ('INCLUDE_PATH', realpath(dirname(__FILE__) . '/includes' )); // Path to includes folder
    	define('BASE_URL', 'http://localhost/user-accounts/'); // the home url of the website
    ?>

    また、ユーザー名などのログインしたユーザー情報を保存するために後で使用する必要があるため、セッションを開始しました。ファイルの最後に、ファイルのインクルードをより適切に処理するのに役立つ定数を定義しています。

    これで、アプリケーションがMySQLデータベースに接続されました。ユーザーが詳細を入力してアカウントを登録できるフォームを作成しましょう。プロジェクトのルートフォルダにsignup.phpファイルを作成します:

    signup.php:

    <?php include('config.php'); ?>
    <?php include(INCLUDE_PATH . '/logic/userSignup.php'); ?>
    <!DOCTYPE html>
    <html>
    <head>
      <meta charset="utf-8">
      <title>UserAccounts - Sign up</title>
      <!-- Bootstrap CSS -->
      <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/3.3.7/css/bootstrap.min.css" />
      <!-- Custom styles -->
      <link rel="stylesheet" href="assets/css/style.css">
    </head>
    <body>
      <?php include(INCLUDE_PATH . "/layouts/navbar.php") ?>
    
      <div class="container">
        <div class="row">
          <div class="col-md-4 col-md-offset-4">
            <form class="form" action="signup.php" method="post" enctype="multipart/form-data">
              <h2 class="text-center">Sign up</h2>
              <hr>
              <div class="form-group">
                <label class="control-label">Username</label>
                <input type="text" name="username" class="form-control">
              </div>
              <div class="form-group">
                <label class="control-label">Email Address</label>
                <input type="email" name="email" class="form-control">
              </div>
              <div class="form-group">
                <label class="control-label">Password</label>
                <input type="password" name="password" class="form-control">
              </div>
              <div class="form-group">
                <label class="control-label">Password confirmation</label>
                <input type="password" name="passwordConf" class="form-control">
              </div>
              <div class="form-group" style="text-align: center;">
                <img src="http://via.placeholder.com/150x150" id="profile_img" style="height: 100px; border-radius: 50%" alt="">
                <!-- hidden file input to trigger with JQuery  -->
                <input type="file" name="profile_picture" id="profile_input" value="" style="display: none;">
              </div>
              <div class="form-group">
                <button type="submit" name="signup_btn" class="btn btn-success btn-block">Sign up</button>
              </div>
              <p>Aready have an account? <a href="login.php">Sign in</a></p>
            </form>
          </div>
        </div>
      </div>
    <?php include(INCLUDE_PATH . "/layouts/footer.php") ?>
    <script type="text/javascript" src="assets/js/display_profile_image.js"></script>
    

    このファイルの最初の行には、config.phpがsignup.phpファイル内で提供するINCLUDE_PATH定数を使用する必要があるため、前に作成したconfig.phpファイルを含めています。このINCLUDE_PATH定数を使用して、データベースにユーザーを登録するためのロジックを保持するnavbar.php、footer.php、userSignup.phpも含まれています。これらのファイルはまもなく作成されます。

    ファイルの終わり近くに、ユーザーがプロフィール画像をアップロードするためにクリックできる丸いフィールドがあります。ユーザーがこの領域をクリックしてコンピューターからプロフィール画像を選択すると、最初にこの画像のプレビューが表示されます。

    この画像プレビューはjqueryで実現されます。ユーザーが[画像のアップロード]ボタンをクリックすると、JQueryを使用してプログラムでファイル入力フィールドがトリガーされます。これにより、ユーザーのコンピューターファイルが表示され、ユーザーがコンピューターを参照してプロフィール画像を選択できるようになります。彼らが画像を選択するとき、Jqueryを使用して画像を一時的に表示します。これを行うコードは、まもなく作成するdisplay_profile_image.phpファイルにあります。

    まだブラウザで表示しないでください。まず、このファイルに私たちが借りているものを与えましょう。今のところ、assets / cssフォルダー内に、headセクションでリンクしたstyle.cssファイルを作成しましょう。

    style.css:

    @import url('https://fonts.googleapis.com/css?family=Lora');
    * { font-family: 'Lora', serif; font-size: 1.04em; }
    span.help-block { font-size: .7em; }
    form label { font-weight: normal; }
    .success_msg { color: '#218823'; }
    .form { border-radius: 5px; border: 1px solid #d1d1d1; padding: 0px 10px 0px 10px; margin-bottom: 50px; }
    #image_display { height: 90px; width: 80px; float: right; margin-right: 10px; }

    このファイルの最初の行に、「Lora」という名前のGoogleフォントをインポートして、アプリのフォントをより美しくします。

    このsignup.phpで次に必要なファイルは、navbar.phpファイルとfooter.phpファイルです。 include/layoutsフォルダ内に次の2つのファイルを作成します。

    navbar.php:

    <div class="container"> <!-- The closing container div is found in the footer -->
      <nav class="navbar navbar-default">
        <div class="container-fluid">
          <div class="navbar-header">
            <a class="navbar-brand" href="#">UserAccounts</a>
          </div>
          <ul class="nav navbar-nav navbar-right">
              <li><a href="<?php echo BASE_URL . 'signup.php' ?>"><span class="glyphicon glyphicon-user"></span> Sign Up</a></li>
              <li><a href="<?php echo BASE_URL . 'login.php' ?>"><span class="glyphicon glyphicon-log-in"></span> Login</a></li>
          </ul>
        </div>
      </nav>

    footer.php:

        <!-- JQuery -->
        <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
        <!-- Bootstrap JS -->
        <script src="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/3.3.7/js/bootstrap.min.js"></script>
      </div> <!-- closing container div -->
    </body>
    </html>

    signup.phpファイルの最後の行は、display_profile_image.jsという名前のJQueryスクリプトにリンクしており、その名前が示すとおりに機能します。このファイルをassets/jsフォルダー内に作成し、このコードをその中に貼り付けます:

    display_profile_image.js:

    $(document).ready(function(){
      // when user clicks on the upload profile image button ...
      $(document).on('click', '#profile_img', function(){
        // ...use Jquery to click on the hidden file input field
        $('#profile_input').click();
        // a 'change' event occurs when user selects image from the system.
        // when that happens, grab the image and display it
        $(document).on('change', '#profile_input', function(){
          // grab the file
          var file = $('#profile_input')[0].files[0];
          if (file) {
              var reader = new FileReader();
              reader.onload = function (e) {
                  // set the value of the input for profile picture
                  $('#profile_input').attr('value', file.name);
                  // display the image
                  $('#profile_img').attr('src', e.target.result);
              };
              reader.readAsDataURL(file);
          }
        });
      });
    });

    そして最後に、userSignup.phpファイル。このファイルは、データベースで処理および保存するために登録フォームデータが送信される場所です。 include / logicフォルダ内にuserSignup.phpを作成し、その中にこのコードを貼り付けます:

    userSignup.php:

    <?php include(INCLUDE_PATH . "/logic/common_functions.php"); ?>
    <?php
    // variable declaration
    $username = "";
    $email  = "";
    $errors  = [];
    // SIGN UP USER
    if (isset($_POST['signup_btn'])) {
    	// validate form values
    	$errors = validateUser($_POST, ['signup_btn']);
    
    	// receive all input values from the form. No need to escape... bind_param takes care of escaping
    	$username = $_POST['username'];
    	$email = $_POST['email'];
    	$password = password_hash($_POST['password'], PASSWORD_DEFAULT); //encrypt the password before saving in the database
    	$profile_picture = uploadProfilePicture();
    	$created_at = date('Y-m-d H:i:s');
    
    	// if no errors, proceed with signup
    	if (count($errors) === 0) {
    		// insert user into database
    		$query = "INSERT INTO users SET username=?, email=?, password=?, profile_picture=?, created_at=?";
    		$stmt = $conn->prepare($query);
    		$stmt->bind_param('sssss', $username, $email, $password, $profile_picture, $created_at);
    		$result = $stmt->execute();
    		if ($result) {
    		  $user_id = $stmt->insert_id;
    			$stmt->close();
    			loginById($user_id); // log user in
    		 } else {
    			 $_SESSION['error_msg'] = "Database error: Could not register user";
    		}
    	 }
    }
    

    このファイルにはさらに作業が必要だったため、最後に保存しました。まず、このファイルの先頭にcommon_functions.phpという名前の別のファイルを含めています。このファイルから派生した2つのメソッド、つまり、validateUser()とloginById()を使用しているため、このファイルを含めています。これらはまもなく作成されます。

    include/logicフォルダに次のcommon_functions.phpファイルを作成します。

    common_functions.php:

    <?php
      // Accept a user ID and returns true if user is admin and false if otherwise
      function isAdmin($user_id) {
        global $conn;
        $sql = "SELECT * FROM users WHERE id=? AND role_id IS NOT NULL LIMIT 1";
        $user = getSingleRecord($sql, 'i', [$user_id]); // get single user from database
        if (!empty($user)) {
          return true;
        } else {
          return false;
        }
      }
      function loginById($user_id) {
        global $conn;
        $sql = "SELECT u.id, u.role_id, u.username, r.name as role FROM users u LEFT JOIN roles r ON u.role_id=r.id WHERE u.id=? LIMIT 1";
        $user = getSingleRecord($sql, 'i', [$user_id]);
    
        if (!empty($user)) {
          // put logged in user into session array
          $_SESSION['user'] = $user;
          $_SESSION['success_msg'] = "You are now logged in";
          // if user is admin, redirect to dashboard, otherwise to homepage
          if (isAdmin($user_id)) {
            $permissionsSql = "SELECT p.name as permission_name FROM permissions as p
                                JOIN permission_role as pr ON p.id=pr.permission_id
                                WHERE pr.role_id=?";
            $userPermissions = getMultipleRecords($permissionsSql, "i", [$user['role_id']]);
            $_SESSION['userPermissions'] = $userPermissions;
            header('location: ' . BASE_URL . 'admin/dashboard.php');
          } else {
            header('location: ' . BASE_URL . 'index.php');
          }
          exit(0);
        }
      }
    
    // Accept a user object, validates user and return an array with the error messages
      function validateUser($user, $ignoreFields) {
      		global $conn;
          $errors = [];
          // password confirmation
          if (isset($user['passwordConf']) && ($user['password'] !== $user['passwordConf'])) {
            $errors['passwordConf'] = "The two passwords do not match";
          }
          // if passwordOld was sent, then verify old password
          if (isset($user['passwordOld']) && isset($user['user_id'])) {
            $sql = "SELECT * FROM users WHERE id=? LIMIT 1";
            $oldUser = getSingleRecord($sql, 'i', [$user['user_id']]);
            $prevPasswordHash = $oldUser['password'];
            if (!password_verify($user['passwordOld'], $prevPasswordHash)) {
              $errors['passwordOld'] = "The old password does not match";
            }
          }
          // the email should be unique for each user for cases where we are saving admin user or signing up new user
          if (in_array('save_user', $ignoreFields) || in_array('signup_btn', $ignoreFields)) {
            $sql = "SELECT * FROM users WHERE email=? OR username=? LIMIT 1";
            $oldUser = getSingleRecord($sql, 'ss', [$user['email'], $user['username']]);
            if (!empty($oldUser['email']) && $oldUser['email'] === $user['email']) { // if user exists
              $errors['email'] = "Email already exists";
            }
            if (!empty($oldUser['username']) && $oldUser['username'] === $user['username']) { // if user exists
              $errors['username'] = "Username already exists";
            }
          }
    
          // required validation
      	  foreach ($user as $key => $value) {
            if (in_array($key, $ignoreFields)) {
              continue;
            }
      			if (empty($user[$key])) {
      				$errors[$key] = "This field is required";
      			}
      	  }
      		return $errors;
      }
      // upload's user profile profile picture and returns the name of the file
      function uploadProfilePicture()
      {
        // if file was sent from signup form ...
        if (!empty($_FILES) && !empty($_FILES['profile_picture']['name'])) {
            // Get image name
            $profile_picture = date("Y.m.d") . $_FILES['profile_picture']['name'];
            // define Where image will be stored
            $target = ROOT_PATH . "/assets/images/" . $profile_picture;
            // upload image to folder
            if (move_uploaded_file($_FILES['profile_picture']['tmp_name'], $target)) {
              return $profile_picture;
              exit();
            }else{
              echo "Failed to upload image";
            }
        }
      }

    このファイルの2つの重要な機能に注意を向けさせてください。それらは、getSingleRecord()とgetMultipleRecords()です。これらの関数は非常に重要です。アプリケーション全体のどこでも、データベースからレコードを選択する場合は、getSingleRecord()関数を呼び出して、SQLクエリを渡すだけだからです。複数のレコードを選択したい場合は、ご想像のとおり、適切なSQLクエリを渡してgetMultipleRecords()関数も呼び出すだけです。

    これらの2つの関数は、SQLクエリ、変数タイプ(たとえば、「s」は文字列、「si」は文字列と整数などを意味します)、最後にすべての値の配列である3番目のパラメーターの3つのパラメーターを取ります。クエリを実行するには

    たとえば、usernameが「John」で24歳のusersテーブルから選択する場合は、次のようにクエリを記述します。

    $sql = SELECT * FROM users WHERE username=John AND age=20; // this is the query
    
    $user = getSingleRecord($sql, 'si', ['John', 20]); // perform database query

    関数呼び出しでは、「s」は文字列型を表し(ユーザー名「John」は文字列であるため)、「i」は整数を意味します(20歳は整数です)。この関数を使用すると、アプリケーションの100の異なる場所でデータベースクエリを実行する場合に、これら2行だけを実行する必要がなくなるため、作業が非常に簡単になります。関数自体にはそれぞれ約8〜10行のコードがあるため、コードを繰り返す必要はありません。これらのメソッドを一度に実装しましょう。

    config.phpファイルは、データベース構成を保持しているため、データベースクエリが実行されるすべてのファイルに含まれます。したがって、これらのメソッドを定義するのに最適な場所です。 config.phpをもう一度開き、ファイルの最後に次のメソッドを追加します。

    config.php:

    // ...More code here ...
    
    function getMultipleRecords($sql, $types = null, $params = []) {
      global $conn;
      $stmt = $conn->prepare($sql);
      if (!empty($params) && !empty($params)) { // parameters must exist before you call bind_param() method
        $stmt->bind_param($types, ...$params);
      }
      $stmt->execute();
      $result = $stmt->get_result();
      $user = $result->fetch_all(MYSQLI_ASSOC);
      $stmt->close();
      return $user;
    }
    function getSingleRecord($sql, $types, $params) {
      global $conn;
      $stmt = $conn->prepare($sql);
      $stmt->bind_param($types, ...$params);
      $stmt->execute();
      $result = $stmt->get_result();
      $user = $result->fetch_assoc();
      $stmt->close();
      return $user;
    }
    function modifyRecord($sql, $types, $params) {
      global $conn;
      $stmt = $conn->prepare($sql);
      $stmt->bind_param($types, ...$params);
      $result = $stmt->execute();
      $stmt->close();
      return $result;
    }

    プリペアドステートメントを使用していますが、これはセキュリティ上の理由から重要です。

    ここで、common_functions.phpファイルに戻ります。このファイルには、後で他の多くのファイルで使用される4つの重要な機能が含まれています。

    ユーザーが登録するときに、ユーザーが正しいデータを提供していることを確認したいので、このファイルが提供するvalidateUser()関数を呼び出します。プロフィール画像が選択されている場合は、このファイルが提供するuploadProfilePicture()関数を呼び出してアップロードします。

    ユーザーをデータベースに正常に保存できたら、すぐにログインしたいので、このファイルが提供するloginById()関数を呼び出します。ユーザーがログインするときに、ユーザーが管理者であるか通常であるかを知りたいので、このファイルが提供するisAdmin()関数を呼び出します。それらが管理者であることがわかった場合(isAdmin()がtrueを返す場合)、ダッシュボードにリダイレクトします。通常のユーザーの場合は、ホームページにリダイレクトします。

    したがって、common_functions.phpファイルが非常に重要であることがわかります。管理セクションで作業するときは、これらすべての関数を使用します。これにより、作業が大幅に削減され、コードの繰り返しが回避されます。

    ユーザーがサインアップできるようにするには、usersテーブルを作成しましょう。ただし、usersテーブルはrolesテーブルに関連しているため、最初にrolesテーブルを作成します。

    役割表:

    CREATE TABLE `roles` (
     `id` int(11) NOT NULL AUTO_INCREMENT,
     `name` varchar(255) NOT NULL,
     `description` text NOT NULL,
      PRIMARY KEY (`id`)
    )

    ユーザーテーブル:

    CREATE TABLE `users`(
        `id` INT(11) PRIMARY KEY NOT NULL AUTO_INCREMENT,
        `role_id` INT(11) DEFAULT NULL,
        `username` VARCHAR(255) UNIQUE NOT NULL,
        `email` VARCHAR(255) UNIQUE NOT NULL,
        `password` VARCHAR(255) NOT NULL,
        `profile_picture` VARCHAR(255) DEFAULT NULL,
        `created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
        `updated_at` TIMESTAMP NOT NULL DEFAULT '0000-00-00 00:00:00',
        CONSTRAINT `users_ibfk_1` FOREIGN KEY(`role_id`) REFERENCES `roles`(`id`) ON DELETE SET NULL ON UPDATE NO ACTION
    )

    ユーザーテーブルは、多対1の関係にあるロールテーブルに関連しています。ロールがロールテーブルから削除された場合、以前にそのrole_idを属性として持っていたすべてのユーザーの値をNULLに設定する必要があります。これは、ユーザーが管理者ではなくなることを意味します。

    テーブルを手動で作成する場合は、この制約を追加することをお勧めします。 PHPMyAdminを使用している場合は、ユーザーテーブルの[構造]タブをクリックし、次にリレーションビューテーブルをクリックして、最後に次のようにこのフォームに入力することで、これを行うことができます。

    この時点で、システムはユーザーが登録できるようにし、登録後、自動的にログインします。ただし、ログイン後、loginById()関数に示されているように、ユーザーはホームページ(index.php)にリダイレクトされます。そのページを作成しましょう。アプリケーションのルートで、index.phpという名前のファイルを作成します。

    index.php:

    <?php include("config.php") ?>
    <?php include(INCLUDE_PATH . "/logic/common_functions.php"); ?>
    <!DOCTYPE html>
    <html>
    <head>
      <meta charset="utf-8">
      <title>UserAccounts - Home</title>
      <!-- Bootstrap CSS -->
      <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/3.3.7/css/bootstrap.min.css" />
      <!-- Custome styles -->
      <link rel="stylesheet" href="static/css/style.css">
    </head>
    <body>
        <?php include(INCLUDE_PATH . "/layouts/navbar.php") ?>
        <?php include(INCLUDE_PATH . "/layouts/messages.php") ?>
        <h1>Home page</h1>
        <?php include(INCLUDE_PATH . "/layouts/footer.php") ?>

    次に、ブラウザを開き、http://localhost/user-accounts/signup.phpにアクセスして、フォームにテスト情報を入力します(後でユーザーを使用してログインするため、テスト情報を覚えておいてください)。サインアップボタン。すべてがうまくいけば、ユーザーはデータベースに保存され、アプリケーションはホームページにリダイレクトされます。

    ホームページには、まだ作成していないmessages.phpファイルが含まれているために発生するエラーが表示されます。すぐに作成しましょう。

    include / layoutsディレクトリに、messages.phpという名前のファイルを作成します。

    messages.php:

    <?php if (isset($_SESSION['success_msg'])): ?>
      <div class="alert <?php echo 'alert-success'; ?> alert-dismissible" role="alert">
        <button type="button" class="close" data-dismiss="alert" aria-label="Close"><span aria-hidden="true">&times;</span></button>
        <?php
          echo $_SESSION['success_msg'];
          unset($_SESSION['success_msg']);
        ?>
      </div>
    <?php endif; ?>
    
    <?php if (isset($_SESSION['error_msg'])): ?>
      <div class="alert alert-danger alert-dismissible" role="alert">
        <button type="button" class="close" data-dismiss="alert" aria-label="Close"><span aria-hidden="true">&times;</span></button>
        <?php
          echo $_SESSION['error_msg'];
          unset($_SESSION['error_msg']);
        ?>
      </div>
    <?php endif; ?>

    ホームページを更新すると、エラーはなくなります。

    この部分は以上です。次のパートでは、サインアップフォーム、ユーザーログイン/ログアウトの検証を続行し、管理セクションでの作業を開始します。これは大変な作業のように聞こえますが、私を信じてください。特に、管理セクションでの作業を容易にするコードをすでに作成しているので、簡単です。

    フォローしていただきありがとうございます。あなたが一緒に来ていることを願っています。何か考えがあれば、下のコメントにドロップしてください。エラーが発生した場合、または何かがわからない場合は、コメントセクションでお知らせください。サポートさせていただきます。

    次のパートでお会いしましょう。


    1. MySQLで現在の日付と時刻を取得する方法

    2. Postgres:CASTエラーのデフォルト値を定義しますか?

    3. SQL /HQLJavaからのテーブル名と列名の解析

    4. SQL結合の概要