Introduction to Lua and Using Redis + Lua Scripts to Limit IP Login Attempts
This article introduces the lightweight Lua scripting language, outlines its key features and typical use cases, and provides a complete example of using Redis together with Lua scripts (and a Java client) to enforce IP‑based login‑attempt limits for improved security.
Lua is a lightweight, embeddable scripting language created in 1993 by a research group at the Pontifical Catholic University of Rio de Janeiro. Written in standard C, it runs on virtually any operating system, can call C/C++ functions and be called from them, and its interpreter is only about 200 KB, making it ideal for embedding.
Lua Overview
Lua provides a minimal core and basic libraries, focusing on simplicity and speed rather than a large standard library. It supports procedural and functional programming, automatic memory management, tables that can act as arrays, hash tables, sets, or objects, closures for object‑oriented patterns, and lightweight coroutines.
Key Features
Lightweight: Small size and fast startup, perfect for embedding.
Extensible: Easy to extend via C/C++ host interfaces.
Supports procedural, functional, and object‑oriented styles.
Automatic memory management.
Coroutines for cooperative multitasking.
Typical Application Scenarios
Game development (e.g., cheat scripts).
Application scripting (e.g., Redis Lua scripts).
Database plugins (MySQL Proxy, MySQL Workbench).
Security systems such as intrusion‑detection.
Basic Lua Syntax
Variable declaration:
local a = 10Conditional statements:
if x > 0 then
print("x is positive")
elseif x == 0 then
print("x is zero")
else
print("x is negative")
endLoops:
for i = 1, 10 do
print(i)
endFunction definition:
function add(a, b)
return a + b
endUsing Redis + Lua to Limit IP Login Attempts
Redis supports executing Lua scripts atomically via the EVAL command. The following Lua script limits an IP address to five failed password attempts within ten minutes; exceeding this threshold locks the IP for one hour.
-- Limit IP multiple failed password attempts
local ip = KEYS[1]
local current_time = tonumber(ARGV[1])
local expire_time = 600 -- 10 minutes
local max_attempts = 5
local lock_duration = 3600 -- 1 hour
local attempts = tonumber(redis.call('get', ip) or '0')
local lock_time = tonumber(redis.call('get', ip .. ':lock') or '0')
if current_time < lock_time then
return -1 -- IP is locked
end
attempts = attempts + 1
if attempts >= max_attempts then
redis.call('set', ip .. ':lock', current_time + lock_duration)
return -1
else
redis.call('set', ip, attempts)
redis.call('expire', ip, expire_time)
return attempts
endThe script receives the target IP as KEYS[1] and the current Unix timestamp as ARGV[1] . It checks whether the IP is already locked, updates the failure count, and either locks the IP or refreshes the counter with an expiration.
Script Logic Flow
Fetch current attempt count and lock timestamp from Redis.
If the current time is less than the lock timestamp, return -1 (locked).
Otherwise, increment the attempt count.
If the count reaches max_attempts , set a lock key with lock_duration and return -1 .
If not, store the updated count and set its expiration to expire_time .
Execution via Redis CLI:
redis-cli --eval limit_login_attempts.lua 192.168.1.1 , 1620000000Java Integration Example (Jedis)
The following Java class demonstrates how to load the Lua script as a string, evaluate it with Jedis, and interpret the result.
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
public class LoginAttemptLimiter {
private static final String LUA_SCRIPT =
"local ip = KEYS[1] " +
"local current_time = tonumber(ARGV[1]) " +
"local expire_time = 600 " + // 10 minutes
"local max_attempts = 5 " +
"local lock_duration = 3600 " + // 1 hour
"local attempts = tonumber(redis.call('get', ip) or '0') " +
"local lock_time = tonumber(redis.call('get', ip .. ':lock') or '0') " +
"if current_time < lock_time then return -1 end " +
"attempts = attempts + 1 " +
"if attempts >= max_attempts then " +
" redis.call('set', ip .. ':lock', current_time + lock_duration) " +
" return -1 " +
"else " +
" redis.call('set', ip, attempts) " +
" redis.call('expire', ip, expire_time) " +
" return attempts " +
"end";
private final JedisPool jedisPool;
public LoginAttemptLimiter(JedisPool jedisPool) {
this.jedisPool = jedisPool;
}
public int checkLoginAttempts(String ip) {
try (Jedis jedis = jedisPool.getResource()) {
long currentTime = System.currentTimeMillis() / 1000;
Object result = jedis.eval(LUA_SCRIPT, 1, ip, String.valueOf(currentTime));
return ((Long) result).intValue();
}
}
public static void main(String[] args) {
try (JedisPool jedisPool = new JedisPool("localhost", 6379)) {
LoginAttemptLimiter limiter = new LoginAttemptLimiter(jedisPool);
String ip = "192.168.1.1";
int attemptResult = limiter.checkLoginAttempts(ip);
if (attemptResult == -1) {
System.out.println("IP is locked due to too many failed attempts.");
} else {
System.out.println("Failed attempt count for IP: " + attemptResult);
}
}
}
}The eval call passes the script, the number of keys (1), the IP address, and the current timestamp. The method returns the current failure count or -1 if the IP is locked.
Parameter Details
KEYS[1] (ip): The target IP address.
ARGV[1] (current_time): Current Unix time in seconds.
expire_time: Validity period of the failure counter (600 s).
max_attempts: Maximum allowed failures before locking (5).
lock_duration: Lock period after exceeding attempts (3600 s).
attempts: Current failure count fetched from Redis.
lock_time: Timestamp when the lock expires.
Integrating this logic into a login service allows automatic, atomic enforcement of rate‑limiting policies, helping to mitigate brute‑force attacks.
Selected Java Interview Questions
A professional Java tech channel sharing common knowledge to help developers fill gaps. Follow us!
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.