From f1bdb285c951f7aef6f1f217773afb7737779435 Mon Sep 17 00:00:00 2001
From: puzhibing <393733352@qq.com>
Date: 星期三, 26 八月 2026 09:03:30 +0800
Subject: [PATCH] 增加管理后台登录频次校验和连续错误登录后限制登录 修改管理后台验证码生成和登录校验

---
 .gitignore                                                                                      |    3 
 ruoyi-auth/src/main/java/com/ruoyi/auth/form/LoginBody.java                                     |   30 +++++++
 ruoyi-modules/ruoyi-system/src/main/java/com/ruoyi/system/controller/SysUserController.java     |    2 
 ruoyi-gateway/src/main/java/com/ruoyi/gateway/filter/ValidateCodeFilter.java                    |   86 +++++++++++----------
 ruoyi-auth/src/main/java/com/ruoyi/auth/service/SysLoginService.java                            |    8 +
 ruoyi-auth/src/main/java/com/ruoyi/auth/service/SysPasswordService.java                         |   13 ++-
 ruoyi-gateway/src/main/java/com/ruoyi/gateway/config/RouterFunctionConfiguration.java           |    3 
 ruoyi-common/ruoyi-common-core/src/main/java/com/ruoyi/common/core/constant/CacheConstants.java |   19 ++++
 ruoyi-common/ruoyi-common-redis/src/main/java/com/ruoyi/common/redis/service/RedisService.java  |   20 +++++
 9 files changed, 130 insertions(+), 54 deletions(-)

diff --git a/.gitignore b/.gitignore
index 09e64c6..ac69452 100644
--- a/.gitignore
+++ b/.gitignore
@@ -17,4 +17,5 @@
 
 *.yaml
 *.yml
-*.log
\ No newline at end of file
+*.log
+*.codebuddy
\ No newline at end of file
diff --git a/ruoyi-auth/src/main/java/com/ruoyi/auth/form/LoginBody.java b/ruoyi-auth/src/main/java/com/ruoyi/auth/form/LoginBody.java
index b12fb31..0967989 100644
--- a/ruoyi-auth/src/main/java/com/ruoyi/auth/form/LoginBody.java
+++ b/ruoyi-auth/src/main/java/com/ruoyi/auth/form/LoginBody.java
@@ -17,6 +17,16 @@
      */
     private String password;
 
+    /**
+     * 验证码
+     */
+    private String code;
+
+    /**
+     * 验证码唯一标识
+     */
+    private String uuid;
+
     public String getUsername()
     {
         return username;
@@ -36,4 +46,24 @@
     {
         this.password = password;
     }
+
+    public String getCode()
+    {
+        return code;
+    }
+
+    public void setCode(String code)
+    {
+        this.code = code;
+    }
+
+    public String getUuid()
+    {
+        return uuid;
+    }
+
+    public void setUuid(String uuid)
+    {
+        this.uuid = uuid;
+    }
 }
diff --git a/ruoyi-auth/src/main/java/com/ruoyi/auth/service/SysLoginService.java b/ruoyi-auth/src/main/java/com/ruoyi/auth/service/SysLoginService.java
index bc201c9..94db0b3 100644
--- a/ruoyi-auth/src/main/java/com/ruoyi/auth/service/SysLoginService.java
+++ b/ruoyi-auth/src/main/java/com/ruoyi/auth/service/SysLoginService.java
@@ -39,18 +39,22 @@
 	
 	@Autowired
 	private RedisService redisService;
+
+	@Autowired
+	private LoginRateLimitService loginRateLimitService;
 	
 	/**
 	 * 登录
 	 */
 	public LoginUser login(String username, String password, HttpServletRequest request) {
-		username = username.trim();
+		loginRateLimitService.checkLoginRate(username, request);
+		username = StringUtils.trim(username);
 		// 查询用户信息
 		R<LoginUser> userResult = remoteUserService.getUserInfo(username, SecurityConstants.INNER);
 		
 		if (StringUtils.isNull(userResult) || StringUtils.isNull(userResult.getData())) {
 			recordLogService.recordLogininfor(request, null, username, Constants.LOGIN_FAIL_STATUS, "登录用户不存在");
-			throw new ServiceException("用户名/密码错误");
+			throw new ServiceException("用户名或密码错误");
 		}
 		LoginUser userInfo = userResult.getData();
 		SysUser user = userResult.getData().getSysUser();
diff --git a/ruoyi-auth/src/main/java/com/ruoyi/auth/service/SysPasswordService.java b/ruoyi-auth/src/main/java/com/ruoyi/auth/service/SysPasswordService.java
index bb85a0f..f746379 100644
--- a/ruoyi-auth/src/main/java/com/ruoyi/auth/service/SysPasswordService.java
+++ b/ruoyi-auth/src/main/java/com/ruoyi/auth/service/SysPasswordService.java
@@ -61,10 +61,15 @@
 
         if (!matches(user, password))
         {
-            retryCount = retryCount + 1;
-            recordLogService.recordLogininfor(request, user.getUserId().intValue(), username, Constants.LOGIN_FAIL_STATUS, String.format("密码输入错误%s次", retryCount));
-            redisService.setCacheObject(getCacheKey(username), retryCount, lockTime, TimeUnit.MINUTES);
-            throw new ServiceException("用户名/密码错误");
+            retryCount = (int) redisService.incrementCacheValue(getCacheKey(username), lockTime, TimeUnit.MINUTES);
+            redisService.expire(getCacheKey(username), lockTime, TimeUnit.MINUTES);
+            String errMsg = String.format("密码输入错误%s次", retryCount);
+            recordLogService.recordLogininfor(request, user.getUserId().intValue(), username, Constants.LOGIN_FAIL_STATUS, errMsg);
+            if (retryCount >= maxRetryCount)
+            {
+                throw new ServiceException(String.format("密码输入错误%s次,帐户锁定%s分钟", maxRetryCount, lockTime));
+            }
+            throw new ServiceException("用户名或密码错误");
         }
         else
         {
diff --git a/ruoyi-common/ruoyi-common-core/src/main/java/com/ruoyi/common/core/constant/CacheConstants.java b/ruoyi-common/ruoyi-common-core/src/main/java/com/ruoyi/common/core/constant/CacheConstants.java
index 72bde3b..516d612 100644
--- a/ruoyi-common/ruoyi-common-core/src/main/java/com/ruoyi/common/core/constant/CacheConstants.java
+++ b/ruoyi-common/ruoyi-common-core/src/main/java/com/ruoyi/common/core/constant/CacheConstants.java
@@ -24,9 +24,24 @@
     public final static int PASSWORD_MAX_RETRY_COUNT = 5;
 
     /**
-     * 密码锁定时间,默认10(分钟)
+     * 密码锁定时间,默认15(分钟)
      */
-    public final static long PASSWORD_LOCK_TIME = 10;
+    public final static long PASSWORD_LOCK_TIME = 15;
+
+    /**
+     * 登录限流缓存前缀
+     */
+    public static final String LOGIN_RATE_LIMIT_KEY = "login_rate_limit:";
+
+    /**
+     * 登录限流窗口,默认60(秒)
+     */
+    public static final long LOGIN_RATE_LIMIT_WINDOW = 60;
+
+    /**
+     * 单个维度在限流窗口内允许的最大登录次数
+     */
+    public static final int LOGIN_RATE_LIMIT_MAX_COUNT = 5;
 
     /**
      * 权限缓存前缀
diff --git a/ruoyi-common/ruoyi-common-redis/src/main/java/com/ruoyi/common/redis/service/RedisService.java b/ruoyi-common/ruoyi-common-redis/src/main/java/com/ruoyi/common/redis/service/RedisService.java
index 5d3ad86..4c02c90 100644
--- a/ruoyi-common/ruoyi-common-redis/src/main/java/com/ruoyi/common/redis/service/RedisService.java
+++ b/ruoyi-common/ruoyi-common-redis/src/main/java/com/ruoyi/common/redis/service/RedisService.java
@@ -1,6 +1,7 @@
 package com.ruoyi.common.redis.service;
 
 import java.util.Collection;
+import java.util.Collections;
 import java.util.Iterator;
 import java.util.List;
 import java.util.Map;
@@ -11,6 +12,7 @@
 import org.springframework.data.redis.core.HashOperations;
 import org.springframework.data.redis.core.RedisTemplate;
 import org.springframework.data.redis.core.ValueOperations;
+import org.springframework.data.redis.core.script.DefaultRedisScript;
 import org.springframework.stereotype.Component;
 
 /**
@@ -50,6 +52,24 @@
     }
 
     /**
+     * 缓存值原子递增,并在首次递增时设置有效时间
+     *
+     * @param key 缓存键值
+     * @param timeout 时间
+     * @param timeUnit 时间颗粒度
+     * @return 递增后的值
+     */
+    public long incrementCacheValue(final String key, final long timeout, final TimeUnit timeUnit)
+    {
+        String scriptText = "local count = redis.call('incr', KEYS[1]); "
+                + "if count == 1 then redis.call('pexpire', KEYS[1], ARGV[1]); end; return count;";
+        DefaultRedisScript<Long> script = new DefaultRedisScript<>(scriptText, Long.class);
+        long timeoutMillis = timeUnit.toMillis(timeout);
+        Long count = (Long) redisTemplate.execute(script, Collections.singletonList(key), timeoutMillis);
+        return count == null ? 0L : count;
+    }
+
+    /**
      * 设置有效时间
      *
      * @param key Redis键
diff --git a/ruoyi-gateway/src/main/java/com/ruoyi/gateway/config/RouterFunctionConfiguration.java b/ruoyi-gateway/src/main/java/com/ruoyi/gateway/config/RouterFunctionConfiguration.java
index 05bdc0b..8cdce62 100644
--- a/ruoyi-gateway/src/main/java/com/ruoyi/gateway/config/RouterFunctionConfiguration.java
+++ b/ruoyi-gateway/src/main/java/com/ruoyi/gateway/config/RouterFunctionConfiguration.java
@@ -6,7 +6,6 @@
 import org.springframework.http.HttpHeaders;
 import org.springframework.http.HttpMethod;
 import org.springframework.http.HttpStatus;
-import org.springframework.http.MediaType;
 import org.springframework.http.server.reactive.ServerHttpRequest;
 import org.springframework.http.server.reactive.ServerHttpResponse;
 import org.springframework.web.cors.reactive.CorsUtils;
@@ -43,7 +42,7 @@
 	@Bean
 	public RouterFunction routerFunction() {
 		return RouterFunctions.route(
-				RequestPredicates.GET("/code").and(RequestPredicates.accept(MediaType.TEXT_PLAIN)),
+				RequestPredicates.GET("/code"),
 				validateCodeHandler);
 	}
 
diff --git a/ruoyi-gateway/src/main/java/com/ruoyi/gateway/filter/ValidateCodeFilter.java b/ruoyi-gateway/src/main/java/com/ruoyi/gateway/filter/ValidateCodeFilter.java
index 02d7bb4..1a469d2 100644
--- a/ruoyi-gateway/src/main/java/com/ruoyi/gateway/filter/ValidateCodeFilter.java
+++ b/ruoyi-gateway/src/main/java/com/ruoyi/gateway/filter/ValidateCodeFilter.java
@@ -1,8 +1,6 @@
 package com.ruoyi.gateway.filter;
 
-import java.nio.CharBuffer;
 import java.nio.charset.StandardCharsets;
-import java.util.concurrent.atomic.AtomicReference;
 
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.cloud.gateway.filter.GatewayFilter;
@@ -10,13 +8,16 @@
 import org.springframework.core.io.buffer.DataBuffer;
 import org.springframework.core.io.buffer.DataBufferUtils;
 import org.springframework.http.server.reactive.ServerHttpRequest;
+import org.springframework.http.server.reactive.ServerHttpRequestDecorator;
 import org.springframework.stereotype.Component;
+
 import com.alibaba.fastjson2.JSON;
 import com.alibaba.fastjson2.JSONObject;
 import com.ruoyi.common.core.utils.ServletUtils;
 import com.ruoyi.common.core.utils.StringUtils;
 import com.ruoyi.gateway.config.properties.CaptchaProperties;
 import com.ruoyi.gateway.service.ValidateCodeService;
+
 import reactor.core.publisher.Flux;
 
 /**
@@ -26,44 +27,45 @@
  */
 @Component
 public class ValidateCodeFilter extends AbstractGatewayFilterFactory<Object> {
-	private final static String[] VALIDATE_URL = new String[]{"/auth/login", "/auth/register"};
-	private static final String CODE = "code";
-	private static final String UUID = "uuid";
-	@Autowired
-	private ValidateCodeService validateCodeService;
-	@Autowired
-	private CaptchaProperties captchaProperties;
-	
-	@Override
-	public GatewayFilter apply(Object config) {
-		return (exchange, chain) -> {
-			ServerHttpRequest request = exchange.getRequest();
-			
-			// 非登录/注册请求或验证码关闭,不处理
-			if (!StringUtils.containsAnyIgnoreCase(request.getURI().getPath(), VALIDATE_URL) || !captchaProperties.getEnabled()) {
-				return chain.filter(exchange);
-			}
-			
-			try {
-				String rspStr = resolveBodyFromRequest(request);
-				JSONObject obj = JSON.parseObject(rspStr);
-				validateCodeService.checkCaptcha(obj.getString(CODE), obj.getString(UUID));
-			} catch (Exception e) {
-				return ServletUtils.webFluxResponseWriter(exchange.getResponse(), e.getMessage());
-			}
-			return chain.filter(exchange);
-		};
-	}
-	
-	private String resolveBodyFromRequest(ServerHttpRequest serverHttpRequest) {
-		// 获取请求体
-		Flux<DataBuffer> body = serverHttpRequest.getBody();
-		AtomicReference<String> bodyRef = new AtomicReference<>();
-		body.subscribe(buffer -> {
-			CharBuffer charBuffer = StandardCharsets.UTF_8.decode(buffer.asByteBuffer());
-			DataBufferUtils.release(buffer);
-			bodyRef.set(charBuffer.toString());
-		});
-		return bodyRef.get();
-	}
+    private static final String[] VALIDATE_URL = new String[] { "/auth/login", "/auth/register" };
+    private static final String CODE = "code";
+    private static final String UUID = "uuid";
+
+    @Autowired
+    private ValidateCodeService validateCodeService;
+
+    @Autowired
+    private CaptchaProperties captchaProperties;
+
+    @Override
+    public GatewayFilter apply(Object config) {
+        return (exchange, chain) -> {
+            ServerHttpRequest request = exchange.getRequest();
+            if (!StringUtils.containsAnyIgnoreCase(request.getURI().getPath(), VALIDATE_URL)
+                    || !captchaProperties.getEnabled()) {
+                return chain.filter(exchange);
+            }
+
+            return DataBufferUtils.join(request.getBody()).flatMap(dataBuffer -> {
+                byte[] body = new byte[dataBuffer.readableByteCount()];
+                dataBuffer.read(body);
+                DataBufferUtils.release(dataBuffer);
+
+                try {
+                    JSONObject json = JSON.parseObject(new String(body, StandardCharsets.UTF_8));
+                    validateCodeService.checkCaptcha(json.getString(CODE), json.getString(UUID));
+                } catch (Exception e) {
+                    return ServletUtils.webFluxResponseWriter(exchange.getResponse(), e.getMessage());
+                }
+
+                ServerHttpRequest decoratedRequest = new ServerHttpRequestDecorator(request) {
+                    @Override
+                    public Flux<DataBuffer> getBody() {
+                        return Flux.just(exchange.getResponse().bufferFactory().wrap(body));
+                    }
+                };
+                return chain.filter(exchange.mutate().request(decoratedRequest).build());
+            });
+        };
+    }
 }
diff --git a/ruoyi-modules/ruoyi-system/src/main/java/com/ruoyi/system/controller/SysUserController.java b/ruoyi-modules/ruoyi-system/src/main/java/com/ruoyi/system/controller/SysUserController.java
index 849327f..4371be0 100644
--- a/ruoyi-modules/ruoyi-system/src/main/java/com/ruoyi/system/controller/SysUserController.java
+++ b/ruoyi-modules/ruoyi-system/src/main/java/com/ruoyi/system/controller/SysUserController.java
@@ -492,7 +492,7 @@
     public R<LoginUser> info(@PathVariable("username") String username) {
         SysUser sysUser = userService.selectUserByUserName(username);
         if (StringUtils.isNull(sysUser)) {
-            return R.fail("用户名/密码错误");
+            return R.fail("用户名或密码错误");
         }
         // 角色集合
         Set<String> roles = permissionService.getRolePermission(sysUser);

--
Gitblit v1.7.1