From 3ab1165e69c75c82fe6e9daa149fc57d26261038 Mon Sep 17 00:00:00 2001 From: huangping Date: Mon, 25 May 2026 01:33:11 +0800 Subject: [PATCH] feat(m11): add login failure lockout after 5 failed attempts in 15 min --- .../platform/api/auth/AuthController.java | 39 ++++++++++- .../auth/PlatformLoginAttempt.java | 65 +++++++++++++++++++ .../auth/PlatformLoginAttemptMapper.java | 7 ++ .../db/migration/V12__login_attempts.sql | 10 +++ 4 files changed, 120 insertions(+), 1 deletion(-) create mode 100644 services/delivery-platform-api/src/main/java/cn/craftlabs/platform/api/persistence/auth/PlatformLoginAttempt.java create mode 100644 services/delivery-platform-api/src/main/java/cn/craftlabs/platform/api/persistence/auth/PlatformLoginAttemptMapper.java create mode 100644 services/delivery-platform-api/src/main/resources/db/migration/V12__login_attempts.sql diff --git a/services/delivery-platform-api/src/main/java/cn/craftlabs/platform/api/auth/AuthController.java b/services/delivery-platform-api/src/main/java/cn/craftlabs/platform/api/auth/AuthController.java index bfb95f9..1f48235 100644 --- a/services/delivery-platform-api/src/main/java/cn/craftlabs/platform/api/auth/AuthController.java +++ b/services/delivery-platform-api/src/main/java/cn/craftlabs/platform/api/auth/AuthController.java @@ -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 login(@RequestBody Map body) { String user = body.getOrDefault("username", ""); String pass = body.getOrDefault("password", ""); + + LambdaQueryWrapper 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"); } } diff --git a/services/delivery-platform-api/src/main/java/cn/craftlabs/platform/api/persistence/auth/PlatformLoginAttempt.java b/services/delivery-platform-api/src/main/java/cn/craftlabs/platform/api/persistence/auth/PlatformLoginAttempt.java new file mode 100644 index 0000000..8565b73 --- /dev/null +++ b/services/delivery-platform-api/src/main/java/cn/craftlabs/platform/api/persistence/auth/PlatformLoginAttempt.java @@ -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; + } +} diff --git a/services/delivery-platform-api/src/main/java/cn/craftlabs/platform/api/persistence/auth/PlatformLoginAttemptMapper.java b/services/delivery-platform-api/src/main/java/cn/craftlabs/platform/api/persistence/auth/PlatformLoginAttemptMapper.java new file mode 100644 index 0000000..9fe9fd6 --- /dev/null +++ b/services/delivery-platform-api/src/main/java/cn/craftlabs/platform/api/persistence/auth/PlatformLoginAttemptMapper.java @@ -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 {} diff --git a/services/delivery-platform-api/src/main/resources/db/migration/V12__login_attempts.sql b/services/delivery-platform-api/src/main/resources/db/migration/V12__login_attempts.sql new file mode 100644 index 0000000..cc230f1 --- /dev/null +++ b/services/delivery-platform-api/src/main/resources/db/migration/V12__login_attempts.sql @@ -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);