How to Manage PHP Session Data With Redis® On Ubuntu 20.04
Introduction
When users interact with your web application, their current state is referred to as a session. Session data allows your application to remember the identity of end-users for the entire period they're logged in. In a typical web application, an end-user submits a username and a password in a login form. Your application then finds those credentials in a database. If there is a match, you simply grant the user permission to access your web application. Otherwise, the user receives an access denied error.
Since a logged-in user can request different pages from your web application, you must find a way to persist the session data. This helps users to easily navigate your site or web application during the lifetime of a session without re-submitting their login credentials. In PHP, the best way to achieve this functionality is to issue an access token to any user who successfully logs in to your application. Then, you should save the token in a database table, and send it back to the user's browser in form of an HTTP cookie.
When the end user's browser receives the cookie data, it will send it back during any subsequent HTTP requests. From this point forward, you can validate the access token from your database to remember the user's details every time a web page is requested.
The entire process of issuing and validating the access tokens requires your application to write and read huge amounts of data from the database. This might hurt the user's experience since every HTTP request requires a roundtrip to the database to verify the access token. To overcome this challenge and offer a fast response, you should cache the session data in an in-memory database like Redis®. In this guide, you'll learn how to manage PHP session data with Redis® Server on Ubuntu 20.04.
Prerequisites
To complete this tutorial, you require the following:
1 - Install the Redis® Extension for PHP
To communicate with the Redis® server key-value store in PHP, you'll need to install the php-redis
library. First, SSH to your server and update the package information index.
$ sudo apt update
Next, issue the following command to install the php-redis
extension.
$ sudo apt install -y php-redis
Restart the Apache webserver to load the new changes.
$ sudo systemctl restart apache2
After initializing the API for interacting with the Redis® Server on PHP, you'll create a test database and a table in the next step.
2 - Create a Test Database and a Table
In this step, you'll set up a sample database and a table to store users' login credentials including their names and hashed passwords. Log in to your MySQL server as root.
$ sudo mysql -u root -p
Enter your MySQL server's password when prompted and press Enter to proceed. Next, issue the CREATE DATABASE
command to set up a new sample_cms
database.
mysql> CREATE DATABASE sample_cms;
Confirm the output below to make sure you've created the database.
Query OK, 1 row affected (0.01 sec)
Next, you'll create a non-root user for your sample_cms
database since it's not recommended to use the root credentials in a PHP script. Run the command below to create a sample_cms_user
and replace EXAMPLE_PASSWORD
with a strong password.
mysql> CREATE USER 'sample_cms_user'@'localhost' IDENTIFIED WITH mysql_native_password BY 'EXAMPLE_PASSWORD';
GRANT ALL PRIVILEGES ON sample_cms.* TO 'sample_cms_user'@'localhost';
FLUSH PRIVILEGES;
Make sure you receive the following response to confirm that you've successfully created the sample_cms_user
user.
...
Query OK, 0 rows affected (0.00 sec)
Next, run the USE
command to switch to the new sample_cms
database.
mysql> USE sample_cms;
Ensure you've selected the new database by verifying the output below.
Database changed
Next, set up a system_users
table. You'll use the user_id
column to uniquely identify each user's account. Then, use the AUTO_INCREMENT
keyword on this field to automatically generate a new user_id
for each record. To accommodate a large number of users on the table, use the BIGINT
data type on the user_id
column. Finally, use the VARCHAR
data type for the username
, first_name
, last_name
, and pwd
fields. The keyword ENGINE = InnoDB
allows you to use the InnoDB
engine which is fast and transaction-ready.
To create the system_users
table, run the following command.
mysql> CREATE TABLE system_users (
user_id BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY,
username VARCHAR(15),
first_name VARCHAR(50),
last_name VARCHAR(50),
pwd VARCHAR(255)
) ENGINE = InnoDB;
Make sure you've successfully created the table by confirming the output below.
Query OK, 0 rows affected (0.02 sec)
Exit from the MySQL command-line interface.
mysql> QUIT;
Output.
Bye
The system_users
table is now ready to receive data. In the next step, you'll create a PHP script for populating the table.
3 - Create a Register Script
To test the user-session functionalities, you'll require some sample records. In this step, you'll set up a PHP script that accepts users' data from the Linux curl
command and in turn populates the system_users
table. In a production environment, you may create a registration page where users can sign up for your application. For this guide, you just need a script to automate the users' registration process without creating any registration form.
Open a new /var/www/html/register.php
file under the root directory of your web server.
$ sudo nano /var/www/html/register.php
Next, enter the information below into the file.
<?php
try {
$db_name = 'sample_cms';
$db_user = 'sample_cms_user';
$db_password = 'EXAMPLE_PASSWORD';
$db_host = 'localhost';
$pdo = new PDO('mysql:host=' . $db_host . '; dbname=' . $db_name, $db_user, $db_password);
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
$pdo->setAttribute(PDO::ATTR_EMULATE_PREPARES, false);
$sql = 'insert into system_users
(
username,
first_name,
last_name,
pwd
)
values
(
:username,
:first_name,
:last_name,
:pwd
)
';
$data = [];
$data = [
'username' => $_POST['username'],
'first_name' => $_POST['first_name'],
'last_name' => $_POST['last_name'],
'pwd' => password_hash($_POST['pwd'], PASSWORD_BCRYPT)
];
$stmt = $pdo->prepare($sql);
$stmt->execute($data);
echo "User data saved successfully.\n" ;
} catch (PDOException $e) {
echo 'Database error. ' . $e->getMessage();
}
Save and close the file when you're done with editing. At the top of the file, you're declaring the database variables that you set up in Step 2. Next. you're using the PHP PDO library to execute a prepared statement to save data into the system_users
table. To avoid saving passwords in plain text, you're using the statement password_hash($_POST['pwd'], PASSWORD_BCRYPT)
to hash passwords using the bcrypt algorithm.
Next, execute the following curl
commands to create some users' accounts in your database. Please note, you can use stronger values for the passwords by replacing ...EXAMPLE_PASSWORD_1...
, ...EXAMPLE_PASSWORD_2...
, and ...EXAMPLE_PASSWORD_3...
with your desired passwords.
$ curl --data "username=john_doe&first_name=JOHN&last_name=DOE&pwd=EXAMPLE_PASSWORD_1" http://localhost/register.php
$ curl --data "username=mary_smith&first_name=MARY&last_name=SMITH&pwd=EXAMPLE_PASSWORD_2" http://localhost/register.php
$ curl --data "username=roe_jane&first_name=ROE&last_name=JANE&pwd=EXAMPLE_PASSWORD_3" http://localhost/register.php
After executing each command, you should get the following output to confirm that you've successfully created a user account.
...
User data saved successfully.
Next, confirm the records by logging into your MySQL server as sample_cms_user
. You don't need any sudo
privileges to execute the following command.
$ mysql -u sample_cms_user -p
Enter the password for the sample_cms_user
(For instance, EXAMPLE_PASSWORD
) and press Enter to proceed. Then, switch to the new sample_cms
database.
mysql> USE sample_cms;
Ensure you've switched to the database by confirming the output below.
Database changed
Next, run a SELECT
statement against the system_users
table to verify the records.
mysql> SELECT
user_id,
username,
first_name,
last_name,
pwd
FROM system_users;
You should now receive the following output to confirm the data is in place. As you can see, the pwd
column is hashed.
+---------+------------+------------+-----------+--------------------------------------------------------------+
| user_id | username | first_name | last_name | pwd |
+---------+------------+------------+-----------+--------------------------------------------------------------+
| 1 | john_doe | JOHN | DOE | $2y$10$8WcrxHkCUuRM4upVmYJhe.xKAXpoQkVQahoYI87RAlgSeTaxgq3Km |
| 2 | mary_smith | MARY | SMITH | $2y$10$Yk3ZngColV9WGL4c/mgxvuwaVMutq73NW1mWXMrydoukEUxpq0XA2 |
| 3 | roe_jane | ROE | JANE | $2y$10$TcSaOC6MylunFXI4s.XTW.W70i9XjJIa3VyT2JXBygW4pvSoKvj4y |
+---------+------------+------------+-----------+--------------------------------------------------------------+
3 rows in set (0.01 sec)
Exit from the MySQL server command-line interface.
mysql> QUIT;
Output.
Bye
With the sample user accounts in place, you'll now create a PHP login page in the next step.
4 - Create a User Login Form and a Processing Script
Visitors using your sample application will access it through a login page. In this step, you'll create an HTML form that accepts a username and password. This form will then send the login credentials to a PHP script that compares the values from the database you've created to authenticate users in case there is a matching record.
Open a new /var/www/html/login.php
file.
$ sudo nano /var/www/html/login.php
Then enter the following information into the /var/www/html/login.php
file.
<html>
<head>
<title>User Login</title>
</head>
<body>
<h2>User Login Page</h2>
<form action="/process.php" method="post">
<label for="username">Username:</label><br>
<input type="text" id="username" name="username" ><br><br>
<label for="pwd">Password:</label><br>
<input type="password" id="pwd" name="pwd"><br><br>
<input type="submit" value="Submit">
</form>
</body>
</html>
Save and close the file. The statement action="/process.php
instructs the form to send the data to a file named process.php
. Next, use nano
to create the new /var/www/html/process.php
file.
$ sudo nano /var/www/html/process.php
Then, enter the information below into the /var/www/html/process.php
file.
<?php
try {
$db_name = 'sample_cms';
$db_user = 'sample_cms_user';
$db_password = 'EXAMPLE_PASSWORD';
$db_host = 'localhost';
$pdo = new PDO('mysql:host=' . $db_host . '; dbname=' . $db_name, $db_user, $db_password);
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
$pdo->setAttribute(PDO::ATTR_EMULATE_PREPARES, false);
$sql = 'select
user_id,
username,
first_name,
last_name,
pwd
from system_users
where username = :username
';
$data = [];
$data = [
'username' => $_POST['username']
];
$stmt = $pdo->prepare($sql);
$stmt->execute($data);
$user_data = [];
while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) {
$user_data = $row;
}
if (password_verify($_POST['pwd'], $user_data['pwd']) == true) {
$session_token = bin2hex(openssl_random_pseudo_bytes(16));
$user_data['token'] = $session_token;
setcookie('token', $session_token, time()+3600);
setcookie('username', $user_data['username'], time()+3600);
$redis = new Redis();
$redis->connect('127.0.0.1', 6379);
$redis_key = $user_data['username'];
$redis->set($redis_key, serialize($user_data));
$redis->expire($redis_key, 3600);
header('Location: dashboard.php');
} else {
header('Location: login.php');
}
} catch (PDOException $e) {
echo 'Database error. ' . $e->getMessage();
}
Save and close the file. In the above file, you're connecting to the sample_cms
database, then you're searching for a username
based on the value received from the $_POST['username']
variable from the login.php
form. If there is a match, you're placing the user's information in an array named $user_data
. Next, you're using the PHP if (password_verify($_POST['pwd'], $user_data['pwd']) == true) {...}
statement to check if the supplied password matches the value in the system_users
table.
In case the user has entered the correct password, you're assigning a new session token to the user using the statement $session_token = bin2hex(openssl_random_pseudo_bytes(16));
. Next, you're creating two cookies in the user's browser with an expiration time of 3600
seconds(1 hour). The first cookie contains the value of the access token ($session_token
) and the second cookie stores the username
. You'll use these details to identify the user next time they make a request to your web application without requiring them to log in again.
Then, you're opening a new instance of a Redis® server using the following code block.
...
$redis = new Redis();
$redis->connect('127.0.0.1', 6379);
...
Since several users may connect to your application, you're distinguishing their session data by naming the Redis® key($redis_key
) with the username
value using the $redis->set
and $redis->expire
functions. These functions cache the users' session data in the Redis® server for faster retrieval instead of saving the data to the MySQL database.
...
$redis_key = $user_data['username'];
$redis->set($redis_key, serialize($user_data));
$redis->expire($redis_key, 3600);
...
Finally, once you've authenticated the user and passed the session handling to the Redis® server, you're directing the user to the dashboard.php
page using the header('Location: dashboard.php');
statement, otherwise, you're lopping back unauthenticated users with invalid login credentials to the login.php
page with the following statements.
...
header('Location: dashboard.php');
} else {
header('Location: login.php');
}
...
The login.php
and process.php
pages are now ready to authenticate users. In the next step, you'll create a dashboard page where logged-in users will be redirected.
5 - Create a Dashboard Page
In a web application, the dashboard is the main page that allows users to navigate through menus and links. It should only be accessed by authenticated users who've been redirected from the login page after entering valid credentials.
Since anyone might attempt to access the dashboard page directly by entering its URL on their browser, the only way to authorize access to this page is by examining the session data coming from a user's browser's cookies.
In the previous step, you've assigned authenticated user with two unique cookies and cached them in the Redis® server. In this step, you'll examine the value of these cookies and re-validate them from the Redis® server to ensure they're valid.
Any user visiting the dashboard.php
without any cookies will receive an Access denied.
error. Also, in case the cookie information has been modified and doesn't match the values in the Redis® server, your script should respond with an Invalid token.
error.
Use the nano
text editor to open a new /var/www/html/dashboard.php
file.
$ sudo nano /var/www/html/dashboard.php
Then enter the information below into the file.
<html>
<head>
<title>Dashboard</title>
</head>
<body>
<h1>Dashboard</h1>
<p>
<?php
$redis = new Redis();
$redis->connect('127.0.0.1', 6379);
if ($redis->exists($_COOKIE['username'])) {
$user_data = unserialize($redis->get($_COOKIE['username']));
if ($_COOKIE['token'] == $user_data['token']) {
echo "Welcome, " . $user_data['first_name'] . ' ' . $user_data['last_name'] . "<br>"
. "Your token is " . $user_data['token'];
} else {
echo "Invalid token.";
}
} else {
echo "Access denied.";
}
?>
</p>
</body>
</html>
Save and close the file when you're done with editing. In the above file, you're connecting to the Redis® server and then, you're using the statement ...if ($redis->exists($_COOKIE['username'])) {...}...
to check if there is a key named after the $_COOKIE['username']
value, otherwise, you're responding with an Access denied.
error. Next, in case Redis® has cached a key with that name, you're comparing its value with the browser's access token cookie ($_COOKIE['token']
). If the values are the same, you're echoing the details of the user from the Redis® server using the following statement:
echo "Welcome, " . $user_data['first_name'] . ' ' . $user_data['last_name'] . "<br>"
. "Your token is " . $user_data['token'];
Please note, you should include the session handling logic from this dashboard.php
page on all pages that require you to know the logged-in status of the users. In the next step, you'll test the functionalities of all the codes that you've written.
6 - Test the Application
On a browser, visit the page below and replace 192.0.2.1
with the correct public IP address or domain name of your webserver.
You should be prompted to enter a username and a password. Enter JOHN DOE's
credentials (For instance, Username:john_doe
, Password:EXAMPLE_PASSWORD_1
) and click Submit to login.
You should now be re-directed to the http://192.0.2.1/dashboard.php
page and receive a welcome message.
Even if you refresh the dashboard.php
page, you'll still be logged in since you set the session data to persist for a period of 1 hour. The above screenshots confirm that Redis® is able to handle your session data without hitting the MySQL database and this will greatly improve the response time of your web application particularly if you have lots of users.
Conclusion
In this guide, you've set up a sample database and a user table. You've then coded a login page that sends users' account credentials to a PHP page that persists data to a Redis® server to manage sessions. Use the logic in this guide to design a highly available project that requires scalable users' session management,
Apart from handling users' session data, the Redis® server offers more functionalities. Refer to the documents below to learn how you can further reduce database and front-end loads with Redis® server: