Table of Contents
*The author selected Apache Software Foundation to receive a donation as part of the Write for DOnations program.*
Introduction
Authentication is the process of verifying users' identity during login requests. In an authentication process, users submit their credentials as usernames and passwords. Then, the application matches those login credentials with stored database entries. The application grants users access to the system if there is a match.
Storing login credentials in a relational database like MySQL or PostgreSQL without a caching mechanism is still a common and practical approach, but it comes with the following limitations:
- Overloading the database. The application must make a roundtrip to the database server to verify the users' credentials from a database table every time a user submits a login request. Because the database might still serve other read/write requests, the entire process overloads the database and makes it slow.
- Traditional disk-based databases have scalability issues. When your application receives thousands of requests per second, disk-based databases do not perform optimally.
To overcome the above challenges, you can use Redis to cache the users' login credentials so that your application doesn't have to contact the backend database during each login request. Redis is one of the most popular ultrafast data stores that utilizes your computer's RAM to store data in key-value pairs. In this guide, you'll use the Redis database to speed up session handling in your Python/MySQL application on the Ubuntu 22.04 server.
Prerequisites
Before you begin this tutorial, you will need to setup following:
- Switch to the new
sudouser account and install:
Step 1 — Installing Python Database Drivers for Redis and MySQL
This application permanently stores users' credentials, such as names and passwords, in a MySQL database server. When a user logs in to the application, a Python script queries the MySQL database and matches the details with stored values. Then, the Python script caches the user's login credentials in a Redis database to serve other future requests. To complete that logic, your Python scripts require database drivers (Python modules) to communicate with the MySQL and Redis servers. Follow the steps below to install the drivers:
- Update your package information index and run the following command to install
python3-pip, a Python package manager allowing you to install additional modules not part of the Python standard library.
sudo apt install python3-pip
- Install the MySQL driver for Python:
pip install mysql-connector-python
- Install the Redis driver for Python:
pip install redis
After installing the necessary drivers for communicating with MySQL and Redis, proceed to the next step and initialize a MySQL database.
Step 2 — Setting Up a Sample MySQL Database
For this guide, you require one MySQL table. In a production environment, you can have dozens of tables that serve other requests. Set up a database and create the table by executing the following commands:
- Log in to the MySQL database server as a
rootuser:
sudo mysql -u root -p
- Enter your MySQL server's
rootpassword when prompted and pressENTERto proceed. Then, run the following command to create a samplecompanydatabase and acompany_useraccount. Replaceexample-mysql-passwordwith a strong password:
CREATE DATABASE company;
CREATE USER 'company_user'@'localhost' IDENTIFIED WITH mysql_native_password BY 'example-mysql-password';
GRANT ALL PRIVILEGES ON company.* TO 'company_user'@'localhost';
FLUSH PRIVILEGES;
- Ensure you receive the following output to confirm that the previous commands have run successfully:
[secondary_label Output]
Query OK, 1 row affected (0.01 sec)
- Switch to the new
companydatabase:
USE company;
- Confirm you're connected to the new database by verifying the following output:
[secondary_label Output]
Database changed
- Create a
system_userstable. Theuser_idcolumn serves as aPRIMARY KEYto uniquely identify each user. Theusernameandpasswordcolumns are the login credentials that users must submit to log in to the application. Thefirst_nameandlast_namecolumns store the users' names:
custom_prefix(mysql>)
CREATE TABLE system_users (
user_id BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY,
username VARCHAR(50),
first_name VARCHAR(50),
last_name VARCHAR(50),
password VARCHAR(50)
) ENGINE = InnoDB;
- Ensure you've created the new table by verifying the following output:
[secondary_label Output]
Query OK, 0 rows affected (0.03 sec)
- Populate the
system_userstable with sample data. Use the MySQL inbuiltMD5(...)function to hash the password for security purposes:
INSERT INTO system_users (username, first_name, last_name, password) VALUES ('john_doe', 'JOHN', 'DOE', MD5('password_1'));
INSERT INTO system_users (username, first_name, last_name, password) VALUES ('mary_henry', 'MARY', 'HENRY', MD5('password_2'));
INSERT INTO system_users (username, first_name, last_name, password) VALUES ('peter_jade', 'PETER', 'JADE', MD5('password_3'));
- Verify the output below:
[secondary_label Output]
Query OK, 1 row affected (0.00 sec)
- Query the
system_userstable to ensure the data is in place:
SELECT
user_id,
first_name,
last_name,
password
FROM system_users;
- Verify the following output:
[secondary_label Output]
+---------+------------+-----------+----------------------------------+
| user_id | first_name | last_name | password |
+---------+------------+-----------+----------------------------------+
| 1 | JOHN | DOE | 57210b12af5e06ad2e6e54a93b1465aa |
| 2 | MARY | HENRY | 259640f97ac2b4379dd540ff4016654c |
| 3 | PETER | JADE | 48ef85c894a06a4562268de8e4d934e1 |
+---------+------------+-----------+----------------------------------+
3 rows in set (0.00 sec)
- Log out from the MySQL database:
QUIT;
You've now set up the right MySQL database for your application. In the next step, you'll build a Python module communicating with your sample database.
Step 3 — Creating a Central MySQL Gateway Module for Python
When coding any Python project, you should create a separate module for each task to promote code reusability. In this step, you'll set up a central module that allows you to connect and query the MySQL database from a Python script. Follow the steps below:
- Create a
projectdirectory. This directory separates your Python source code files from the rest of the system files:
mkdir project
- Switch to the new
projectdirectory:
cd project
- Use
nanotext editor to open a newmysql_db.pyfile. This file hosts the Python module that talks to the MySQL database:
nano mysql_db.py
- Enter the following information into the
mysql_db.pyfile. Replaceexample-mysql-passwordwith the correct MySQL password for thecompany_useraccount:
[label ~/project/mysql_db.py]
import mysql.connector
class MysqlDb:
def db_con(self):
mysql_con = mysql.connector.connect(
host = "localhost",
user = "company_user",
password = "<^>example-mysql-password<^>",
database = "company",
port = "3306"
)
return mysql_con
def query(self, username, password):
db = self.db_con()
db_cursor = db.cursor()
db_query = "select username, password from system_users where username = %s and password = md5(%s)"
db_cursor.execute(db_query, (username, password))
result = db_cursor.fetchone()
row_count = db_cursor.rowcount
if row_count < 1:
return False
else:
return result[1]
- Save and close the
mysql_db.pyfile.
The mysql_db.py module file has one class (MysqlDb:) with two methods:
db_con(self):, connects to the samplecompanydatabase that you created earlier and returns a reusable MySQL connection using thereturn mysql_constatement.query(self, username, password):, a method that accepts ausernameandpasswordand queries thesystem_userstable to find if there is a match. The conditionalif row_count < 1: ... else: return result[1]statement returns the booleanFalsevalue if a user doesn't exist in the table or the user's password (result[1]) if the application finds a match.
With the MySQL module ready, follow the next step to set up a similar Redis module that communicates to the Redis key-value store.
Step 4 — Creating a Central Redis Module for Python
In this step, you'll code a module that connects to the Redis server. Execute the following steps:
- Open a new
redis_db.pyfile:
nano redis_db.py
- Enter the following information into the
redis_db.pyfile. Replaceexample-redis-passwordwith the correct password for the Redis server:
[label ~/project/redis_db.py]
import redis
class RedisDb:
def db_con(self):
r_host = 'localhost'
r_port = 6379
r_pass = '<^>example-redis-password<^>'
redis_con = redis.Redis(host = r_host, port = r_port, password = r_pass)
return redis_con
- Save and close the
redis_db.pyfile.
- The above file has one class (
RedisDb:).
- Under this class, the
db_con(self):method uses the provided credentials to connect to the Redis server and returns a reusable connection using thereturn redis_constatement.
After setting up the Redis class, create the main file for your project in the next step.
Step 5 — Creating the Application's Entry Point
Every Python application must have an entry point or the main file that executes when the application runs. In this file, you'll create a code that shows the current server's time for authenticated users. This file uses the custom MySQL and Redis modules you created to authenticate users. Follow the steps below to create the file:
- Open a new
index.pyfile:
nano index.py
- Enter the following information into the
index.pyfile:
[label ~/project/index.py]
from encodings import utf_8
import base64
from hashlib import md5
import json
import datetime
import http.server
from http import HTTPStatus
import socketserver
import mysql_db
import redis_db
class HttpHandler(http.server.SimpleHTTPRequestHandler):
def do_GET(self):
self.send_response(HTTPStatus.OK)
self.send_header('Content-type', 'application/json')
self.end_headers()
authHeader = self.headers.get('Authorization').split(' ');
auth_user, auth_password = base64.b64decode(authHeader[1]).decode('utf8').split(':')
mysql_server = mysql_db.MysqlDb()
redis_server = redis_db.RedisDb()
redis_client = redis_server.db_con()
now = datetime.datetime.now()
current_time = now.strftime("%Y-%m-%d %H:%M:%S")
resp = {}
if redis_client.exists(auth_user):
if md5(auth_password.encode('utf8')).hexdigest() != redis_client.get(auth_user).decode('utf8'):
resp = {"error": "Invalid username/password."}
else:
resp = {"time": current_time, "authorized by": "Redis server"}
else:
mysql_resp = mysql_server.query(auth_user, auth_password)
if mysql_resp == False:
resp = {"error": "Invalid username/password."}
else:
resp = {"time": current_time, "authorized by": "MySQL server"}
redis_client.set(auth_user, mysql_resp)
self.wfile.write(bytes(json.dumps(resp, indent = 2) + "\r\n", "utf8"))
httpd = socketserver.TCPServer(('', 8080), HttpHandler)
print("Web server is running on port 8080...")
try:
httpd.serve_forever()
except KeyboardInterrupt:
httpd.server_close()
print("Web server has stopped runing.")
- Save and close the
index.pyfile.
- In the
index.pyfile, theimport...section adds the following modules to your project: utf_8,base64,md5, andjson, text encoding and formatting modules.
http.server,HTTPStatus, andsocketserver, web server modules.
datetime, time/date module.
mysql_dbandredis_db, custom modules that you previously created to access the MySQL and Redis servers.
- The
HttpHandler(http.server.SimpleHTTPRequestHandler):is a handler class for the HTTP server. Under the class, thedo_GET(self):method servers the HTTP GET requests and displays the system's date/time for authenticated users.
- In the
if ... : else: ...logic, the Python script runs the logicalif redis_client.exists(auth_user):statement to check if the user's credentials exist in the Redis server. If the user details exist and the Redis stored password doesn't match the user's submitted password, the application returns the{"error": "Invalid username/password."}error.
If the user details do not exist in the Redis server, the application queries the MySQL database server using the mysql_resp = mysql_server.query(auth_user, auth_password) statement. In case the user's supplied password doesn't match the database stored value, the application returns the {"error": "Invalid username/password."} error. Otherwise, the application caches the user's details in the Redis server using the redis_client.set(auth_user, mysql_resp) statement.
- In all cases where the user's credentials match the Redis/MySQL details, the application displays the system's current date/time using the
{"time": current_time, ...}statement. Theauthorized byentry in the output allows you to see the database server that authenticates the users in the application.
if redis_client.exists(auth_user):
if md5(auth_password.encode('utf8')).hexdigest() != redis_client.get(auth_user).decode('utf8'):
resp = {"error": "Invalid username/password."}
else:
resp = {"time": current_time, "authorized by": "Redis server"}
else:
mysql_resp = mysql_server.query(auth_user, auth_password)
if mysql_resp == False:
resp = {"error": "Invalid username/password."}
else:
resp = {"time": current_time, "authorized by": "MySQL server"}
redis_client.set(auth_user, mysql_resp)
You have now set up the main file for the application. In the next step, you'll test the application.
Step 6 — Testing the Application
In this step, you'll run your application to see if the Redis caching mechanism works. Execute the commands below to test the application:
- Use the following
python3command to run the application:
python3 index.py
- Ensure the application's custom web server is running:
[secondary_label Output]
Web server is running on port 8080...
- Establish another
SSHconnection to your server in a new terminal window and run the followingcurlcommands to send four GET request usingjohn_doe'scredentials. Append[1-4]at the end of thehttp://localhost:8080/URL to send the four requests in a single command:
curl -X GET -u john_doe:password_1 http://localhost:8080/[1-4]
- Verify the following outputs. The MySQL server only serves the first authentication request. Then, the Redis database serves the next three requests.
[secondary_label Output]
[1/4]
{
"time": "2023-11-07 10:04:38",
"authorized by": "MySQL server"
}
[4/4]
{
"time": "2023-11-07 10:04:38",
"authorized by": "Redis server"
}
Your application logic is now working as expected.
Conclusion
In this guide, you built a Python application that uses the Redis server to cache users' login credentials. Redis is a highly available and scalable database server that can perform thousands of transactions per second. With the Redis caching mechanism in your application, you can highly reduce traffic in your backend database server. To learn more about Redis applications, refer to our Redis tutorials .