Questa è la seconda parte di una serie su sistema di gestione degli account utente, autenticazione, ruoli, autorizzazioni. Puoi trovare la prima parte qui.
Configurazione database
Crea un database MySQL chiamato account-utente. Quindi nella cartella principale del tuo progetto (cartella degli account utente), crea un file e chiamalo config.php. Questo file verrà utilizzato per configurare le variabili del database e quindi connettere la nostra applicazione al database MySQL che abbiamo appena creato.
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
?>
Abbiamo anche avviato la sessione perché dovremo usarla in seguito per memorizzare le informazioni dell'utente registrato come il nome utente. Alla fine del file, stiamo definendo le costanti che ci aiuteranno a gestire meglio le inclusioni di file.
La nostra applicazione è ora connessa al database MySQL. Creiamo un modulo che consenta a un utente di inserire i propri dati e registrare il proprio account. Crea un file signup.php nella cartella principale del progetto:
iscrizione.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>
Nella prima riga di questo file, includiamo il file config.php che abbiamo creato in precedenza perché dovremo utilizzare la costante INCLUDE_PATH che config.php fornisce all'interno del nostro file signup.php. Usando questa costante INCLUDE_PATH, includiamo anche navbar.php, footer.php e userSignup.php che contiene la logica per la registrazione di un utente in un database. Creeremo questi file molto presto.
Verso la fine del file, c'è un campo rotondo in cui l'utente può fare clic per caricare un'immagine del profilo. Quando l'utente fa clic su quest'area e seleziona un'immagine del profilo dal proprio computer, viene prima visualizzata un'anteprima di questa immagine.
Questa anteprima dell'immagine è ottenuta con jquery. Quando l'utente fa clic sul pulsante di caricamento dell'immagine, attiveremo in modo programmatico il campo di input del file utilizzando JQuery e questo farà apparire i file del computer dell'utente affinché possano esplorare il proprio computer e scegliere l'immagine del profilo. Quando selezionano l'immagine, utilizziamo ancora Jquery per visualizzare temporaneamente l'immagine. Il codice che fa questo si trova nel nostro file display_profile_image.php che creeremo presto.
Non visualizzare ancora sul browser. Diamo prima a questo file ciò che gli dobbiamo. Per ora, all'interno della cartella asset/css, creiamo il file style.css che abbiamo collegato nella sezione head.
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; }
Nella prima riga di questo file, stiamo importando un font Google chiamato "Lora" per rendere la nostra app più bella.
Il prossimo file di cui abbiamo bisogno in questo signup.php sono i file navbar.php e footer.php. Crea questi due file all'interno della cartella include/layout:
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>
L'ultima riga del file signup.php si collega a uno script JQuery denominato display_profile_image.js e fa esattamente quello che dice il suo nome. Crea questo file all'interno della cartella assets/js e incolla questo codice al suo interno:
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);
}
});
});
});
E infine, il file userSignup.php. Questo file è il luogo in cui i dati del modulo di registrazione vengono inviati per l'elaborazione e il salvataggio nel database. Crea userSignup.php all'interno della cartella include/logic e incolla al suo interno questo codice:
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";
}
}
}
Ho salvato questo file per ultimo perché aveva più lavoro. La prima cosa è che stiamo includendo un altro file chiamato common_functions.php nella parte superiore di questo file. Stiamo includendo questo file perché stiamo usando due metodi che ne derivano, vale a dire:validateUser() e loginById() che creeremo a breve.
Crea questo file common_functions.php nella tua cartella 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";
}
}
}
Consentitemi di attirare la vostra attenzione su 2 funzioni importanti in questo file. Sono: getSingleRecord() e getMultipleRecords(). Queste funzioni sono molto importanti perché ovunque nella nostra intera applicazione, quando vogliamo selezionare un record dal database, chiameremo semplicemente la funzione getSingleRecord() e le passeremo la query SQL. Se vogliamo selezionare più record, hai indovinato, chiameremo semplicemente anche la funzione getMultipleRecords() passando la query SQL appropriata.
Queste due funzioni accettano 3 parametri ovvero la query SQL, i tipi di variabile (ad esempio, 's' significa stringa, 'si' significa stringa e intero e così via) e infine un terzo parametro che è un array di tutti i valori che la query deve essere eseguita.
Ad esempio, se voglio selezionare dalla tabella degli utenti in cui il nome utente è "John" e l'età di 24 anni, scriverò semplicemente la mia query in questo modo:
$sql = SELECT * FROM users WHERE username=John AND age=20; // this is the query
$user = getSingleRecord($sql, 'si', ['John', 20]); // perform database query
Nella chiamata di funzione, 's' rappresenta il tipo di stringa (poiché il nome utente 'John' è una stringa) e 'i' significa intero (età 20 è un numero intero). Questa funzione rende il nostro lavoro immensamente facile perché se vogliamo eseguire una query di database in centinaia di punti diversi nella nostra applicazione, non dovremo solo queste due righe. Le funzioni stesse hanno ciascuna circa 8 - 10 righe di codice, quindi siamo risparmiati dalla ripetizione del codice. Implementiamo subito questi metodi.
Il file config.php verrà incluso in ogni file in cui vengono eseguite le query del database poiché contiene la configurazione del database. Quindi è il posto perfetto per definire questi metodi. Apri config.php ancora una volta e aggiungi questi metodi alla fine del file:
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;
}
Utilizziamo dichiarazioni preparate e questo è importante per motivi di sicurezza.
Ora torniamo di nuovo al nostro file common_functions.php. Questo file contiene 4 importanti funzioni che verranno utilizzate in seguito da molti altri file.
Quando l'utente si registra, vogliamo assicurarci che abbia fornito i dati corretti, quindi chiamiamo la funzione validateUser() , fornita da questo file. Se è stata selezionata un'immagine del profilo, la carichiamo chiamando la funzione uploadProfilePicture() , fornita da questo file.
Se salviamo correttamente l'utente nel database, vogliamo accedervi immediatamente, quindi chiamiamo la funzione loginById(), fornita da questo file. Quando un utente effettua l'accesso, vogliamo sapere se è amministratore o normale, quindi chiamiamo la funzione isAdmin() , fornita da questo file. Se scopriamo che sono admin (se isAdmin() restituisce true), li reindirizziamo alla dashboard. Se utenti normali, reindirizziamo alla home page.
Quindi puoi vedere che il nostro file common_functions.php è molto importante. Utilizzeremo tutte queste funzioni quando lavoreremo sulla nostra sezione di amministrazione che riduce notevolmente il nostro lavoro ed evita la ripetizione del codice.
Per consentire all'utente di registrarsi, creiamo la tabella degli utenti. Ma poiché la tabella degli utenti è correlata alla tabella dei ruoli, creeremo prima la tabella dei ruoli.
tabella dei ruoli:
CREATE TABLE `roles` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(255) NOT NULL,
`description` text NOT NULL,
PRIMARY KEY (`id`)
)
tabella utenti:
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
)
La tabella utenti è correlata alla tabella ruoli in una relazione Molti-a-Uno. Quando un ruolo viene eliminato dalla tabella dei ruoli, desideriamo che tutti gli utenti che in precedenza avevano quel role_id come attributo abbiano il valore impostato su NULL. Ciò significa che l'utente non sarà più amministratore.
Se stai creando la tabella manualmente, fai bene ad aggiungere questo vincolo. Se stai utilizzando PHPMyAdmin, puoi farlo facendo clic sulla scheda della struttura nella tabella degli utenti, quindi sulla tabella di visualizzazione delle relazioni e infine compilando questo modulo in questo modo:
A questo punto, il nostro sistema consente a un utente di registrarsi e quindi, dopo la registrazione, viene automaticamente loggato. Ma dopo il login, come mostrato nella funzione loginById() , viene reindirizzato alla home page (index.php). Creiamo quella pagina. Nella radice dell'applicazione, crea un file denominato 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") ?>
Ora apri il browser, vai su http://localhost/user-accounts/signup.php, compila il modulo con alcune informazioni di prova (e fai bene a ricordarle poiché utilizzeremo l'utente in seguito per accedere), quindi fai clic su il pulsante di registrazione. Se tutto è andato bene, l'utente verrà salvato nel database e la nostra applicazione reindirizzerà alla Home page.
Sulla homepage, vedrai un errore che si verifica perché stiamo includendo il file messages.php che non abbiamo ancora creato. Creiamolo subito.
Nella directory include/layouts, crea un file chiamato messages.php:
messaggi.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">×</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">×</span></button>
<?php
echo $_SESSION['error_msg'];
unset($_SESSION['error_msg']);
?>
</div>
<?php endif; ?>
Ora aggiorna la home page e l'errore è scomparso.
E questo è tutto per questa parte. Nella parte successiva continueremo con la convalida del modulo di registrazione, il login/logout dell'utente e inizieremo a lavorare sulla sezione di amministrazione. Sembra troppo lavoro, ma fidati di me, è semplice soprattutto perché abbiamo già scritto del codice che facilita il nostro lavoro sulla sezione Admin.
Grazie per aver seguito. Spero che tu stia venendo avanti. Se hai qualche idea, lasciala nei commenti qui sotto. Se hai riscontrato errori o non hai capito qualcosa, faccelo sapere nella sezione commenti così posso provare ad aiutarti.
Alla prossima parte.