How to Enforce Login Attempt Limits with Spring Boot, Redis, and Lua Scripts
This article walks through the problem of locking out users after multiple failed login attempts, explains why IP‑based locking with Redis and Lua scripts is effective, and provides a complete Spring Boot implementation—including front‑end HTML, custom annotations, AOP aspect, Redis configuration, and sample controller code—to enforce a configurable login‑attempt limit.
Background
Forgotten password scenarios often block further attempts after a few failures. The goal is to limit login attempts per IP address, lock the IP after a configurable number of failures, and automatically lift the lock after a short expiration period.
Key Questions
Is the lock triggered only by an incorrect password or by any wrong credential (username or password)?
Why does the lock automatically lift after a few minutes?
What technology stack can implement this efficiently?
Solution Overview
The implementation uses Spring Boot 2.7.11 , Redis for distributed counters, and a Lua script to perform the counter operations atomically in a single round‑trip.
Front‑end
A minimal HTML login page submits username and password to the /login endpoint.
<!DOCTYPE html>
<html>
<head>
<title>Login Page</title>
<style>
body {background-color:#F5F5F5;}
form {width:300px;margin:100px auto;padding:20px;background:white;border-radius:5px;box-shadow:0 0 10px rgba(0,0,0,0.2);}
label, input {display:block;margin-bottom:10px;width:100%;}
input[type=submit] {background:#30B0F0;color:white;border:none;padding:10px;cursor:pointer;}
</style>
</head>
<body>
<form action="http://localhost:8080/login" method="get">
<label for="username">Username</label>
<input type="text" id="username" name="username" placeholder="Enter username" required>
<label for="password">Password</label>
<input type="password" id="password" name="password" placeholder="Enter password" required>
<input type="submit" value="Login">
</form>
</body>
</html>Backend Design
A custom annotation @LimitCount declares rate‑limit parameters (key, prefix, period in seconds, and maximum count). An AOP aspect intercepts methods annotated with @LimitCount, extracts the client IP, builds a Redis key in the form prefix_key_ip, and executes a Lua script via RedisTemplate. The script increments the counter, sets an expiration on the first increment, and returns the current count. If the count exceeds the configured limit, the aspect returns a lock‑out message; otherwise the original method proceeds.
The counter is incremented before the actual login logic runs, and the success or failure of the login does not affect the counter. The key expires automatically, allowing retries after the timeout.
Lua Script
local c
c = redis.call('get',KEYS[1])
if c and tonumber(c) > tonumber(ARGV[1]) then
return c
end
c = redis.call('incr',KEYS[1])
if tonumber(c) == 1 then
redis.call('expire',KEYS[1],ARGV[2])
end
return cKey Components
LimitCount annotation – defines name, key, prefix, period (seconds), and count (max attempts).
LimitCountAspect – obtains the HttpServletRequest, extracts the real client IP via IPUtil, builds the Redis key, runs the Lua script, logs the attempt, and either proceeds with the target method or returns a lock‑out response.
IPUtil – retrieves the client IP, handling common reverse‑proxy headers ( x-forwarded-for, Proxy-Client-IP, WL-Proxy-Client-IP) and falling back to request.getRemoteAddr().
RedisConfig – configures a Jedis pool, JedisConnectionFactory, a generic RedisTemplate, and a dedicated limitRedisTemplate with StringRedisSerializer for keys and GenericJackson2JsonRedisSerializer for values.
Configuration Files
pom.xml (excerpt)
<project xmlns="http://maven.apache.org/POM/4.0.0">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.7.11</version>
</parent>
<groupId>com.example</groupId>
<artifactId>LoginLimit</artifactId>
<version>0.0.1-SNAPSHOT</version>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
</dependency>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
</dependency>
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>23.0</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
</project>application.properties
spring.redis.host=127.0.0.1
spring.redis.port=6379
spring.redis.password=
spring.redis.timeout=1000
spring.redis.jedis.pool.max-idle=500
spring.redis.jedis.pool.max-active=2000
spring.redis.jedis.pool.max-wait=10000Controller
package com.example.loginlimit.controller;
import javax.servlet.http.HttpServletRequest;
import com.example.loginlimit.annotation.LimitCount;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
@Slf4j
@RestController
public class LoginController {
@GetMapping("/login")
@LimitCount(key = "login", name = "Login API", prefix = "limit")
public String login(@RequestParam String username,
@RequestParam String password,
HttpServletRequest request) {
if (StringUtils.equals("张三", username) && StringUtils.equals("123456", password)) {
return "Login successful";
}
return "Invalid username or password";
}
}Application Entry
package com.example.loginlimit;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class LoginLimitApplication {
public static void main(String[] args) {
SpringApplication.run(LoginLimitApplication.class, args);
}
}Demonstration
When the application is running, accessing /login from the same IP address results in a lockout after three failed attempts. Subsequent requests within the configured period (default 60 seconds) receive the response “Interface access exceeds frequency limit”. After the key expires, the counter resets and login attempts are allowed again.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Java Architect Essentials
Committed to sharing quality articles and tutorials to help Java programmers progress from junior to mid-level to senior architect. We curate high-quality learning resources, interview questions, videos, and projects from across the internet to help you systematically improve your Java architecture skills. Follow and reply '1024' to get Java programming resources. Learn together, grow together.
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.
