feat: 增加ip跳动检测

This commit is contained in:
张松 2024-12-30 11:15:32 +08:00
parent fde0d80a16
commit 3386e1f6a4
5 changed files with 218 additions and 12 deletions

View File

@ -10,6 +10,8 @@ public class RedisKeys {
public static final String FREE_WATCH_KEY = "free:watch:";
public static final String LOCK_KEY = "SYS:LOCK:";
public static final String RATE_LIMIT = "RATE:z";
public static String getSysConfigKey(String key){
@ -41,4 +43,12 @@ public class RedisKeys {
}
return key.toString();
}
public static String getUserIpRateKey(long userId, String ip) {
return RATE_LIMIT + "user:" + userId + ":ip:" + ip;
}
public static String getUserUrlRateKey(long userId, String url) {
return RATE_LIMIT + "user:" + userId + ":url:" + url;
}
}

View File

@ -1,14 +1,20 @@
package com.sqx.modules.app.interceptor;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.sqx.common.exception.CzgException;
import com.sqx.common.exception.SqxException;
import com.sqx.common.utils.DateUtils;
import com.sqx.common.utils.IPUtils;
import com.sqx.modules.app.entity.UserEntity;
import com.sqx.modules.app.service.UserService;
import com.sqx.modules.app.utils.JwtUtils;
import com.sqx.modules.redisService.RedisService;
import io.jsonwebtoken.Claims;
import com.sqx.modules.app.annotation.Login;
import org.apache.commons.lang.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Component;
@ -24,13 +30,20 @@ import java.util.Date;
*/
@Component
public class AuthorizationInterceptor extends HandlerInterceptorAdapter {
private static final Logger log = LoggerFactory.getLogger(AuthorizationInterceptor.class);
@Autowired
private JwtUtils jwtUtils;
@Autowired
private UserService userService;
private final RedisService redisService;
public static final String USER_KEY = "userId";
public AuthorizationInterceptor(RedisService redisService) {
this.redisService = redisService;
}
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
Login annotation;
@ -44,13 +57,13 @@ public class AuthorizationInterceptor extends HandlerInterceptorAdapter {
return true;
}
//获取用户凭证
// 获取用户凭证
String token = request.getHeader(jwtUtils.getHeader());
if (StringUtils.isBlank(token)) {
token = request.getParameter(jwtUtils.getHeader());
}
//凭证为空
// 凭证为空
if (StringUtils.isBlank(token)) {
throw new SqxException(jwtUtils.getHeader() + "不能为空", HttpStatus.UNAUTHORIZED.value());
}
@ -60,18 +73,35 @@ public class AuthorizationInterceptor extends HandlerInterceptorAdapter {
throw new SqxException(jwtUtils.getHeader() + "失效,请重新登录", HttpStatus.UNAUTHORIZED.value());
}
//设置userId到request里后续根据userId获取用户信息
long userId = Long.parseLong(claims.getSubject());
String ip = IPUtils.getIpAddr(request); // 获取用户的 IP 地址
// 检查用户是否超过限流
if (redisService.checkIpJumpLimit(userId, ip)) {
log.warn("用户地址跳动频繁,封禁: {}", userId);
userService.update(null, new LambdaUpdateWrapper<UserEntity>()
.eq(UserEntity::getUserId, userId)
.set(UserEntity::getStatus, 0));
throw new CzgException("ip跳动过于频繁请联系管理员解封");
}
redisService.recordUrlVisitCountWithIp(userId, request.getRequestURI(), ip);
// 设置 userId request 后续根据 userId 获取用户信息
UserEntity user = userService.selectUserById(userId);
if (user.getStatus().equals(0)) {
return false;
throw new CzgException("异常行为用户: {}" + user.getUserId());
}
request.setAttribute(USER_KEY, userId);
//记录用户最后一次调用接口的时间
UserEntity userEntity = new UserEntity();
userEntity.setUserId(userId);
userEntity.setOnLineTime(DateUtils.format(new Date()));
userService.updateById(userEntity);
if (redisService.isRecordUserOnLineTime(userId)) {
// 记录用户最后一次调用接口的时间
UserEntity userEntity = new UserEntity();
userEntity.setUserId(userId);
userEntity.setOnLineTime(DateUtils.format(new Date()));
userService.updateById(userEntity);
}
return true;
}
}

View File

@ -14,4 +14,9 @@ public interface RedisService {
Boolean getFreeWatchTimeIsExpire(Long userId);
Long getFreeWatchRemainTime(Long userId, boolean isPermanently);
boolean checkIpJumpLimit(long userId, String ip);
void recordUrlVisitCountWithIp(long userId, String url, String ip);
boolean isRecordUserOnLineTime(long userId);
}

View File

@ -12,23 +12,36 @@ import com.sqx.modules.discSpinning.entity.DiscSpinningAmount;
import com.sqx.modules.discSpinning.service.DiscSpinningAmountService;
import com.sqx.modules.redisService.RedisService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Lazy;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.text.SimpleDateFormat;
import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
import java.util.Map;
@Service
public class RedisServiceImpl implements RedisService {
@Value("${limit.urlRate}")
private Integer urlLimitRate;
@Value("${limit.ipJumpLimit}")
private Integer ipJumpLimit;
@Lazy
@Autowired
private RedisUtils redisUtils;
@Autowired
private DiscSpinningAmountService amountService;
private final StringRedisTemplate redisTemplate;
public RedisServiceImpl(StringRedisTemplate redisTemplate) {
this.redisTemplate = redisTemplate;
}
@Override
public void setDiscSpinningAmounts(String key) {
@ -138,4 +151,147 @@ public class RedisServiceImpl implements RedisService {
return expireTime == -1 ? second : expireTime > DateUtil.current() ? expireTime - DateUtil.current() : 0L;
}
public boolean checkIpJumpLimit(long userId, String ip) {
String userKey = "user:" + userId + ":last_ip"; // 存储用户上次的 IP 地址
String lastIp = redisTemplate.opsForValue().get(userKey);
// 获取用户跳动的历史 IP 地址集合
String jumpHistoryKey = "user:" + userId + ":ip_history"; // 记录用户跳动过的 IP 地址
Set<String> jumpHistory = redisTemplate.opsForSet().members(jumpHistoryKey);
// 记录 IP 跳动次数
String ipTimestampKey = "user:" + userId + ":ip_timestamp"; // 记录每个 IP 跳动的过期时间
String userJumpCountKey = "user:" + userId + ":jump_count"; // 用户跳动的总次数
// 新增用于记录所有 IP 请求时间和次数的 Hash
String ipRequestsKey = "user:" + userId + ":ip_requests"; // 用来记录 IP 的请求次数和时间
// 如果用户之前没有记录过 IP说明是第一次请求直接保存
if (lastIp == null) {
redisTemplate.opsForValue().set(userKey, ip);
// 将当前 IP 添加到跳动历史中设置 60 秒过期时间
redisTemplate.opsForSet().add(jumpHistoryKey, ip);
redisTemplate.expire(jumpHistoryKey, 1, TimeUnit.MINUTES);
// 设置该 IP 的过期时间为 60 表示跳动记录会在 60 秒后过期
redisTemplate.opsForValue().set(ipTimestampKey + ":" + ip, "1", 1, TimeUnit.MINUTES);
// 记录用户 IP 请求的次数和时间
String ipData = DateUtil.date() + "|1"; // 时间和请求次数
redisTemplate.opsForHash().put(ipRequestsKey, ip, ipData);
return false;
}
// 如果当前 IP 和上次 IP 不一致说明发生了 IP 跳动
if (!lastIp.equals(ip)) {
// 判断当前 IP 是否在历史跳动的 IP
if (jumpHistory != null && jumpHistory.contains(ip)) {
// 如果当前 IP 存在于历史跳动记录中认为是多节点登录增加跳动次数
String s = redisTemplate.opsForValue().get(userJumpCountKey);
Integer currentJumpCount = s == null ? null : Integer.parseInt(s);
if (currentJumpCount == null) {
currentJumpCount = 0;
}
currentJumpCount++;
// 更新跳动次数
redisTemplate.opsForValue().set(userJumpCountKey, String.valueOf(currentJumpCount));
// 如果跳动次数超过限制触发封禁
if (currentJumpCount >= ipJumpLimit) {
return true;
}
}
// 检查用户是否跳动过多次
if (jumpHistory != null && jumpHistory.size() >= ipJumpLimit) {
// 如果历史 IP 地址数超过最大跳动次数触发封禁
return true;
}
// 将当前 IP 添加到跳动历史中设置 60 秒过期时间
redisTemplate.opsForSet().add(jumpHistoryKey, ip);
redisTemplate.expire(jumpHistoryKey, 1, TimeUnit.MINUTES);
// 更新用户的上次 IP 地址
redisTemplate.opsForValue().set(userKey, ip);
// 增加该 IP 的跳动计数并设置过期时间为 60
redisTemplate.opsForValue().increment(ipTimestampKey + ":" + ip, 1);
redisTemplate.expire(ipTimestampKey + ":" + ip, 1, TimeUnit.MINUTES);
// 记录该 IP 请求的次数和时间
String ipData = DateUtil.date() + "|1"; // 时间和请求次数
redisTemplate.opsForHash().put(ipRequestsKey, ip, ipData);
} else {
// 如果 IP 没有变化更新该 IP 的请求次数和时间
String currentCount = (String) redisTemplate.opsForHash().get(ipRequestsKey, ip);
String[] ipData = currentCount != null ? currentCount.split("\\|") : new String[]{"", "0"};
int newCount = Integer.parseInt(ipData[1]) + 1; // 增加请求次数
// 更新新的时间和请求次数
String updatedIpData = DateUtil.date() + "|" + newCount;
redisTemplate.opsForHash().put(ipRequestsKey, ip, updatedIpData);
}
return false;
}
// 增加跳动次数的记录
private void increaseJumpCount(long userId) {
String jumpCountKey = "user:" + userId + ":jump_count"; // 跳动次数
String jumpCount = redisTemplate.opsForValue().get(jumpCountKey);
if (jumpCount == null) {
redisTemplate.opsForValue().set(jumpCountKey, "1", 1, TimeUnit.MINUTES); // 初始跳动
} else {
int newJumpCount = Integer.parseInt(jumpCount) + 1;
redisTemplate.opsForValue().set(jumpCountKey, String.valueOf(newJumpCount), 1, TimeUnit.MINUTES);
}
}
// 记录用户访问 URL 的次数和 IP 地址
public void recordUrlVisitCountWithIp(long userId, String url, String ip) {
String userUrlKey = "user:" + userId + ":url:" + url; // 存储用户访问特定 URL 的数据
// 获取用户访问该 URL 的访问记录
Map<Object, Object> urlAccessInfo = redisTemplate.opsForHash().entries(userUrlKey);
// 记录访问次数的键名和每个 IP 地址的计数
String ipKey = "ip:" + ip;
int currentIpVisitCount = urlAccessInfo.containsKey(ipKey) ? Integer.parseInt(urlAccessInfo.get(ipKey).toString()) : 0;
// 更新该 IP 的访问计数
redisTemplate.opsForHash().put(userUrlKey, ipKey, String.valueOf(currentIpVisitCount + 1));
// 更新总的访问次数不区分 IP
int totalVisitCount = urlAccessInfo.containsKey("total") ? Integer.parseInt(urlAccessInfo.get("total").toString()) : 0;
redisTemplate.opsForHash().put(userUrlKey, "total", String.valueOf(totalVisitCount + 1));
}
@Override
public boolean isRecordUserOnLineTime(long userId) {
// Redis 键值为 "user:{userId}:last_call_time"存储的是用户最后一次调用接口的时间戳
String lastCallTimeKey = "user:" + userId + ":last_call_time";
// 获取 Redis 中存储的上次更新时间
String lastCallTimeStr = redisTemplate.opsForValue().get(lastCallTimeKey);
// 如果 Redis 中没有记录过表示这是用户第一次调用接口
if (lastCallTimeStr == null) {
// 更新 Redis 中记录的最后调用时间
redisTemplate.opsForValue().set(lastCallTimeKey, String.valueOf(DateUtil.date().getTime()), 10, TimeUnit.MINUTES);
return true;
} else {
return false;
}
}
}

View File

@ -84,3 +84,8 @@ sqx:
header: token
uni:
adSecret: 122e4ff1edc66dcf8761f7f7ffc81e0f8773cbfafb58aed29c72fbd092c77315
limit:
urlRate: 10 # 同一用户单url每秒限制次数
ipJumpLimit: 1 # 同一ip每分钟跳动次数