feat(m11): add login failure lockout after 5 failed attempts in 15 min

This commit is contained in:
2026-05-25 01:33:11 +08:00
parent c2118b16aa
commit 3ab1165e69
4 changed files with 120 additions and 1 deletions
@@ -1,7 +1,11 @@
package cn.craftlabs.platform.api.auth;
import cn.craftlabs.platform.api.persistence.auth.PlatformLoginAttempt;
import cn.craftlabs.platform.api.persistence.auth.PlatformLoginAttemptMapper;
import cn.craftlabs.platform.api.security.JwtService;
import cn.craftlabs.platform.api.security.PlatformRoles;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
@@ -9,6 +13,7 @@ import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.server.ResponseStatusException;
import java.time.OffsetDateTime;
import java.util.List;
import java.util.Map;
@@ -20,15 +25,39 @@ import java.util.Map;
public class AuthController {
private final JwtService jwtService;
private final PlatformLoginAttemptMapper loginAttemptMapper;
private final HttpServletRequest request;
public AuthController(JwtService jwtService) {
public AuthController(JwtService jwtService, PlatformLoginAttemptMapper loginAttemptMapper, HttpServletRequest request) {
this.jwtService = jwtService;
this.loginAttemptMapper = loginAttemptMapper;
this.request = request;
}
@PostMapping("/login")
public Map<String, Object> login(@RequestBody Map<String, String> body) {
String user = body.getOrDefault("username", "");
String pass = body.getOrDefault("password", "");
LambdaQueryWrapper<PlatformLoginAttempt> recentQuery = new LambdaQueryWrapper<>();
recentQuery.eq(PlatformLoginAttempt::getUsername, user)
.eq(PlatformLoginAttempt::getSuccess, false)
.ge(PlatformLoginAttempt::getAttemptedAt, OffsetDateTime.now().minusMinutes(15));
long recentFailures = loginAttemptMapper.selectCount(recentQuery);
if (recentFailures >= 5) {
PlatformLoginAttempt attempt = new PlatformLoginAttempt();
attempt.setUsername(user);
attempt.setSuccess(false);
attempt.setIpAddress(request.getRemoteAddr());
attempt.setAttemptedAt(OffsetDateTime.now());
loginAttemptMapper.insert(attempt);
throw new ResponseStatusException(
HttpStatus.TOO_MANY_REQUESTS,
"账户已临时锁定,请 15 分钟后重试");
}
if ("admin".equals(user) && "admin".equals(pass)) {
String token =
jwtService.createToken(user, "管理员", List.of(PlatformRoles.SYS_ADMIN));
@@ -67,6 +96,14 @@ public class AuthController {
"displayName",
"运营账号");
}
PlatformLoginAttempt attempt = new PlatformLoginAttempt();
attempt.setUsername(user);
attempt.setSuccess(false);
attempt.setIpAddress(request.getRemoteAddr());
attempt.setAttemptedAt(OffsetDateTime.now());
loginAttemptMapper.insert(attempt);
throw new ResponseStatusException(HttpStatus.UNAUTHORIZED, "invalid credentials");
}
}
@@ -0,0 +1,65 @@
package cn.craftlabs.platform.api.persistence.auth;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import java.time.OffsetDateTime;
@TableName("platform_login_attempt")
public class PlatformLoginAttempt {
@TableId(type = IdType.AUTO)
private Long id;
private String username;
private Boolean success;
@TableField("ip_address")
private String ipAddress;
@TableField("attempted_at")
private OffsetDateTime attemptedAt;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public Boolean getSuccess() {
return success;
}
public void setSuccess(Boolean success) {
this.success = success;
}
public String getIpAddress() {
return ipAddress;
}
public void setIpAddress(String ipAddress) {
this.ipAddress = ipAddress;
}
public OffsetDateTime getAttemptedAt() {
return attemptedAt;
}
public void setAttemptedAt(OffsetDateTime attemptedAt) {
this.attemptedAt = attemptedAt;
}
}
@@ -0,0 +1,7 @@
package cn.craftlabs.platform.api.persistence.auth;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface PlatformLoginAttemptMapper extends BaseMapper<PlatformLoginAttempt> {}
@@ -0,0 +1,10 @@
CREATE TABLE platform_login_attempt (
id BIGSERIAL PRIMARY KEY,
username VARCHAR(256) NOT NULL,
success BOOLEAN NOT NULL,
ip_address VARCHAR(64),
attempted_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX idx_login_attempt_username ON platform_login_attempt(username);
CREATE INDEX idx_login_attempt_time ON platform_login_attempt(attempted_at);