.gitignore
@@ -18,3 +18,4 @@ *.yaml *.yml *.log *.codebuddy 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; } } ruoyi-auth/src/main/java/com/ruoyi/auth/service/SysLoginService.java
@@ -40,17 +40,21 @@ @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(); 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 { 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; /** * 权限缓存前缀 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键 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); } 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,11 +27,13 @@ */ @Component public class ValidateCodeFilter extends AbstractGatewayFilterFactory<Object> { private final static String[] VALIDATE_URL = new String[]{"/auth/login", "/auth/register"}; 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; @@ -38,32 +41,31 @@ public GatewayFilter apply(Object config) { return (exchange, chain) -> { ServerHttpRequest request = exchange.getRequest(); // 非登录/注册请求或验证码关闭,不处理 if (!StringUtils.containsAnyIgnoreCase(request.getURI().getPath(), VALIDATE_URL) || !captchaProperties.getEnabled()) { 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 { String rspStr = resolveBodyFromRequest(request); JSONObject obj = JSON.parseObject(rspStr); validateCodeService.checkCaptcha(obj.getString(CODE), obj.getString(UUID)); 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()); } 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()); 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()); }); return bodyRef.get(); }; } } 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);