SpringBoot 防止接口惡意多次請求的操作
前言
剛寫代碼不就,還不能做深層次安全措施,今天研究了一下基本的防止接口多次惡意請求的方法。
思路
1:設(shè)置同一IP,一個時(shí)間段內(nèi)允許訪問的最大次數(shù)
2:記錄所有IP單位時(shí)間內(nèi)訪問的次數(shù)
3:將所有被限制IP存到存儲器
4:通過IP過濾訪問請求
該demo只有后臺Java代碼,沒有前端
代碼
首先是獲取IP的工具類
public class Ipsettings {
public static String getRemoteHost(HttpServletRequest request) {
String ipAddress = null;
//ipAddress = request.getRemoteAddr();
ipAddress = request.getHeader("x-forwarded-for");
if(ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) {
ipAddress = request.getHeader("Proxy-Client-IP");
}
if(ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) {
ipAddress = request.getHeader("WL-Proxy-Client-IP");
}
if(ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) {
ipAddress = request.getRemoteAddr();
if(ipAddress.equals("127.0.0.1")){
//根據(jù)網(wǎng)卡取本機(jī)配置的IP
InetAddress inet=null;
try {
inet = InetAddress.getLocalHost();
} catch (UnknownHostException e) {
e.printStackTrace();
}
ipAddress= inet.getHostAddress();
}
}
//對于通過多個代理的情況,第一個IP為客戶端真實(shí)IP,多個IP按照','分割
if(ipAddress!=null && ipAddress.length()>15){ //"***.***.***.***".length() = 15
if(ipAddress.indexOf(",")>0){
ipAddress = ipAddress.substring(0,ipAddress.indexOf(","));
}
}
return ipAddress;
}
}
其次是監(jiān)聽器以及IP存儲器
import java.util.HashMap;
import java.util.Map;
import javax.servlet.ServletContext;
import javax.servlet.ServletContextEvent;
import javax.servlet.ServletContextListener;
import javax.servlet.annotation.WebListener;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@WebListener
public class MyApplicationListener implements ServletContextListener {
private Logger logger = LoggerFactory.getLogger(MyApplicationListener.class);
@Override
public void contextInitialized(ServletContextEvent sce) {
logger.info("liting: contextInitialized");
System.err.println("初始化成功");
ServletContext context = sce.getServletContext();
// IP存儲器
Map<String, Long[]> ipMap = new HashMap<String, Long[]>();
context.setAttribute("ipMap", ipMap);
// 限制IP存儲器:存儲被限制的IP信息
Map<String, Long> limitedIpMap = new HashMap<String, Long>();
context.setAttribute("limitedIpMap", limitedIpMap);
logger.info("ipmap:"+ipMap.toString()+";limitedIpMap:"+limitedIpMap.toString()+"初始化成功。。。。。");
}
@Override
public void contextDestroyed(ServletContextEvent sce) {
// TODO Auto-generated method stub
}
}
最后是具體規(guī)則設(shè)置
import java.io.IOException;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.annotation.WebFilter;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
@WebFilter(urlPatterns="/*")
public class IpFilter implements Filter{
/**
* 默認(rèn)限制時(shí)間(單位:ms)
*/
private static final long LIMITED_TIME_MILLIS = 5 * 2 * 1000;
/**
* 用戶連續(xù)訪問最高閥值,超過該值則認(rèn)定為惡意操作的IP,進(jìn)行限制
*/
private static final int LIMIT_NUMBER = 2;
/**
* 用戶訪問最小安全時(shí)間,在該時(shí)間內(nèi)如果訪問次數(shù)大于閥值,則記錄為惡意IP,否則視為正常訪問
*/
private static final int MIN_SAFE_TIME = 5000;
private FilterConfig config;
@Override
public void init(FilterConfig filterConfig) throws ServletException {
this.config = filterConfig; //設(shè)置屬性filterConfig
}
/* (non-Javadoc)
* @see javax.servlet.Filter#doFilter(javax.servlet.ServletRequest, javax.servlet.ServletResponse, javax.servlet.FilterChain)
*/
@SuppressWarnings("unchecked")
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) servletRequest;
HttpServletResponse response = (HttpServletResponse) servletResponse;
ServletContext context = config.getServletContext();
// 獲取限制IP存儲器:存儲被限制的IP信息
Map<String, Long> limitedIpMap = (Map<String, Long>) context.getAttribute("limitedIpMap");
// 過濾受限的IP
filterLimitedIpMap(limitedIpMap);
// 獲取用戶IP
String ip = Ipsettings.getRemoteHost(request);
System.err.println("ip:"+ip);
//以下是處理限制IP的規(guī)則,可以自己寫
// 判斷是否是被限制的IP,如果是則跳到異常頁面
if (isLimitedIP(limitedIpMap, ip)) {
long limitedTime = limitedIpMap.get(ip) - System.currentTimeMillis();
// 剩余限制時(shí)間(用為從毫秒到秒轉(zhuǎn)化的一定會存在些許誤差,但基本可以忽略不計(jì))
request.setAttribute("remainingTime", ((limitedTime / 1000) + (limitedTime % 1000 > 0 ? 1 : 0)));
//request.getRequestDispatcher("/error/overLimitIP").forward(request, response);
System.err.println("ip訪問過于頻繁:"+ip);
return;
}
// 獲取IP存儲器
Map<String, Long[]> ipMap = (Map<String, Long[]>) context.getAttribute("ipMap");
// 判斷存儲器中是否存在當(dāng)前IP,如果沒有則為初次訪問,初始化該ip
// 如果存在當(dāng)前ip,則驗(yàn)證當(dāng)前ip的訪問次數(shù)
// 如果大于限制閥值,判斷達(dá)到閥值的時(shí)間,如果不大于[用戶訪問最小安全時(shí)間]則視為惡意訪問,跳轉(zhuǎn)到異常頁面
if (ipMap.containsKey(ip)) {
Long[] ipInfo = ipMap.get(ip);
ipInfo[0] = ipInfo[0] + 1;
System.out.println("當(dāng)前第[" + (ipInfo[0]) + "]次訪問");
if (ipInfo[0] > LIMIT_NUMBER) {
Long ipAccessTime = ipInfo[1];
Long currentTimeMillis = System.currentTimeMillis();
if (currentTimeMillis - ipAccessTime <= MIN_SAFE_TIME) {
limitedIpMap.put(ip, currentTimeMillis + LIMITED_TIME_MILLIS);
request.setAttribute("remainingTime", LIMITED_TIME_MILLIS);
System.err.println("ip訪問過于頻繁:"+ip);
request.getRequestDispatcher("/error/overLimitIP").forward(request, response);
return;
} else {
initIpVisitsNumber(ipMap, ip);
}
}
} else {
initIpVisitsNumber(ipMap, ip);
System.out.println("您首次訪問該網(wǎng)站");
}
context.setAttribute("ipMap", ipMap);
chain.doFilter(request, response);
}
@Override
public void destroy() {
// TODO Auto-generated method stub
}
/**
* @Description 過濾受限的IP,剔除已經(jīng)到期的限制IP
* @param limitedIpMap
*/
private void filterLimitedIpMap(Map<String, Long> limitedIpMap) {
if (limitedIpMap == null) {
return;
}
Set<String> keys = limitedIpMap.keySet();
Iterator<String> keyIt = keys.iterator();
long currentTimeMillis = System.currentTimeMillis();
while (keyIt.hasNext()) {
long expireTimeMillis = limitedIpMap.get(keyIt.next());
if (expireTimeMillis <= currentTimeMillis) {
keyIt.remove();
}
}
}
/**
* @Description 是否是被限制的IP
* @param limitedIpMap
* @param ip
* @return true : 被限制 | false : 正常
*/
private boolean isLimitedIP(Map<String, Long> limitedIpMap, String ip) {
if (limitedIpMap == null || ip == null) {
// 沒有被限制
return false;
}
Set<String> keys = limitedIpMap.keySet();
Iterator<String> keyIt = keys.iterator();
while (keyIt.hasNext()) {
String key = keyIt.next();
if (key.equals(ip)) {
// 被限制的IP
return true;
}
}
return false;
}
/**
* 初始化用戶訪問次數(shù)和訪問時(shí)間
*
* @param ipMap
* @param ip
*/
private void initIpVisitsNumber(Map<String, Long[]> ipMap, String ip) {
Long[] ipInfo = new Long[2];
ipInfo[0] = 0L;// 訪問次數(shù)
ipInfo[1] = System.currentTimeMillis();// 初次訪問時(shí)間
ipMap.put(ip, ipInfo);
}
}
然后再在啟動類上加上注解掃描配置包
@ServletComponentScan(basePackages="掃描剛才的MyApplicationListener")
補(bǔ)充:springboot和redis控制單位時(shí)間內(nèi)同個ip訪問同個接口的次數(shù)
注:本文中的修改于網(wǎng)上一個錯誤的例子,不知道為什么一個錯誤的例子還被人瘋狂轉(zhuǎn)載,還都標(biāo)著原創(chuàng)。。。具體是那個這里就不指出了!
第一步:自定義一個注解
注:其實(shí)完全沒必要(這樣做的唯一好處就是每個接口與的訪問限制次數(shù)都可以不一樣)。。但是注解這個東西自從培訓(xùn)結(jié)束后沒有在用到過,決定還是再復(fù)習(xí)下
package com.mzd.redis_springboot_mybatis_mysql.limit;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import java.lang.annotation.*;
/**
* @Retention:注解的保留位置
* @Retention(RetentionPolicy.SOURCE) //注解僅存在于源碼中,在class字節(jié)碼文件中不包含
* @Retention(RetentionPolicy.CLASS) // 默認(rèn)的保留策略,注解會在class字節(jié)碼文件中存在,但運(yùn)行時(shí)無法獲得,
* @Retention(RetentionPolicy.RUNTIME) // 注解會在class字節(jié)碼文件中存在,在運(yùn)行時(shí)可以通過反射獲取到
*/
@Retention(RetentionPolicy.RUNTIME)
/**
* @Target:注解的作用目標(biāo)
* @Target(ElementType.TYPE) //接口、類、枚舉、注解
* @Target(ElementType.FIELD) //字段、枚舉的常量
* @Target(ElementType.METHOD) //方法
* @Target(ElementType.PARAMETER) //方法參數(shù)
* @Target(ElementType.CONSTRUCTOR) //構(gòu)造函數(shù)
* @Target(ElementType.LOCAL_VARIABLE) //局部變量
* @Target(ElementType.ANNOTATION_TYPE) //注解
* @Target(ElementType.PACKAGE) ///包
*/
@Target(ElementType.METHOD)
/**
* @Document 說明該注解將被包含在javadoc中
*/
@Documented
/**
* Ordered接口是由spring提供的,為了解決相同接口實(shí)現(xiàn)類的優(yōu)先級問題
*/
//最高優(yōu)先級- - - 個人覺得這個在這里沒必要加
//@order,使用注解方式使類的加載順序得到控制
@Order(Ordered.HIGHEST_PRECEDENCE)
public @interface RequestTimes {
//單位時(shí)間允許訪問次數(shù) - - -默認(rèn)值是2
int count() default 2;
//設(shè)置單位時(shí)間為1分鐘 - - - 默認(rèn)值是1分鐘
long time() default 60 * 1000;
}
Ordered:
1、接口內(nèi)容:我們可以打開這個接口查看它的源碼

我們可以看到這個接口中只有一個方法兩個屬性,一個是int的最小值,另一個是int的最大值
2、OrderComparator接口: PriorityOrdered是個接口,是Ordered接口的子類,并沒有實(shí)現(xiàn)任何方法

這個Comparator方法的邏輯大致是:
PriorityOrdered的優(yōu)先級高于Ordered
如果兩個都是Ordered或者PriorityOrdered就比較他們的order值,order值越大,優(yōu)先級越小
第二步:定義一個aop
package com.mzd.redis_springboot_mybatis_mysql.limit;
import com.mzd.redis_springboot_mybatis_mysql.bean.generator.Student;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.servlet.http.HttpServletRequest;
import java.util.concurrent.TimeUnit;
//使用@Aspect注解將一個java類定義為切面類
@Aspect
@Component
public class RequestTimesAop {
@Autowired
private RedisTemplate<String, String> redisTemplate;
//切面范圍
@Pointcut("execution(public * com.mzd.redis_springboot_mybatis_mysql.controller.*.*(..))")
public void WebPointCut() {
}
@Before("WebPointCut() && @annotation(times)")
/**
* JoinPoint對象封裝了SpringAop中切面方法的信息,在切面方法中添加JoinPoint參數(shù),就可以獲取到封裝了該方法信息的JoinPoint對象.
*/
public void ifovertimes(final JoinPoint joinPoint, RequestTimes times) {
try {
//java.lang.Object[] getArgs():獲取連接點(diǎn)方法運(yùn)行時(shí)的入?yún)⒘斜恚?
//Signature getSignature() :獲取連接點(diǎn)的方法簽名對象;
//java.lang.Object getTarget() :獲取連接點(diǎn)所在的目標(biāo)對象;
//java.lang.Object getThis() :獲取代理對象本身;
//####################################################################
/**
* 比如:獲取連接點(diǎn)方法運(yùn)行時(shí)的入?yún)⒘斜?
* 不足:如果連接點(diǎn)方法中沒有request參數(shù)的話,就沒法獲取request,如果不做處理的話,會報(bào)空指針異常的
* 但是所有請求怎么可能沒有request
*/
// Object[] objects = joinPoint.getArgs();
// HttpServletRequest request = null;
// for (int i = 0; i < objects.length; i++) {
// if (objects[i] instanceof HttpServletRequest) {
// request = (HttpServletRequest) objects[i];
// break;
// }
// }
//####################################################################
/**
* 另一種獲取request
*/
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = attributes.getRequest();
String ip = request.getRemoteAddr();
String url = request.getRequestURL().toString();
String key = "ifovertimes".concat(url).concat(ip);
//訪問次數(shù)加一
long count = redisTemplate.opsForValue().increment(key, 1);
//如果是第一次,則設(shè)置過期時(shí)間
if (count == 1) {
redisTemplate.expire(key, times.time(), TimeUnit.MILLISECONDS);
}
if (count > times.count()) {
request.setAttribute("ifovertimes", "true");
} else {
request.setAttribute("ifovertimes", "false");
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
提問:就是在aop方法中返回的值在controller層值如何才能獲得,比如:ifovertimes這個方法返回的String類型的值,那我在controller層如何獲得這個值。我現(xiàn)在是將這個值放在了request域里面,不知道有沒有別的更好的值。。。求大神幫助啊。。。
第三步:寫一個測試接口
@RequestTimes(count = 3, time = 60000)
@RequestMapping("hello.do")
public String hello(String username, HttpServletRequest request) {
System.out.println(request.getAttribute("ifovertimes"));
if (request.getAttribute("ifovertimes").equals("false")) {
System.out.println(username);
return "hello redis_springboot_mybatis_mysql";
}
return "HTTP請求超出設(shè)定的限制";
}
總結(jié):
這是一個完全可以跑的例子,當(dāng)然,springboot集成redis這里就不講了。。。
以上為個人經(jīng)驗(yàn),希望能給大家一個參考,也希望大家多多支持腳本之家。如有錯誤或未考慮完全的地方,望不吝賜教。
相關(guān)文章
解決IDEA導(dǎo)入javaWeb項(xiàng)目注解爆紅的問題
這篇文章主要介紹了解決IDEA導(dǎo)入javaWeb項(xiàng)目注解爆紅的問題,本文通過圖文并茂的形式給大家介紹的非常詳細(xì),對大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2020-10-10
IDEA運(yùn)行java程序時(shí)總彈出提示配置Edit Configurations問題
更新IDEA后,運(yùn)行Java程序時(shí)可能需要手動配置JDK版本,通過統(tǒng)一設(shè)置默認(rèn)JDK可以解決每次彈出EditConfigurations的問題,此操作可以簡化開發(fā)流程,提高效率2024-09-09
分析Java中ArrayList與LinkedList列表結(jié)構(gòu)的源碼
這篇文章主要介紹了Java中ArrayList與LinkedList列表結(jié)構(gòu)的源碼,文章最后對LinkedList和ArrayList以及Vector的特性有一個對比總結(jié),需要的朋友可以參考下2016-05-05
SpringSecurity中@PermitAll與@PreAuthorize的實(shí)現(xiàn)
@PermitAll和@PreAuthorize都是處理安全性的強(qiáng)大工具,本文主要介紹了SpringSecurity中@PermitAll與@PreAuthorize的實(shí)現(xiàn),具有一定的參考價(jià)值,感興趣的可以了解一下2024-07-07

