现在好多商城类项目对接口限流做了限制,一来为了更好的营造一个健康的绿色的网站,而来防止对网站进行破坏比如黑客写个脚本不停的对服务器进行请求。
话不多说本需求的原来是:#####对每个访问的请求,使用 前缀+请求地址+ip+系统时间,存入 redis 里面,然后 访问之前先去 进行 泛读取key,就是省去最后的系统时间+"*" 进行匹配,看 key 有多少个,如果超过了,注解限制的,就直接返回 请求失败!这里之所以要加上 系统时间,是因为 如果第一次存1,第二次+1 的话可能会 大量的 修改操作,为了方便,所以这样写的#####
①pom.xml配置文件
4.0.0
org.springframework.boot
spring-boot-starter-parent
2.3.3.RELEASE
com.hytc.mall
hytcmall
0.0.1-SNAPSHOT
hytcmall
弘毅天承商城
1.8
org.springframework.boot
spring-boot-starter-web
com.baomidou
dynamic-datasource-spring-boot-starter
2.5.4
com.baomidou
mybatis-plus-boot-starter
3.1.2
com.baomidou
mybatis-plus
3.3.2
com.baomidou
mybatis-plus-generator
3.3.2
org.freemarker
freemarker
mysql
mysql-connector-java
8.0.19
org.springframework.boot
spring-boot-starter-data-redis
redis.clients
jedis
3.2.0
com.alibaba
druid-spring-boot-starter
1.1.22
com.alibaba
fastjson
1.2.58
com.auth0
java-jwt
3.10.2
org.apache.commons
commons-lang3
3.10
cn.hutool
hutool-all
5.3.0
com.xiaoleilu
hutool-all
3.3.0
org.projectlombok
lombok
1.18.8
provided
log4j
log4j
1.2.17
io.springfox
springfox-swagger2
2.9.2
io.swagger
swagger-annotations
1.5.21
io.swagger
swagger-models
1.5.21
com.github.xiaoymin
swagger-bootstrap-ui
1.9.6
org.apache.poi
poi
3.9
org.apache.poi
poi-ooxml
3.7
org.apache.poi
poi-ooxml-schemas
3.9
org.springframework.boot
spring-boot-starter-test
test
org.junit.vintage
junit-vintage-engine
org.springframework.boot
spring-boot-maven-plugin
②application.yml
server:
port: 90
spring:
application:
name: hytc-crm-server
#缓存配置
cache:
type: redis
datasource:
type: com.alibaba.druid.pool.DruidDataSource
dynamic:
#设置默认的数据源或者数据源组,默认值即为master
primary: master
datasource:
#主库配置
master:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/hytcmall_20200923?serverTimezone=UTC&characterEncoding=utf8&useUnicode=true&useSSL=false
username: root
password: root
druid:
connectionProperties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=50000
filters: stat,slf4j
initial-size: 10
maxActive: 200
maxPoolPreparedStatementPerConnectionSize: 200
maxWait: 60000
min-idle: 5
minEvictableIdleTimeMillis: 180000
poolPreparedStatements: true
stat-view-servlet:
allow: 127.0.0.1,192.168.163.1
deny: 192.168.1.73
#Druid 管理密码
login-password: 123456
#Druid 管理账号
login-username: admin
reset-enable: false
url-pattern: /druid/*
testOnBorrow: false
testOnReturn: false
testWhileIdle: true
#配置一个连接在池中最小生存的时间,单位是毫秒
timeBetweenEvictionRunsMillis: 300000
validationQuery: SELECT 1 FROM DUAL
web-stat-filter:
enabled: true
exclusions: '*.js,*.gif,*.jpg,*.bmp,*.png,*.css,*.ico,/druid/*'
url-pattern: /*
remove-abandoned: true
remove-abandoned-timeout-millis: 300
log-abandoned: true
redis:
# Redis数据库索引(默认为0)
database: 15
# Redis服务器地址
host: 127.0.0.1
# Redis服务器连接端口
port: 6379
# Redis服务器连接密码(默认为空)
password:
# 连接池最大连接数(使用负值表示没有限制)
jedis:
pool:
## 连接池最大连接数(使用负值表示没有限制)
max-active: 20
# 连接池最大阻塞等待时间(使用负值表示没有限制
max-wait: -1
# 连接池中的最大空闲连接
max-idle: 10
# 连接池中的最小空闲连接
min-idle: 0
# 连接超时时间(毫秒)
timeout: 5000
#mybatis
mybatis:
mapper-locations: classpath:/mapper/*.xml
type-aliases-package: com.hytc.admin.vo
configuration:
map-underscore-to-camel-case: true
#log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
#mybatisPlus
mybatis-plus:
configuration:
# 原生配置
cache-enabled: false
call-setters-on-nulls: true
map-underscore-to-camel-case: true
global-config:
db-config:
column-underline: true
#字段策略 IGNORED:"忽略判断",NOT_NULL:"非 NULL 判断"),NOT_EMPTY:"非空判断"
field-strategy: not_empty
#数据库相关配置
#主键类型 AUTO:"数据库ID自增", INPUT:"用户输入ID",ID_WORKER:"全局唯一ID (数字类型唯一ID)", UUID:"全局唯一ID UUID";
id-type: AUTO
#数据库大写下划线转换
#capital-mode: true
#逻辑删除配置
logic-delete-value: 0
logic-not-delete-value: 1
#自定义填充策略接口实现
#metaObjectHandler: com.hytc.admin.config.MetaObjectHandlerConfig
#自定义SQL注入器
#sql-injector: com.baomidou.springboot.xxx
configuration:
map-underscore-to-camel-case: true
cache-enabled: false
#刷新mapper 调试神器
refresh: true
#mybatis plus mapper文件路径
mapperLocations: classpath:/mapper/*.xml
#mybaits plus 实体类路径
typeAliasesPackage: com.hytc.admin.vo
typeEnumsPackage: ''
# logging:
# config: classpath:log/logback.xml
# #com.hytc.mapper 该包打印DEBUG级别日志
# level:
# com:
# hytc:
# mapper: debug
# path: E:hytcbusslog
logging:
level:
com.hytc.mall.hytcmall.mapper: debug
③自定义注解
package com.hytc.mall.hytcmall.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.util.concurrent.TimeUnit;
/**
* 限制每个ip对每个方法的访问限制,加上时间限制
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface RequertIpLimit {
/**
* 时间类型,默认毫秒
* @return
*/
TimeUnit timeUnit() default TimeUnit.MILLISECONDS ;
/**
* 多长时间内限制,默认 60
* @return
*/
long t () default 60;
/**
* 单位时间内能访问多少次,默认10次
* @return
*/
int count () default 10;
}
④aop切面
package com.hytc.mall.hytcmall.aspect;
import com.hytc.mall.hytcmall.annotation.RequertIpLimit;
import com.hytc.mall.hytcmall.cache.RequestIpLImitCache;
import com.hytc.mall.hytcmall.exception.RequestLimitException;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.servlet.http.HttpServletRequest;
/**
* @ClassName: RequestIpLimitAspect
* @Description:https://blog.csdn.net/yali_aini/article/details/92653982
* https://juejin.im/post/6844903985388716045
* http://www.coder55.com/article/76465
* @Author: BYP <[email protected]>
* @Date: 2020/9/25 17:37
* @Copyright: 2019 www.tydic.com Inc. All rights reserved.
* 注意:本内容仅限于弘毅天承信息技术股份有限公司内部传阅,禁止外泄以及用于其他的商业目
*/
@Aspect//首先,这个@Aspect注释告诉Spring这是个切面类
@Component//然后@Compoment将转换成Spring容器中的bean或者是代理bean
@Order(1)
@Slf4j
public class RequestIpLimitAspect {
@Autowired
private RequestIpLImitCache requestIpLImitCache;
@Pointcut(value = "@annotation(com.hytc.mall.hytcmall.annotation.RequertIpLimit)" )
public void requestLimitPointCut(){};
@Around(value = "requestLimitPointCut())")
public Object requestLimitAround(ProceedingJoinPoint pjp) {
// 获取 request , 然后获取访问 ip
RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();//这个RequestContextHolder是Springmvc提供来获得请求的东西
HttpServletRequest request = ((ServletRequestAttributes) requestAttributes).getRequest();
String ip = request.getRemoteAddr();
String Ip = RequestIpLimitAspect.getRequestIp(request);
if(StringUtils.isEmpty(Ip)){
throw new RequestLimitException("300","非法访问!ip不能为空");
}
log.info("访问的ip地址为:{}", ip);
MethodSignature methodSignature = (MethodSignature)pjp.getSignature();
RequertIpLimit limit = methodSignature.getMethod().getAnnotation(RequertIpLimit.class);
// 限制访问次数
int count = limit.count();
RequestMapping methodAnnotation = methodSignature.getMethod().getAnnotation(RequestMapping.class);
String url = methodAnnotation.value()[0];
String key = "_" + url + "_" + Ip +"_";
if(requestIpLImitCache.count(key) > count ){
log.info("当前请求次数为:{},该次请求已经超过了规定时间范围内请求的最大次数", requestIpLImitCache.count(key));
throw new RequestLimitException("300","访问失败!超过访问限制!");
}
// 将访问存进缓存
requestIpLImitCache.add(key+System.currentTimeMillis(), "1", limit.timeUnit(), limit.t());
//记录开始时间
long startTime = System.currentTimeMillis();
//获取传入目标方法的参数
Object[] args = pjp.getArgs();
Object result = null;
try {
// 执行访问并返回数据
result = pjp.proceed(args);
return result;
} catch (Throwable throwable) {
throwable.printStackTrace();
return null;
} finally {
log.info("当前请求的Key是:{}", key);
//记录结束时间
long endTime = System.currentTimeMillis();
log.info("请求耗时为:{}", (endTime - startTime));
}
}
public static String getRequestIp(HttpServletRequest request){
// 获取请求IP
String ip = request.getHeader("x-forwarded-for");
if(ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip) || "null".equals(ip)){
ip = "" + request.getHeader("Proxy-Client-IP");
}
if(ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip) || "null".equals(ip)){
ip = "" + request.getHeader("WL-Proxy-Client-IP");
}
if(ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip) || "null".equals(ip)){
ip = "" + request.getRemoteAddr();
}
if("0.0.0.0".equals(ip) || "0.0.0.0.0.0.1".equals(ip) || "localhost".equals(ip) || "127.0.0.1".equals(ip) || "0:0:0:0:0:0:0:1".equals(ip)) {
ip = "127.0.0.1";
}
return ip;
}
}
⑤Redis工具类
package com.hytc.mall.hytcmall.config;
import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.jedis.JedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
/**
* @ClassName: RedisConfig
* @Description:https://www.cnblogs.com/lzhdonald/p/11560002.html
* @Author: BYP <[email protected]>
* @Date: 2020/9/24 10:40
* @Copyright: 2019 www.tydic.com Inc. All rights reserved.
* 注意:本内容仅限于弘毅天承信息技术股份有限公司内部传阅,禁止外泄以及用于其他的商业目
*/
@Configuration
@EnableCaching
public class RedisConfig {
//配置使用jedis作为redis的客户端
@Bean
public JedisConnectionFactory jedisConnectionFactory(){
return new JedisConnectionFactory();
}
@Bean
public RedisTemplate
package com.hytc.mall.hytcmall.cache;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import java.util.concurrent.TimeUnit;
/**
* @ClassName: RequestIpLImitCache
* @Description:
* @Author: BYP <[email protected]>
* @Date: 2020/9/25 17:48
* @Copyright: 2019 www.tydic.com Inc. All rights reserved.
* 注意:本内容仅限于弘毅天承信息技术股份有限公司内部传阅,禁止外泄以及用于其他的商业目
*/
@Component
public class RequestIpLImitCache {
private static final String PREFIX = "REQUEST_IP_LIMIT:";
@Autowired
private RedisTemplate redisTemplate;
public void add(String key , String value , TimeUnit timeUnit , long t){
redisTemplate.opsForValue().set(PREFIX + key, value, t, timeUnit);
}
public int count(String key ){
return redisTemplate.keys(PREFIX + key + "*").size();
}
}
⑦spring全局事务GlobalExceptionHandler
package com.hytc.mall.hytcmall.exception;
import com.google.common.collect.Maps;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import javax.servlet.http.HttpServletRequest;
import java.util.Map;
/**
* @ClassName: GlobalExceptionHandler
* @Description:全局异常处理
* @Author: BYP <[email protected]>
* @Date: 2020/9/27 10:03
* @Copyright: 2019 www.tydic.com Inc. All rights reserved.
* 注意:本内容仅限于弘毅天承信息技术股份有限公司内部传阅,禁止外泄以及用于其他的商业目
*/
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(RequestLimitException.class)
public Map defaultExceptionHandler(HttpServletRequest request,Exception e){
Mapmap = Maps.newConcurrentMap();
if(e instanceof RequestLimitException){
RequestLimitException limit = (RequestLimitException) e;
map.put("code", limit.getCode());
map.put("msg", limit.getMsg());
}else{
map.put("code", -1);
map.put("msg", "系统异常");
}
//未知错误
return map;
}
}
⑧请求限制异常
package com.hytc.mall.hytcmall.exception;
import lombok.Data;
/**
* @ClassName: RequestLimit
* @Description:
* @Author: BYP <[email protected]>
* @Date: 2020/9/27 9:59
* @Copyright: 2019 www.tydic.com Inc. All rights reserved.
* 注意:本内容仅限于弘毅天承信息技术股份有限公司内部传阅,禁止外泄以及用于其他的商业目
*/
@Data
public class RequestLimitException extends RuntimeException{
/*错误码*/
private String code;
/*错误提示*/
private String msg;
public RequestLimitException(){
}
public RequestLimitException(String code,String msg){
this.code = code;
this.msg = msg;
}
}