| New file |
| | |
| | | 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); |
| | | } |
| | | } |
| | | } |