The AccessControl class is a request security gate that runs automatically on every request before any application code executes. It checks the visitor's IP address against up to three independent blocking rules and throws a 403 Forbidden exception the moment any rule matches.
- Class:
JiFramework\Core\Security\AccessControl - Access:
$app->accessControl - Runs: automatically inside
App::__construct()— no manual call needed
The three gates (in order):
- IP block list — blocks individual IPv4 or IPv6 addresses. Checked against a local JSON file. No API call required.
- VPN / proxy detection — blocks visitors using VPNs, proxies, or TOR. Uses the ProxyCheck.io API with 12-hour caching.
- Country block list — blocks visitors from specific countries by ISO country code. Also uses the ProxyCheck.io API (shared with the VPN check — only one API call per visitor).
Each gate is individually enabled or disabled in your config/jiconfig.php. All gates default to off — the class does nothing unless you explicitly enable a gate.
Fail-open policy: if the ProxyCheck.io API is unreachable, access is allowed. Legitimate visitors are never blocked due to a third-party outage.
// AccessControl runs automatically — no code needed in index.php.
// The App constructor handles it:
$app = new App(); // gate fires here — 403 thrown if visitor is blocked
All options are set in config/jiconfig.php. Every option defaults to the safest value — all gates disabled, VPN allowed.
// config/jiconfig.php
return [
// ── IP blocking ──────────────────────────────────────────────────────────
// Enable or disable the IP block list gate.
'ip_blocking_enabled' => false,
// Path to your IP block list JSON file.
// Default: storage/AccessControl/ip_block_list.json
'ip_block_list_path' => null,
// ── Country blocking ─────────────────────────────────────────────────────
// Enable or disable the country block list gate.
'country_blocking_enabled' => false,
// Path to your country block list JSON file.
// Default: storage/AccessControl/country_block_list.json
'country_block_list_path' => null,
// ── VPN / proxy blocking ─────────────────────────────────────────────────
// Set to false to block visitors using VPNs, proxies, or TOR.
// Requires a ProxyCheck.io API key.
'allow_vpn_proxy' => true,
// ── ProxyCheck.io API ─────────────────────────────────────────────────────
// Required when country blocking or VPN blocking is enabled.
// Free tier: 1,000 lookups/day. Paid plans available at proxycheck.io.
'proxycheck_api_key' => '',
'proxycheck_api_url' => 'https://proxycheck.io/v2/{ip}',
];
IP resolution: The visitor's IP is always read from REMOTE_ADDR. Forwarding headers such as HTTP_CLIENT_IP and X-Forwarded-For are intentionally ignored — they are user-controlled and trivially spoofable, which would allow any visitor to bypass IP block rules by forging a header value.
isAccessAllowed(): bool
Runs the active gates against the current visitor's IP address. Returns true when all active gates pass, false when any gate blocks the visitor.
This method is called automatically by the framework inside App::__construct(). You rarely need to call it directly, but it is available on $app->accessControl if you want to re-check access at a later point in your code.
// Called automatically — no need to write this yourself:
if (!$app->accessControl->isAccessAllowed()) {
throw new HttpException(403, 'Access Denied');
}
Gate execution order:
- 1. IP block list — checked first (local file, instant). If the visitor's IP is in the list, the remaining gates are skipped and
falseis returned immediately. - 2. VPN / proxy — checked via ProxyCheck.io (or from cache). Only runs when
allow_vpn_proxyisfalse. - 3. Country block list — checked using the same ProxyCheck.io response as gate 2 — no second API call. Only runs when
country_blocking_enabledistrue.
When the ProxyCheck.io API is unreachable (gates 2 and 3), the method returns true to avoid blocking legitimate visitors during a third-party outage.
blockIp(string $ip): bool
Adds an IP address to the block list file. Both IPv4 and IPv6 addresses are accepted. The operation is idempotent — adding an IP that is already blocked returns true without creating a duplicate entry.
$ip— (string) A valid IPv4 or IPv6 address.
Returns false when $ip is not a valid IP address, or when the block list file cannot be written.
// Block a single IPv4 address
$app->accessControl->blockIp('203.0.113.5');
// Block an IPv6 address
$app->accessControl->blockIp('2001:db8::1');
// Typical usage — block on repeated failed logins
if ($failedAttempts >= 10) {
$app->accessControl->blockIp($visitorIp);
}
unblockIp(string $ip): bool
Removes an IP address from the block list. Also idempotent — removing an IP that is not in the list returns true. Returns false only when the block list file cannot be written.
$ip— (string) The IP address to remove.
// Remove a previously blocked IP
$app->accessControl->unblockIp('203.0.113.5');
Changes take effect on the next request. The block list file is read fresh on every call to isAccessAllowed() — there is no in-memory state to clear.
Note: blockIp() and unblockIp() only work when ip_blocking_enabled is true in your config. If the gate is disabled the IP is still written to the file, but isAccessAllowed() will not read it.
The IP block list is a plain JSON file containing an array of IP address strings. It lives at storage/AccessControl/ip_block_list.json by default — inside your project's storage/ directory alongside the cache, logs, and rate-limit data.
The file ships empty. Add IPs to it using blockIp() or by editing the file directly:
// storage/AccessControl/ip_block_list.json
[
"192.0.2.1",
"198.51.100.10",
"2001:db8::ff00:42:8329"
]
To use a custom path, set ip_block_list_path in your config:
// config/jiconfig.php
return [
'ip_blocking_enabled' => true,
'ip_block_list_path' => null, // default: storage/AccessControl/ip_block_list.json
];
You can manage the list at runtime using blockIp() and unblockIp(), or edit the JSON file directly. Both IPv4 and IPv6 addresses are supported. The list is read from disk on every request — no server restart is needed after changes.
If the file does not exist, the IP gate is skipped and access is allowed. The file is created automatically the first time you call blockIp().
If the file contains invalid JSON, the gate skips silently rather than crashing the application. Fix the file as soon as possible if this happens.
The country block list is a JSON file containing an array of two-letter ISO 3166-1 alpha-2 country codes. It lives at storage/AccessControl/country_block_list.json by default.
The file ships empty. Add country codes to it manually or deploy it as part of your project setup:
// storage/AccessControl/country_block_list.json
[
"CN",
"RU",
"KP"
]
Country codes are case-sensitive and must be uppercase. You can find the full list of codes at ISO 3166-1 alpha-2.
To enable country blocking:
// config/jiconfig.php
return [
'country_blocking_enabled' => true,
'country_block_list_path' => null, // default: storage/AccessControl/country_block_list.json
'proxycheck_api_key' => 'your-api-key-here',
];
Country detection requires a ProxyCheck.io API lookup. The result is cached for 12 hours per IP address, so repeated visitors do not trigger repeated API calls. The free ProxyCheck.io tier allows 1,000 lookups per day.
If the file does not exist, the country gate is skipped and access is allowed even if country_blocking_enabled is true.
When allow_vpn_proxy is set to false, visitors connecting through a VPN, proxy, or TOR exit node are blocked. Detection is powered by the ProxyCheck.io API.
// config/jiconfig.php
return [
'allow_vpn_proxy' => false, // block VPNs and proxies
'proxycheck_api_key' => 'your-key', // required for detection
];
The VPN gate is independent of the country gate. You can block VPNs without enabling country blocking, and vice versa.
// VPN blocking on, country blocking off — works correctly
return [
'allow_vpn_proxy' => false,
'country_blocking_enabled' => false,
'proxycheck_api_key' => 'your-key',
];
API caching: the ProxyCheck.io response for each IP is cached for 12 hours. When both VPN and country blocking are active, both checks share a single API lookup — not two separate calls.
API failure: if the ProxyCheck.io API is unreachable, the VPN gate is skipped and access is allowed. Your site remains functional during API outages.
Free tier limits: ProxyCheck.io's free tier allows 1,000 daily lookups. With 12-hour caching each unique visitor consumes at most two lookups per day. For high-traffic sites, a paid plan is recommended.
Because blockIp() and unblockIp() write directly to the JSON file, you can manage the block list from any part of your application — admin panels, login handlers, API endpoints, or scheduled tasks.
Example: auto-block after repeated failed logins
// pages/login.php
$ip = $_SERVER['REMOTE_ADDR'];
$cacheKey = 'login_fails_' . md5($ip);
$fails = (int) ($app->cache->get($cacheKey) ?? 0);
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$ok = $app->auth->loginUser($_POST['email'], $_POST['password']);
if (!$ok) {
$fails++;
$app->cache->set($cacheKey, $fails, 900); // track for 15 minutes
if ($fails >= 5) {
$app->accessControl->blockIp($ip);
$app->logger->warning('IP blocked after {n} failed logins: {ip}', [
'n' => $fails,
'ip' => $ip,
]);
$app->abort(403, 'Too many failed attempts. Your IP has been blocked.');
}
}
}
Example: admin panel to manage the block list
// pages/admin/block-ip.php
$action = $_POST['action'] ?? '';
$ip = trim($_POST['ip'] ?? '');
if ($action === 'block') {
$ok = $app->accessControl->blockIp($ip);
$msg = $ok ? "$ip has been blocked." : "Invalid IP address.";
}
if ($action === 'unblock') {
$ok = $app->accessControl->unblockIp($ip);
$msg = $ok ? "$ip has been unblocked." : "Could not update block list.";
}