实现IP锁定功能:Java和Lua的实践应用

1,231 阅读9分钟

Lua是一种轻量级的脚本语言,设计之初是为了嵌入应用程序中。它具有简单易懂的语法和较高的执行效率,因此常用于游戏开发、配置脚本、数据处理等领域。Lua脚本通常被集成到其他程序中,实现特定功能或逻辑。

Lua 简介

Lua 是一个小巧的脚本语言,是巴西里约热内卢天主教大学里的一个研究小组于1993年开发的。

Lua 使用标准 C 语言编写并以源代码形式开放,几乎在所有操作系统和平台上都能编译运行。Lua 脚本可以调用 C/C++ 的函数,也可以被 C/C++ 代码调用,所以 Lua 在应用程序中可以被广泛应用。

Lua 并没有提供强大的库,这是由它的定位决定的。所以 Lua 不适合作为开发独立应用程序的语言。其设计目的是为了通过灵活嵌入应用程序中从而为应用程序提供灵活的扩展和定制功能。

Lua 体积小、启动速度快,一个完整的 Lua 解释器不过200k,在所有脚本引擎中,Lua 的速可以于说是最快的。所以 Lua 是作为嵌入式脚本的最佳选择。这也就是我们为什么要学习 Lua 这门语言。

那 Lua 语言能干吗呢?其实它主要是用作脚本语言,用来开发脚本,例如编写游戏辅助脚本,在 Redis 中使用 Lua 脚本等。

Lua 官网地址:www.lua.org/


Lua 特性

  • 轻量级:Lua 使用标准 C 语言编写,Lua 语言的官方版本只包括一个精简的核心和最基本的库,体积小、启动速度快,一个完整的 Lua 解释器不过200k,适合嵌入在别的程序里。

  • 可扩展:Lua 提供了非常易于使用的扩展接口和机制,由宿主语言(通常是 C 或 C++ )提供这些功能,Lua 可以使用它们,就像是本来就内置的功能一样。

  • 其它特性

    • 支持面向过程( procedure-oriented )编程和函数式编程( functional programming );
    • 自动内存管理;
    • 只提供了一种通用类型的表(table),但可以用它实现数组,哈希表,集合,对象;
    • 闭包( closure ),通过闭包和表可以很方便地支持面向对象编程所需要的一些关键机制,比如数据抽象,虚函数,继承和重载等。;
    • 提供多线程(协同进程,并非操作系统所支持的线程)支持;

应用场景

  • 游戏开发,例如游戏辅助脚本。
  • 应用脚本,例如 Redis 使用 Lua 脚本。
  • 数据库插件,例如 MySQL Proxy 和 MySQL WorkBench。
  • 安全系统,如入侵检测系统。

Lua 脚本的基本语法

Lua 的语法简单直观,以下是一些常用的语法:

  • 变量:使用local关键字声明局部变量,例如:local a = 10
  • 条件语句:使用ifthenelseifelseend实现条件判断,例如:
    if x > 0 then
        print("x is positive")
    elseif x == 0 then
        print("x is zero")
    else
        print("x is negative")
    end
    
  • 循环:支持forwhile循环,例如:
    for i = 1, 10 do
        print(i)
    end
    
  • 函数:使用function定义函数,例如:
    function add(a, b)
        return a + b
    end
    

使用 Redis + Lua 脚本实现限制 IP 多次输入错误密码功能

Redis 是一个高性能的键值数据库,支持 Lua 脚本以实现原子操作。以下是通过 RedisLua 脚本限制同一 IP 多次输入错误密码的实现。

应用场景

假设我们要限制同一 IP 在短时间内(例如 10 分钟)输入错误密码不超过 5 次,否则将锁定该 IP 一段时间。

Lua 脚本示例

-- 限制IP多次输入错误密码的Lua脚本
local ip = KEYS[1]
local current_time = tonumber(ARGV[1])
local expire_time = 600  -- 10分钟
local max_attempts = 5
local lock_duration = 3600  -- 锁定时间1小时

-- 获取当前错误计数和上次错误时间
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  -- -1表示IP已被锁定
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

当然,以下是上述 Lua 脚本中每个参数和变量的解释:

  1. KEYS[1] (ip):

    • 这是传递给 Lua 脚本的第一个键参数,表示需要限制的 IP 地址。通过这个参数,Redis 可以针对特定 IP 进行操作。
  2. ARGV[1] (current_time):

    • 这是传递给 Lua 脚本的第一个参数,表示当前的 Unix 时间戳(以秒为单位)。这个时间戳用于检查 IP 是否已经被锁定以及更新锁定时间。
  3. expire_time:

    • 表示错误尝试计数的有效期,这里设定为 600 秒(10 分钟)。在此时间内,如果错误尝试次数没有达到最大限制,计数会自动失效。
  4. max_attempts:

    • 最大允许的错误尝试次数。这里设置为 5 次,意味着在 10 分钟内,如果某个 IP 输入错误密码的次数达到 5 次,则会触发锁定机制。
  5. lock_duration:

    • 锁定时长,单位为秒。这里设置为 3600 秒(1 小时)。如果 IP 被锁定,它将在 1 小时内无法进行任何新的尝试。
  6. attempts:

    • 当前错误尝试计数,从 Redis 中获取。使用 redis.call('get', ip) 来获取指定 IP 的错误次数,如果不存在则默认为 0。
  7. lock_time:

    • IP 的锁定截止时间,从 Redis 中获取。使用 redis.call('get', ip .. ':lock') 获取当前 IP 的锁定时间戳,如果不存在则默认为 0。

脚本逻辑流程

  • 首先,从 Redis 中获取当前 IP 的错误尝试次数和锁定时间。
  • 检查当前时间是否小于锁定时间,若是,说明 IP 已被锁定,返回 -1
  • 如果未锁定,增加错误尝试次数。
  • 检查尝试次数是否达到或超过最大次数 (max_attempts)。
    • 如果达到或超过,则锁定 IP,设置锁定时间,并返回 -1
    • 如果未达到,则更新尝试次数,并设置尝试次数的过期时间为 expire_time

使用 Redis 执行 Lua 脚本

可以使用 Redis 提供的 EVAL 命令执行上述 Lua 脚本。以下是一个示例:

redis-cli --eval limit_login_attempts.lua 192.168.1.1 , 1620000000

在这里,192.168.1.1 是 IP 地址,1620000000是当前的 Unix 时间戳。

通过这种方式,可以有效限制一个 IP 在短时间内多次输入错误密码,防止暴力破解攻击。这种机制可以集成到登录系统中,以增强安全性。


为了更具体地结合项目应用场景,我们可以考虑一个示例场景:在一个用户登录系统中,需要限制某个 IP 地址在短时间内多次输入错误密码以防止暴力攻击。以下是一个完整的 Java 应用示例,演示如何使用 RedisLua 脚本来实现这一功能。

项目背景

在用户登录系统中,我们希望限制某个 IP 地址在 10 分钟内最多输入错误密码 5 次。如果超过这个次数,则在接下来的 1 小时内禁止该 IP 的登录尝试。

Java 代码示例

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 in seconds
        "local max_attempts = 5 " +
        "local lock_duration = 3600 " +  // 1 hour in seconds
        "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 " +  // Lock the IP
        "else " +
        "    redis.call('set', ip, attempts) " +
        "    redis.call('expire', ip, expire_time) " +
        "    return attempts " +  // Return current attempt count
        "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;  // Get current time in seconds
            Object result = jedis.eval(LUA_SCRIPT, 1, ip, String.valueOf(currentTime));
            return ((Long) result).intValue();
        }
    }

    public static void main(String[] args) {
        // Create a connection pool to the Redis server
        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);
            }
        }
    }
}

代码解释

  1. Lua Script: 定义了一个 Lua 脚本,它检查和更新错误登录尝试次数,并在超过限制时锁定 IP
  2. JedisPool: 用于管理 Redis 连接,保证多线程环境下的连接安全。
  3. checkLoginAttempts 方法: 执行 Lua 脚本并返回结果,表示当前错误尝试次数或是否被锁定。
  4. Main 方法: 示例中使用特定 IP 地址进行测试,执行登录尝试检查并输出结果。

在上述 Java 代码中,eval 方法用于执行 Lua 脚本,并传递参数给脚本。eval 方法的参数列表如下:

Object result = jedis.eval(LUA_SCRIPT, 1, ip, String.valueOf(currentTime));

传参细节

  1. LUA_SCRIPT

    • 这是要执行的 Lua 脚本的字符串。脚本中定义了逻辑,用于限制 IP 的登录尝试次数。
  2. 1

    • 这是 eval 方法的第二个参数,表示有多少个键(keys)被传递给 Lua 脚本。在这个例子中,我们只传递了一个键(IP 地址),所以值是 1
  3. ip

    • 这是传递给 Lua 脚本的键参数(KEYS[1])。在 Lua 脚本中通过 KEYS[1] 来访问这个值。
  4. String.valueOf(currentTime)

    • 这是传递给 Lua 脚本的附加参数(ARGV[1])。在 Lua 脚本中通过 ARGV[1] 来访问这个值。这里表示当前的 Unix 时间戳,以秒为单位。

Lua 脚本中的参数使用

  • KEYS[1] 对应传入的 ip,即需要限制的 IP 地址。
  • ARGV[1] 对应传入的 currentTime,即当前时间,用于判断是否需要锁定 IP。

此结构允许对指定的 IP 地址进行操作,判断其登录尝试情况,并根据当前时间做出相应处理。通过 eval 方法传递的参数,可以动态地影响脚本的执行逻辑。

应用场景

这段代码可以集成到实际的登录验证逻辑中,例如在用户每次输入密码时调用 checkLoginAttempts 方法,判断是否允许继续登录尝试。通过这种方式,可以有效地防止暴力破解攻击,保护用户账户安全。