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 javax.servlet.http.HttpServletRequest; 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.core.utils.ip.IpUtils; import com.ruoyi.common.redis.service.RedisService; /** * 登录频率限制服务 */ @Component public class LoginRateLimitService { private static final String UNKNOWN = "unknown"; private static final String SESSION_HEADER = "X-Login-Session"; @Autowired private RedisService redisService; /** * 按 IP、账号、登录会话三个维度限制登录尝试频率。 */ public void checkLoginRate(String username, HttpServletRequest request) { String normalizedUsername = StringUtils.defaultString(username).trim().toLowerCase(Locale.ROOT); String ip = StringUtils.defaultIfEmpty(IpUtils.getIpAddr(request), UNKNOWN); String sessionId = resolveSessionId(request); checkDimension("ip", ip); checkDimension("account", normalizedUsername); checkDimension("session", sessionId); } 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 resolveSessionId(HttpServletRequest request) { String sessionId = request.getHeader(SESSION_HEADER); if (StringUtils.isNotEmpty(sessionId)) { return sessionId; } String userAgent = StringUtils.defaultString(request.getHeader("User-Agent")); return IpUtils.getIpAddr(request) + "|" + userAgent; } 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); } } }