feat: 增加ip跳动检测
This commit is contained in:
parent
fde0d80a16
commit
3386e1f6a4
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -84,3 +84,8 @@ sqx:
|
|||
header: token
|
||||
uni:
|
||||
adSecret: 122e4ff1edc66dcf8761f7f7ffc81e0f8773cbfafb58aed29c72fbd092c77315
|
||||
|
||||
|
||||
limit:
|
||||
urlRate: 10 # 同一用户单url每秒限制次数
|
||||
ipJumpLimit: 1 # 同一ip每分钟跳动次数
|
||||
|
|
|
|||
Loading…
Reference in New Issue