前提说明:SpringBoot 集成封装Redis
功能说明:
使用了Java 注解+Redis实现对接口访问限流控制功能
代码说明:
自定义接口限流注解标签:
package com.zzg.common.annotation;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* 自定义限流注解
* @author zzg
*
*/
@Inherited
@Documented
@Target({ElementType.FIELD,ElementType.TYPE,ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface AccessLimit {
//指定second 时间内 API请求次数
int times() default 4;
// 请求次数的指定时间范围 秒数(redis数据过期时间)
int second() default 10;
}
自定义拦截器解析接口限流注解标签:
package com.digipower.component.interceptor;
import java.io.OutputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;
import com.alibaba.fastjson.JSON;
import com.digipower.common.annotation.AccessLimit;
import com.digipower.common.entity.Result;
import com.digipower.common.util.IpUtils;
import com.digipower.redis.util.RedisUtil;
/**
* 接口防刷拦截器
* @author zzg
*
*/
@Component
public class FangshuaInterceptor extends HandlerInterceptorAdapter {
@Autowired
private RedisUtil redisUtil;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
//判断请求是否属于方法的请求
if(handler instanceof HandlerMethod){
HandlerMethod hm = (HandlerMethod) handler;
//获取方法中的注解,看是否有该注解
AccessLimit accessLimit = hm.getMethodAnnotation(AccessLimit.class);
if(accessLimit == null){
return true;
}
int times = accessLimit.times();//请求次数
int second = accessLimit.second();//请求时间范围
//根据 IP + API 限流
String key = IpUtils.getIpAddr(request) + request.getRequestURI();
//根据key获取已请求次数
Integer maxTimes = (Integer) redisUtil.get(key);
if(maxTimes == null){
//set时一定要加过期时间
redisUtil.set(key, 1, second);
}else if(maxTimes < times){
redisUtil.set(key, maxTimes+1, second);
}else{
//超出访问次数
render(response);
return false;
}
}
return true;
}
private void render(HttpServletResponse response)throws Exception {
response.setContentType("application/json;charset=UTF-8");
OutputStream out = response.getOutputStream();
String str = JSON.toJSONString(Result.error("接口请求过于频繁"));
out.write(str.getBytes("UTF-8"));
out.flush();
out.close();
}
}
涉及IP工具类:IpUtils源代码:
package com.zzg.common.util;
import javax.servlet.http.HttpServletRequest;
/**
* IP 工具类
* @author zzg
*
*/
public class IpUtils {
/**
* IpUtils工具类方法
* 获取真实的ip地址
* @param request
* @return
*/
public static String getIpAddr(HttpServletRequest request) {
String ip = request.getHeader("X-Forwarded-For");
if(org.apache.commons.lang.StringUtils.isNotEmpty(ip) && !"unKnown".equalsIgnoreCase(ip)){
//多次反向代理后会有多个ip值,第一个ip才是真实ip
int index = ip.indexOf(",");
if(index != -1){
return ip.substring(0,index);
}else{
return ip;
}
}
ip = request.getHeader("X-Real-IP");
if(org.apache.commons.lang.StringUtils.isNotEmpty(ip) && !"unKnown".equalsIgnoreCase(ip)){
return ip;
}
return request.getRemoteAddr();
}
}
配置拦截器
package com.zzg.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;
import com.zzg.component.interceptor.FangshuaInterceptor;
@Configuration
@EnableWebMvc
public class SpringMVCConfig extends WebMvcConfigurerAdapter {
/**
* 资源处理
*/
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("swagger-ui.html").addResourceLocations("classpath:/META-INF/resources/");
registry.addResourceHandler("/webjars/**").addResourceLocations("classpath:/META-INF/resources/webjars/");
// 资源图片映射(静态配置->基于数据库实现动态配置)
registry.addResourceHandler("/upload_file/**").addResourceLocations("file:F:/data/upload_file/");
super.addResourceHandlers(registry);
}
// 限流拦截器
@Bean
public FangshuaInterceptor accessLimitInterceptor(){
return new FangshuaInterceptor();
}
@Override
public void addInterceptors(InterceptorRegistry registry) {
// TODO Auto-generated method stub
//API限流拦截
registry.addInterceptor(accessLimitInterceptor()).addPathPatterns("/**").excludePathPatterns("/swagger-ui.html/**", "/swagger-resources/**", "/webjars/**", "/v2/api-docs/**");
}
}
在项目的验证码接口使用接口限流注解:
/**
* 1、生成验证码
* @param httpServletRequest
* @param httpServletResponse
* @throws Exception
*/
@AccessLimit(times = 5, second=10)
@RequestMapping("/defaultKaptcha")
public void defaultKaptcha(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse)
throws Exception {
byte[] captchaChallengeAsJpeg = null;
ByteArrayOutputStream jpegOutputStream = new ByteArrayOutputStream();
try {
// 生产验证码字符串并保存到数据库中
String createText = defaultKaptcha.createText();
saveVerificationCode(createText);
httpServletRequest.getSession().setAttribute("rightCode", createText);
// 使用生产的验证码字符串返回一个BufferedImage对象并转为byte写入到byte数组中
BufferedImage challenge = defaultKaptcha.createImage(createText);
ImageIO.write(challenge, "jpg", jpegOutputStream);
} catch (IllegalArgumentException e) {
httpServletResponse.sendError(HttpServletResponse.SC_NOT_FOUND);
return;
}
// 定义response输出类型为image/jpeg类型,使用response输出流输出图片的byte数组
captchaChallengeAsJpeg = jpegOutputStream.toByteArray();
httpServletResponse.setHeader("Cache-Control", "no-store");
httpServletResponse.setHeader("Pragma", "no-cache");
httpServletResponse.setDateHeader("Expires", 0);
httpServletResponse.setContentType("image/jpeg");
ServletOutputStream responseOutputStream = httpServletResponse.getOutputStream();
responseOutputStream.write(captchaChallengeAsJpeg);
responseOutputStream.flush();
responseOutputStream.close();
}
结果展示:
正常访问验证码接口,验证码图片现在正常输出.
使用F5 频繁刷新验证码接口,会得到如下提示信息: