Implementing Login Attempt Limiting with Spring Boot, Redis, and Lua Scripts
This article explains how to prevent brute‑force password attempts by locking an IP after three failed logins using a Spring Boot backend, Redis for distributed counters, and a Lua script to ensure atomic increment and expiration, while also providing a simple HTML login page for the front end.
When users repeatedly enter wrong passwords, many systems lock the account after a few attempts; however, the lockout often applies to the IP address rather than the username. The author explores three questions: whether only password errors trigger the lock, why the lock expires after a few minutes, and the complexity of the technology stack.
After researching, the solution chosen is a combination of SpringBoot, Redis, and a Lua script. Redis provides a fast, distributed counter, and the Lua script performs atomic get‑incr‑expire operations to enforce the limit without race conditions.
Front‑end Implementation
A minimal HTML login form is created using plain HTML and CSS. The form submits the username and password to http://localhost:8080/login via a GET request.
<!DOCTYPE html>
<html>
<head>
<title>登录页面</title>
<style>
body {background-color:#F5F5F5;}
form {width:300px; margin:0 auto; margin-top:100px; padding:20px; background:white; border-radius:5px;}
label {display:block; margin-bottom:10px;}
input[type="text"], input[type="password"] {border:none; padding:10px; margin-bottom:20px; border-radius:5px; width:100%;}
input[type="submit"] {background:#30B0F0; color:white; border:none; padding:10px; border-radius:5px; width:100%; cursor:pointer;}
</style>
</head>
<body>
<form action="http://localhost:8080/login" method="get">
<label for="username">用户名</label>
<input type="text" id="username" name="username" placeholder="请输入用户名" required>
<label for="password">密码</label>
<input type="password" id="password" name="password" placeholder="请输入密码" required>
<input type="submit" value="登录">
</form>
</body>
</html>Backend Design
The flow diagram shows that the request count is incremented before the actual login logic, and the count expires after a configurable period, allowing further attempts.
Key reasons for using Redis + Lua:
Reduced network overhead by sending multiple commands in a single script.
Atomic execution guarantees no race conditions.
Script reuse across clients without rewriting logic.
The core components are:
LimitCount annotation – defines the limit parameters (key, prefix, period, count).
LimitCountAspect – an AOP aspect that intercepts methods annotated with @LimitCount , extracts the client IP, builds the Redis key, executes the Lua script, and either proceeds with the method or returns a rate‑limit message.
Lua script – checks the current counter, returns it if it exceeds the limit, otherwise increments it and sets an expiration.
RedisConfig – configures Redis connection, pool, and templates for both generic and string operations.
LoginController – a simple endpoint /login annotated with @LimitCount(key="login", name="登录接口", prefix="limit") that validates a hard‑coded username/password pair.
LoginLimitApplication – the Spring Boot entry point.
package com.example.loginlimit.annotation;
import java.lang.annotation.*;
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface LimitCount {
String name() default "";
String key() default "";
String prefix() default "";
int period() default 60; // seconds
int count() default 3; // max attempts
} package com.example.loginlimit.aspect;
@Aspect @Component @Slf4j
public class LimitCountAspect {
private final RedisTemplate
limitRedisTemplate;
@Autowired
public LimitCountAspect(RedisTemplate
limitRedisTemplate) {
this.limitRedisTemplate = limitRedisTemplate;
}
@Pointcut("@annotation(com.example.loginlimit.annotation.LimitCount)")
public void pointcut() {}
@Around("pointcut()")
public Object around(ProceedingJoinPoint point) throws Throwable {
HttpServletRequest request = ((ServletRequestAttributes)Objects.requireNonNull(RequestContextHolder.getRequestAttributes())).getRequest();
Method method = ((MethodSignature)point.getSignature()).getMethod();
LimitCount ann = method.getAnnotation(LimitCount.class);
String ip = IPUtil.getIpAddr(request);
List
keys = ImmutableList.of(ann.prefix() + "_" + ann.key() + "_" + ip);
String script = buildLuaScript();
RedisScript
redisScript = new DefaultRedisScript<>(script, Number.class);
Number cnt = limitRedisTemplate.execute(redisScript, keys, ann.count(), ann.period());
if (cnt != null && cnt.intValue() <= ann.count()) {
return point.proceed();
}
return "接口访问超出频率限制";
}
private String buildLuaScript() {
return "local c\n" +
"c = redis.call('get',KEYS[1])\n" +
"if c and tonumber(c) > tonumber(ARGV[1]) then return c end\n" +
"c = redis.call('incr',KEYS[1])\n" +
"if tonumber(c) == 1 then redis.call('expire',KEYS[1],ARGV[2]) end\n" +
"return c";
}
}Running the application starts a Spring Boot service that enforces a three‑attempt limit per IP within a configurable time window, providing a simple yet effective defense against brute‑force login attacks.
Top Architect
Top Architect focuses on sharing practical architecture knowledge, covering enterprise, system, website, large‑scale distributed, and high‑availability architectures, plus architecture adjustments using internet technologies. We welcome idea‑driven, sharing‑oriented architects to exchange and learn 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.