package com.ruoyi.auth.service; import java.nio.charset.StandardCharsets; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.util.Locale; import java.util.concurrent.TimeUnit; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import com.ruoyi.common.core.constant.CacheConstants; import com.ruoyi.common.core.exception.ServiceException; import com.ruoyi.common.core.utils.StringUtils; import com.ruoyi.common.redis.service.RedisService; /** * 登录频率限制服务(账号维度) */ @Component public class LoginRateLimitService { @Autowired private RedisService redisService; /** * 按账号维度限制登录尝试频率。 * IP、登录会话维度已由网关 LoginRateLimitFilter 在验证码校验之前处理 * (验证码错误的请求不会到达本服务,若在 auth 重复计数会与网关双重计数)。 */ public void checkLoginRate(String username) { String normalizedUsername = StringUtils.defaultString(username).trim().toLowerCase(Locale.ROOT); checkDimension("account", normalizedUsername); } /** * 清除账号维度的登录限流计数(登录成功后调用)。 */ public void clearAccountRecord(String username) { String normalizedUsername = StringUtils.defaultString(username).trim().toLowerCase(Locale.ROOT); String key = CacheConstants.LOGIN_RATE_LIMIT_KEY + "account:" + digest(normalizedUsername); if (redisService.hasKey(key)) { redisService.deleteObject(key); } } private void checkDimension(String dimension, String identity) { String key = CacheConstants.LOGIN_RATE_LIMIT_KEY + dimension + ":" + digest(identity); long count = redisService.incrementCacheValue(key, CacheConstants.LOGIN_RATE_LIMIT_WINDOW, TimeUnit.SECONDS); if (count > CacheConstants.LOGIN_RATE_LIMIT_MAX_COUNT) { long remainingSeconds = redisService.getExpire(key); long retryAfter = remainingSeconds > 0 ? remainingSeconds : CacheConstants.LOGIN_RATE_LIMIT_WINDOW; throw new ServiceException(String.format("登录尝试过于频繁,请%s秒后重试", retryAfter)); } } private String digest(String value) { try { byte[] bytes = MessageDigest.getInstance("SHA-256").digest(value.getBytes(StandardCharsets.UTF_8)); StringBuilder result = new StringBuilder(bytes.length * 2); for (byte current : bytes) { result.append(String.format("%02x", current)); } return result.toString(); } catch (NoSuchAlgorithmException e) { throw new IllegalStateException("SHA-256 algorithm is unavailable", e); } } }