Some applications managers choose to deny access to users based on their location. This is denominated geoblocking, for example it can be used on some shopping websites that might choose not to have visitors from countries they don't ship goods to. If you are willing to implement this feature in your Symfony 5 project, you have found the right place.
This tutorial follows some steps of a previous article, where we explain how to detect the city, country and locale from a visitors IP in Symfony 3, but with the modification that it should restrict the access to the entire website.
1. Download GeoLite2 Free Databases
As first step you will need the binary database of GeoLite in your project or accesible at system level. You can download the database from the official MaxMind GeoLite2 free here. In this page, you will need to sign up for a GeoLite2 account:
After creating the account and follow the steps that you received via email, you will be able to download the MaxMind DB's privately, in this case we will use the GeoLite2 Country version:
In this tutorial we'll use and include the database in our own project in the /private
directory of our symfony project (note that this directory doesn't exist and therefore needs to be created, you can change the path of the database according to your needs) and we'll use the Country version of the database that allows us to get the information that we mentioned in this article, specifically the Country of the visitor's IP. The databases are compressed using tar, so you would be able to extract its content from the command line using the following command:
tar -xzf GeoLite2-Country_20200121.tar.gz
Alternatively in other operative systems like Windows you could simply extract its content using 7Zip, Winrar or another decompression tool. Now that you have the database in your project, the directory structure should look like:
project/
âââ bin
âââ composer.json
âââ composer.lock
âââ config
âââ nbproject
âââ phpunit.xml.dist
âââ private/
â âââ geolite2-country/
â âââ COPYRIGHT.txt
â âââ GeoLite2-Country.mmdb
â âââ LICENSE.txt
âââ public
âââ src
âââ symfony.lock
âââ templates
âââ tests
âââ translations
âââ var
âââ vendor
2. Install MaxMind GeoIP2 PHP API
In order to read the database, you won't need to host in MySQL or other database manager. The Database has a special format from the creators of GeoIP namely MaxMind DB. The MaxMind DB file format is a database format that maps IPv4 and IPv6 addresses to data records using an efficient binary search tree. The binary database is split into three parts:
- The binary search tree. Each level of the tree corresponds to a single bit in the 128 bit representation of an IPv6 address.
- The data section. These are the values returned to the client for a specific IP address, e.g. “US”, “New York”, or a more complex map type made up of multiple fields.
- Database metadata. Information about the database itself.
For more information about the type of database used by this project, please read more about MaxMind DB here.
Now, we need a parser for this database format. Fortunately, the MaxMind team wrote an awesome library for PHP that makes the interaction with the database pretty easy and you will be able to retrieve geo information about an user's IP with only a couple of lines of code. We are talking about the MaxMind GeoIP PHP Api, a package that provides an API for the GeoIP2 web services and databases. The API also works with the free GeoLite2 databases (the one that we're using). You can install this package in your Symfony project with composer running the following command:
composer require geoip2/geoip2
For more information about this library, please visit the official repository at Github here. After the installation of the package you will be able to use its classes on your controllers of Symfony.
3. Create the Request Listener
On every Symfony application, a lot of stuff happens under the hood, so we need to know when that stuff happens. Symfony let you know when that happen through events, it triggers several events related to the kernel while processing the HTTP Request. That's exactly where we are going to identify the users country and determine if he should have access or not. The logic will be the following one, create a RequestListener class inside the EventListener directory of your project source. This class will receive in the constructor only 2 parameters, the first one is the absolute path of the project that can be injected to the class with the %kernel.project_dir% variable in the services.yaml file. The second parameter will be the templating engine (Twig), so we can render a view that will notice the user that the website is blocked.
Additionally, the class will have a private array variable that will contain the ISO codes of the countries that can't access the website, you nee to update it according to your needs, in our case we will simply block 4 countries:
- Colombia
- Brazil
- Bolivia
- United States
The listener class will respond to the onKernelRequest
event of Symfony and will call the RestrictAccessOnDisallowedCountries
method on the runtime, providing as first argument the RequestEvent variable. The mentioned method will read the GeoLite2 database and will check for the country of the user's IP. If there's a result, it will obtain the iso code of the country and it will check if the visitors code is in the blacklist, if it is, a new response will be sent with a custom Twig view that contains the message for the user (note that the response code will be 403 Forbidden):
<?php
// src/EventListener/RequestListener.php
namespace App\EventListener;
use Symfony\Component\HttpKernel\Event\RequestEvent;
use Symfony\Component\HttpFoundation\Response;
use Twig\Environment;
// 1. Include GeoIp2 Classes
use GeoIp2\Database\Reader;
use GeoIp2\Exception\AddressNotFoundException;
class RequestListener
{
// Store the absolute path of the project injected through the service
private $projectDir;
/* @var $twig \Twig\Environment */
private $twig;
/**
* An array with all the ISO codes of the countries where the website shouldn't be accessible
*
* @var Array
*/
private $blacklist = [
"CO", // Colombia
"BO", // Bolivia
"BR", // Brazil
"US", // United States
];
public function __construct(Environment $twigEnvironment, $projectDir)
{
$this->projectDir = $projectDir;
$this->twig = $twigEnvironment;
}
/**
* Run the verification of the users country on every request.
*
* @param RequestEvent $event
* @return type
*/
public function onKernelRequest(RequestEvent $event)
{
if (!$event->isMasterRequest()) {
// don't do anything if it's not the master request
return;
}
$this->RestrictAccessOnDisallowedCountries($event);
}
private function RestrictAccessOnDisallowedCountries(RequestEvent $event)
{
/* @var $request \Symfony\Component\HttpFoundation\Request */
$request = $event->getRequest();
// Declare the path to the GeoLite2-City.mmdb file (database)
$GeoLiteDatabasePath = $this->projectDir . '/private/geolite2-country/GeoLite2-Country.mmdb';
// Create an instance of the Reader of GeoIp2 and provide as first argument
// the path to the database file
$reader = new Reader($GeoLiteDatabasePath);
// Check against the GeoLite database the user's country through his IP
try{
// You can as well test with a fixed IP, for example one of USA in Minessota:
// $reader->country('128.101.101.101');
// However for production, request the client ip:
// $reader->country($request->getClientIp());
/* @var $record \GeoIp2\Model\Country */
$record = $reader->country($request->getClientIp());
$isoCode = $record->country->isoCode;
// If the obtained iso code matches with one of the blacklisted countries, block the access
// rendering a custom page
if(in_array($isoCode, $this->blacklist)){
$response = new Response();
$response->setStatusCode(Response::HTTP_FORBIDDEN);
// Render some twig view, in our case we will render the blocked.html.twig file
$response->setContent($this->twig->render("pages/blocked.html.twig", [
'code' => $isoCode
]));
// Return an HTML file
$response->headers->set('Content-Type', 'text/html');
// Send response
$event->setResponse($response);
}
} catch (AddressNotFoundException $ex) {
// Couldn't retrieve geo information from the given IP
// Is up to you if you want to block the access to the website anyway here ...
}
}
}
Note that this approach doesn't do anything if the country of the visitor can't be identified. As you can see, in the code we render the following Twig view:
{# application/templates/pages/blocked.html.twig #}
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Website Blocked</title>
{% block stylesheets %}{% endblock %}
</head>
<body>
<h1>Access Disallowed</h1>
<p>This website doesn't work in your country ({{ code }})</p>
</body>
</html>
4. Register Event Listener on the services.yaml file
Finally, in order to enable the event listener, unlike services that are autowired, you will need to register the event listener on the services.yaml file like this:
# /application/config/services.yaml
services:
App\EventListener\RequestListener:
tags:
- { name: kernel.event_listener, event: kernel.request }
bind:
$projectDir: '%kernel.project_dir%'
As described in the step 3, we will bind the project directory as a parameter. And that's it ! Clear the cache of your project and try accessing your application through a VPN or set a fixed ip manually to the listener and check that the geoblocking is working properly.
Happy Coding !