Mysql
 sql >> Datenbank >  >> RDS >> Mysql

Benutzerkontenverwaltung, Rollen, Berechtigungen, Authentifizierung PHP und MySQL - Teil 2

Dies ist der zweite Teil einer Serie über Benutzerkontenverwaltungssystem, Authentifizierung, Rollen, Berechtigungen. Den ersten Teil finden Sie hier.

Datenbankkonfiguration

Erstellen Sie eine MySQL-Datenbank namens Nutzerkonten. Erstellen Sie dann im Stammordner Ihres Projekts (Benutzerkontenordner) eine Datei und nennen Sie sie config.php. Diese Datei wird verwendet, um Datenbankvariablen zu konfigurieren und dann unsere Anwendung mit der gerade erstellten MySQL-Datenbank zu verbinden.

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
?>

Wir haben die Sitzung auch gestartet, weil wir sie später verwenden müssen, um angemeldete Benutzerinformationen wie den Benutzernamen zu speichern. Am Ende der Datei definieren wir Konstanten, die uns helfen, Dateiincludes besser zu handhaben.

Unsere Anwendung ist jetzt mit der MySQL-Datenbank verbunden. Lassen Sie uns ein Formular erstellen, das es einem Benutzer ermöglicht, seine Daten einzugeben und sein Konto zu registrieren. Erstellen Sie eine signup.php-Datei im Stammverzeichnis des Projekts:

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>

In der allerersten Zeile dieser Datei fügen wir die zuvor erstellte config.php-Datei ein, da wir die INCLUDE_PATH-Konstante verwenden müssen, die config.php in unserer signup.php-Datei bereitstellt. Mit dieser INCLUDE_PATH-Konstante schließen wir auch navbar.php, footer.php und userSignup.php ein, die die Logik zum Registrieren eines Benutzers in einer Datenbank enthält. Wir werden diese Dateien sehr bald erstellen.

Am Ende der Datei befindet sich ein rundes Feld, auf das der Benutzer klicken kann, um ein Profilbild hochzuladen. Wenn der Benutzer auf diesen Bereich klickt und ein Profilbild von seinem Computer auswählt, wird zunächst eine Vorschau dieses Bildes angezeigt.

Diese Bildvorschau wird mit jquery erreicht. Wenn der Benutzer auf die Schaltfläche „Bild hochladen“ klickt, lösen wir programmgesteuert das Dateieingabefeld mit JQuery aus, wodurch die Computerdateien des Benutzers angezeigt werden, damit er seinen Computer durchsuchen und sein Profilbild auswählen kann. Wenn sie das Bild auswählen, verwenden wir weiterhin Jquery, um das Bild vorübergehend anzuzeigen. Der Code dafür befindet sich in unserer Datei display_profile_image.php, die wir bald erstellen werden.

Noch nicht im Browser anzeigen. Lassen Sie uns zuerst dieser Datei geben, was wir ihr schulden. Lassen Sie uns zunächst im Ordner „assets/css“ die Datei „style.css“ erstellen, die wir im Abschnitt „head“ verlinkt haben.

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; }

In der ersten Zeile dieser Datei importieren wir eine Google-Schriftart namens „Lora“, damit unsere App eine schönere Schriftart erhält.

Die nächste Datei, die wir in dieser signup.php benötigen, sind die Dateien navbar.php und footer.php. Erstellen Sie diese beiden Dateien im Ordner includes/layouts :

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>

Die allerletzte Zeile der Datei „signup.php“ ist mit einem JQuery-Skript namens „display_profile_image.js“ verknüpft, das genau das tut, was sein Name sagt. Erstellen Sie diese Datei im Ordner assets/js und fügen Sie diesen Code darin ein:

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);
      }
    });
  });
});

Und schließlich die Datei userSignup.php. An diese Datei werden die Anmeldeformulardaten zur Verarbeitung und Speicherung in der Datenbank gesendet. Erstellen Sie userSignup.php im Ordner includes/logic und fügen Sie diesen Code darin ein:

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";
		}
	 }
}

Ich habe diese Datei zuletzt gespeichert, weil sie mehr Arbeit hatte. Als erstes fügen wir noch eine weitere Datei namens common_functions.php oben in diese Datei ein. Wir schließen diese Datei ein, weil wir zwei daraus abgeleitete Methoden verwenden:validateUser() und loginById(), die wir in Kürze erstellen werden.

Erstellen Sie diese common_functions.php-Datei in Ihrem include/logic Ordner:

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";
        }
    }
  }

Lassen Sie mich Ihre Aufmerksamkeit auf 2 wichtige Funktionen in dieser Datei lenken. Diese sind: getSingleRecord() und getMultipleRecords(). Diese Funktionen sind sehr wichtig, da wir überall in unserer gesamten Anwendung, wenn wir einen Datensatz aus der Datenbank auswählen möchten, einfach die Funktion getSingleRecord() aufrufen und die SQL-Abfrage an sie übergeben. Wenn wir mehrere Datensätze auswählen wollen, werden wir, Sie haben es erraten, einfach auch die Funktion getMultipleRecords() aufrufen und die entsprechende SQL-Abfrage übergeben.

Diese beiden Funktionen benötigen drei Parameter, nämlich die SQL-Abfrage, die Variablentypen (z. B. „s“ bedeutet Zeichenfolge, „si“ bedeutet Zeichenfolge und Ganzzahl usw.) und schließlich einen dritten Parameter, der ein Array aller Werte ist die Abfrage muss ausgeführt werden.

Wenn ich zum Beispiel aus der Benutzertabelle auswählen möchte, wo der Benutzername „John“ und das Alter 24 ist, schreibe ich meine Abfrage einfach so:

$sql = SELECT * FROM users WHERE username=John AND age=20; // this is the query

$user = getSingleRecord($sql, 'si', ['John', 20]); // perform database query

Im Funktionsaufruf steht 's' für den String-Typ (da der Benutzername 'John' ein String ist) und 'i' bedeutet Ganzzahl (Alter 20 ist eine Ganzzahl). Diese Funktion erleichtert uns die Arbeit ungemein, denn wenn wir in unserer Anwendung an hundert verschiedenen Stellen eine Datenbankabfrage durchführen wollen, müssen wir nicht nur diese beiden Zeilen. Die Funktionen selbst haben jeweils etwa 8 - 10 Codezeilen, sodass wir uns das Wiederholen von Code ersparen. Lassen Sie uns diese Methoden sofort implementieren.

Die Datei config.php wird in jeder Datei enthalten sein, in der Datenbankabfragen durchgeführt werden, da sie die Datenbankkonfiguration enthält. Es ist also der perfekte Ort, um diese Methoden zu definieren. Öffnen Sie config.php noch einmal und fügen Sie einfach diese Methoden am Ende der Datei hinzu:

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;
}

Wir verwenden vorbereitete Anweisungen und dies ist aus Sicherheitsgründen wichtig.

Nun zurück zu unserer Datei common_functions.php. Diese Datei enthält 4 wichtige Funktionen, die später von vielen anderen Dateien verwendet werden.

Wenn sich der Benutzer registriert, möchten wir sicherstellen, dass er die richtigen Daten bereitstellt, also rufen wir die validateUser() Funktion auf, die diese Datei bereitstellt. Wenn ein Profilbild ausgewählt wurde, laden wir es hoch, indem wir die Funktion uploadProfilePicture() aufrufen, die diese Datei bereitstellt.

Wenn wir den Benutzer erfolgreich in der Datenbank speichern, möchten wir ihn sofort anmelden, also rufen wir die Funktion loginById() auf, die diese Datei bereitstellt. Wenn sich ein Benutzer anmeldet, möchten wir wissen, ob er Administrator oder normal ist, also rufen wir die Funktion isAdmin() auf, die diese Datei bereitstellt. Wenn wir feststellen, dass sie Administratoren sind (wenn isAdmin() true zurückgibt), leiten wir sie zum Dashboard um. Bei normalen Benutzern leiten wir auf die Startseite weiter.

Sie können also sehen, dass unsere Datei common_functions.php sehr wichtig ist. Wir werden alle diese Funktionen verwenden, wenn wir an unserem Admin-Bereich arbeiten, was unsere Arbeit erheblich reduziert und die Wiederholung von Code vermeidet.

Um dem Benutzer die Anmeldung zu ermöglichen, erstellen wir die Benutzertabelle. Da die Benutzertabelle jedoch mit der Rollentabelle verknüpft ist, erstellen wir zuerst die Rollentabelle.

Rollentabelle:

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

Benutzertabelle:

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
)

Die Benutzertabelle ist mit der Rollentabelle in einer Viele-zu-Eins-Beziehung verknüpft. Wenn eine Rolle aus der Rollentabelle gelöscht wird, möchten wir, dass der Wert aller Nutzer, die zuvor diese role_id als ihr Attribut hatten, auf NULL gesetzt wird. Das bedeutet, dass der Benutzer kein Administrator mehr ist.

Wenn Sie die Tabelle manuell erstellen, sollten Sie diese Einschränkung hinzufügen. Wenn Sie PHPMyAdmin verwenden, können Sie dies tun, indem Sie auf die Registerkarte „Struktur“ in der Benutzertabelle und dann auf die Beziehungsansichtstabelle klicken und schließlich dieses Formular wie folgt ausfüllen:

An diesem Punkt erlaubt unser System einem Benutzer, sich zu registrieren, und nach der Registrierung werden sie automatisch angemeldet. Aber nach der Anmeldung werden sie, wie in der Funktion loginById() gezeigt, auf die Startseite (index.php) umgeleitet. Lassen Sie uns diese Seite erstellen. Erstellen Sie im Stammverzeichnis der Anwendung eine Datei namens 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") ?>

Öffnen Sie nun Ihren Browser, gehen Sie zu http://localhost/user-accounts/signup.php, füllen Sie das Formular mit einigen Testinformationen aus (und merken Sie sich diese gut, da wir den Benutzer später zum Anmelden verwenden werden), und klicken Sie dann auf die Anmeldeschaltfläche. Wenn alles gut gegangen ist, wird der Benutzer in der Datenbank gespeichert und unsere Anwendung wird auf die Startseite umgeleitet.

Auf der Homepage sehen Sie einen Fehler, der entsteht, weil wir eine noch nicht erstellte Datei messages.php einbinden. Lassen Sie es uns sofort erstellen.

Erstellen Sie im Verzeichnis „includes/layouts“ eine Datei namens 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; ?>

Jetzt Homepage neu laden und der Fehler ist weg.

Und das war es für diesen Teil. Im nächsten Teil werden wir mit der Validierung des Anmeldeformulars, der Benutzeranmeldung/-abmeldung und der Arbeit am Admin-Bereich fortfahren. Das hört sich nach zu viel Arbeit an, aber vertrauen Sie mir, es ist einfach, zumal wir bereits Code geschrieben haben, der unsere Arbeit im Admin-Bereich erleichtert.

Danke fürs Folgen. Hoffe du kommst mit. Wenn Sie irgendwelche Gedanken haben, schreiben Sie sie in die Kommentare unten. Wenn Sie auf Fehler gestoßen sind oder etwas nicht verstanden haben, lassen Sie es mich im Kommentarbereich wissen, damit ich versuchen kann, Ihnen zu helfen.

Wir sehen uns im nächsten Teil.