sql >> Database >  >> RDS >> Mysql

Gebruikersaccountbeheer, rollen, machtigingen, authenticatie PHP en MySQL - Deel 2

Dit is het tweede deel van een serie over het beheersysteem van gebruikersaccounts, authenticatie, rollen, machtigingen. Het eerste deel vind je hier.

Databaseconfiguratie

Maak een MySQL-database met de naam gebruikersaccounts. Maak vervolgens in de hoofdmap van uw project (map met gebruikersaccounts) een bestand en noem het config.php. Dit bestand wordt gebruikt om databasevariabelen te configureren en vervolgens onze applicatie te verbinden met de MySQL-database die we zojuist hebben gemaakt.

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

We hebben de sessie ook gestart omdat we deze later zullen moeten gebruiken om ingelogde gebruikersinformatie zoals gebruikersnaam op te slaan. Aan het einde van het bestand definiëren we constanten die ons zullen helpen om bestandsopnames beter te verwerken.

Onze applicatie is nu verbonden met de MySQL-database. Laten we een formulier maken waarmee een gebruiker zijn gegevens kan invoeren en zijn account kan registreren. Maak een signup.php-bestand in de hoofdmap van het project:

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>

Op de allereerste regel in dit bestand nemen we het bestand config.php op dat we eerder hebben gemaakt, omdat we de constante INCLUDE_PATH moeten gebruiken die config.php biedt in ons bestand signup.php. Met behulp van deze INCLUDE_PATH-constante nemen we ook navbar.php, footer.php en userSignup.php op, die de logica bevatten voor het registreren van een gebruiker in een database. We zullen deze bestanden zeer binnenkort maken.

Aan het einde van het bestand is er een rond veld waar de gebruiker kan klikken om een ​​profielafbeelding te uploaden. Wanneer de gebruiker op dit gebied klikt en een profielafbeelding op zijn computer selecteert, wordt eerst een voorbeeld van deze afbeelding weergegeven.

Dit afbeeldingsvoorbeeld wordt bereikt met jQuery. Wanneer de gebruiker op de upload-afbeeldingsknop klikt, zullen we het bestandsinvoerveld programmatisch activeren met behulp van JQuery en dit brengt de computerbestanden van de gebruiker naar voren zodat ze op hun computer kunnen bladeren en hun profielafbeelding kunnen kiezen. Wanneer ze de afbeelding selecteren, gebruiken we JQuery still om de afbeelding tijdelijk weer te geven. De code die dit doet is te vinden in ons display_profile_image.php-bestand dat we binnenkort zullen maken.

Bekijk nog niet in de browser. Laten we dit bestand eerst geven wat we het verschuldigd zijn. Laten we voor nu, in de map assets/css, het style.css-bestand maken dat we in de head-sectie hebben gelinkt.

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

Op de eerste regel van dit bestand importeren we een Google-lettertype met de naam 'Lora' om onze app een mooier lettertype te geven.

Het volgende bestand dat we nodig hebben in deze signup.php zijn de navbar.php en footer.php bestanden. Maak deze twee bestanden in de map include/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>

De allerlaatste regel van het signup.php-bestand linkt naar een JQuery-script met de naam display_profile_image.js en het doet precies wat de naam zegt. Maak dit bestand in de map Assets/js en plak deze code erin:

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

En tot slot het bestand userSignup.php. In dit bestand worden de gegevens van het aanmeldingsformulier ingediend voor verwerking en opslag in de database. Maak userSignup.php in de map includes/logic en plak deze code erin:

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

Ik heb dit bestand voor het laatst bewaard omdat er meer werk aan was. Het eerste is dat we nog een ander bestand met de naam common_functions.php bovenaan dit bestand opnemen. We nemen dit bestand op omdat we twee methoden gebruiken die eruit voortkomen, namelijk:validUser() en loginById() die we binnenkort zullen maken.

Maak dit common_functions.php-bestand in uw map include/logic:

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

Ik wil uw aandacht vestigen op 2 belangrijke functies in dit bestand. Dit zijn:getSingleRecord() en getMultipleRecords(). Deze functies zijn erg belangrijk omdat we overal in onze hele applicatie, wanneer we een record uit de database willen selecteren, de functie getSingleRecord() aanroepen en de SQL-query eraan doorgeven. Als we meerdere records willen selecteren, je raadt het al, we zullen ook gewoon de functie getMultipleRecords() aanroepen door de juiste SQL-query door te geven.

Deze twee functies hebben 3 parameters, namelijk de SQL-query, de variabele typen (bijvoorbeeld 's' betekent string, 'si' betekent string en integer, enzovoort) en tot slot een derde parameter die een array is van alle waarden die de zoekopdracht moet worden uitgevoerd.

Als ik bijvoorbeeld uit de gebruikerstabel wil selecteren waar gebruikersnaam 'John' en leeftijd 24 is, zal ik mijn vraag als volgt schrijven:

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

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

In de functieaanroep staat 's' voor het type tekenreeks (aangezien gebruikersnaam 'John' een tekenreeks is) en 'i' betekent een geheel getal (leeftijd 20 is een geheel getal). Deze functie maakt ons werk enorm gemakkelijk, want als we een databasequery willen uitvoeren op honderd verschillende plaatsen in onze applicatie, hoeven we niet alleen deze twee regels te gebruiken. De functies zelf hebben elk ongeveer 8 - 10 regels code, dus we worden gespaard van herhalende code. Laten we deze methoden meteen implementeren.

config.php-bestand zal worden opgenomen in elk bestand waarin databasequery's worden uitgevoerd, omdat het databaseconfiguratie bevat. Het is dus de perfecte plek om deze methoden te definiëren. Open config.php nogmaals en voeg deze methoden toe aan het einde van het bestand:

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

We gebruiken voorbereide verklaringen en dit is belangrijk om veiligheidsredenen.

Nu weer terug naar ons common_functions.php-bestand. Dit bestand bevat 4 belangrijke functies die later door veel andere bestanden zullen worden gebruikt.

Wanneer de gebruiker zich registreert, willen we er zeker van zijn dat ze de juiste gegevens hebben verstrekt, dus we noemen de functie validateUser() die dit bestand biedt. Als een profielafbeelding is geselecteerd, uploaden we deze door de functie uploadProfilePicture() aan te roepen, die dit bestand biedt.

Als we de gebruiker met succes in de database hebben opgeslagen, willen we hem onmiddellijk aanmelden, dus we noemen de functie loginById() die dit bestand biedt. Wanneer een gebruiker inlogt, willen we weten of deze beheerder of normaal is, dus we noemen de functie isAdmin() die dit bestand biedt. Als we ontdekken dat ze admin zijn (als isAdmin() true retourneert), leiden we ze door naar het dashboard. Als normale gebruikers, verwijzen we door naar de startpagina.

U kunt dus zien dat ons common_functions.php-bestand erg belangrijk is. We zullen al deze functies gebruiken wanneer we aan onze admin-sectie werken, wat ons werk aanzienlijk vermindert en herhaling van code voorkomt.

Laten we de gebruikerstabel maken om de gebruiker in staat te stellen zich aan te melden. Maar aangezien de gebruikerstabel gerelateerd is aan de rollentabel, zullen we eerst de rollentabel maken.

rollentabel:

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

gebruikerstabel:

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
)

De tabel met gebruikers is gerelateerd aan de tabel met rollen in een Veel-op-een-relatie. Wanneer een rol wordt verwijderd uit de rollentabel, willen we dat alle gebruikers die voorheen die rol-id als hun kenmerk hadden, de waarde moeten instellen op NULL. Dit betekent dat de gebruiker niet langer beheerder is.

Als u de tabel handmatig maakt, doet u er goed aan deze beperking toe te voegen. Als u PHPMyAdmin gebruikt, kunt u dat doen door op het tabblad Structuur in de tabel met gebruikers te klikken, vervolgens op de tabel met de relatieweergave en vervolgens dit formulier als volgt in te vullen:

Op dit punt stelt ons systeem een ​​gebruiker in staat zich te registreren en na registratie wordt hij automatisch ingelogd. Maar na het inloggen, zoals weergegeven in de functie loginById() worden ze doorgestuurd naar de startpagina (index.php). Laten we die pagina maken. Maak in de hoofdmap van de applicatie een bestand met de naam 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") ?>

Open nu uw browser, ga naar http://localhost/user-accounts/signup.php, vul het formulier in met wat testinformatie (en onthoud ze goed, want we zullen de gebruiker later gebruiken om in te loggen), en klik vervolgens op de aanmeldknop. Als alles goed is gegaan, wordt de gebruiker opgeslagen in de database en wordt onze applicatie doorgestuurd naar de startpagina.

Op de startpagina ziet u een fout die optreedt omdat we het bestand messages.php opnemen dat we nog niet hebben gemaakt. Laten we het meteen maken.

Maak in de map include/layouts een bestand aan met de naam 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; ?>

Ververs nu de startpagina en de fout is verdwenen.

En dat was het voor dit onderdeel. In het volgende deel gaan we verder met het valideren van het aanmeldingsformulier, het inloggen/uitloggen van de gebruiker en beginnen we aan het beheerdersgedeelte. Dit klinkt als te veel werk, maar geloof me, het is eenvoudig, vooral omdat we al wat code hebben geschreven die ons werk aan de Admin-sectie vergemakkelijkt.

Bedankt voor het volgen. Hoop dat je meegaat. Als je gedachten hebt, laat ze dan achter in de reacties hieronder. Als je fouten bent tegengekomen of iets niet hebt begrepen, laat het me dan weten in het opmerkingengedeelte, zodat ik kan proberen je te helpen.

Tot ziens in het volgende deel.


  1. Wat zijn de use-cases voor het selecteren van CHAR boven VARCHAR in SQL?

  2. Optimaliseer PostgreSQL voor snel testen

  3. MIN/MAX vs BESTELLEN PER en LIMIT

  4. Een tekenreeks converteren naar een datum/tijd in SQL Server met PARSE()