How to Manage PHP Session Data With Redis On Ubuntu 20.04

Updated on August 31, 2021
How to Manage PHP Session Data With Redis On Ubuntu 20.04 header image

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: