(16)SprintBoot 2.X 接口限流防刷
- 1. 接口限流防刷
- 2. 代码实现
- 2.1 拦截器注解的引用 @AccessLimit(seconds=5, maxCount=5, needLogin=true)
- 2.2 @AccessLimit注解的实现
- 2.3 缓存Key前缀,可以设置有效时间
- 2.4 注解能够生效,必须要配置拦截器AccessInterceptor
- 2.4.1 继承HandlerInterceptorAdapter拦截器基类,通过实现这个接口,拿到方法上的注解
- 2.4.2 判断用户登录
- 2.4.3 判断访问次数与失效时间(缓存时间)
- 2.5 UserContext 封装用户信息 ,ThreadLocal 线程安全,保存当前线程的user,方便下次使用
- 2.6 拦截器注册,继承WebMvcConfigurerAdapter后重写addInterceptors()
- 3.由于拦截器在参数解析器前执行,所以将参数解析器的代码逻辑剪切到拦截器里执行。拦截器把user对象获取后放到线程安全的ThreadLocal中,参数解析器只需要从ThreadLocal中获取即可。
1. 接口限流防刷
1.1 思路
- 限制同一用户一定时间内(如1 min)只能访问固定次数,可以使用拦截器减少对业务的侵入,在服务端对系统做一层保护
1.2 技术细节
1.用户图片验证码验证通过后获取秒杀路径
2. 前缀+userId作为一个记录该用户访问次数的key,在redis中记录该用户访问次数
3. 每次获取秒杀路径前,实现一个拦截器,从redis中取出该该用户的访问次数
1)如果缓存里面没有取到,初始化为1.
2)如果缓存里面取得值并且小于限定次数,incr该key,进入获取秒杀路径业务逻辑
3)如果超过访问次数,直接返回“频繁访问”
2. 代码实现
2.1 拦截器注解的引用 @AccessLimit(seconds=5, maxCount=5, needLogin=true)
@AccessLimit(seconds=5, maxCount=5, needLogin=true)
@RequestMapping(value="/path", method=RequestMethod.GET)
@ResponseBody
public Result<String> getMiaoshaPath(HttpServletRequest request, MiaoshaUser user,
@RequestParam("goodsId")long goodsId,
@RequestParam(value="verifyCode", defaultValue="0")int verifyCode
) {
if(user == null) {
return Result.error(CodeMsg.SESSION_ERROR);
}
String path = miaoshaService.createMiaoshaPath(user,goodsId);
boolean check = miaoshaService.checkVerifyCode(user, goodsId, verifyCode);
if(!check) {
return Result.error(CodeMsg.REQUEST_ILLEGAL);
}
return Result.success(path);
}
2.2 @AccessLimit注解的实现
@Retention(RUNTIME)
@Target(METHOD)
public @interface AccessLimit {
int seconds();
int maxCount();
boolean needLogin() default true;
}
2.3 缓存Key前缀,可以设置有效时间
public class AccessKey extends BasePrefix{
private AccessKey( int expireSeconds, String prefix) {
super(expireSeconds, prefix);
}
public static AccessKey withExpire(int expireSeconds) {
return new AccessKey(expireSeconds, "access");
}
}
2.4 注解能够生效,必须要配置拦截器AccessInterceptor
2.4.1 继承HandlerInterceptorAdapter拦截器基类,通过实现这个接口,拿到方法上的注解
2.4.2 判断用户登录
- 这里将之前原先定义在解析用户参数的代码封装。然后在将用这个封装的用户信息,set到ThreadLocal 中,本地线程副本,该变量与线程绑定,存取只会存取在本地线程中。然后之前获取用户的代码直接取到该用户即可。
2.4.3 判断访问次数与失效时间(缓存时间)
* 判断访问次数count ,从缓存中存取,然后根据注解时间,动态设置缓存的过期时间
@Service
public class AccessInterceptor extends HandlerInterceptorAdapter{
@Autowired
MiaoshaUserService userService;
@Autowired
RedisService redisService;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
throws Exception {
if(handler instanceof HandlerMethod) {
MiaoshaUser user = getUser(request, response);
UserContext.setUser(user);
HandlerMethod hm = (HandlerMethod)handler;
AccessLimit accessLimit = hm.getMethodAnnotation(AccessLimit.class);
if(accessLimit == null) {
return true;
}
int seconds = accessLimit.seconds();
int maxCount = accessLimit.maxCount();
boolean needLogin = accessLimit.needLogin();
String key = request.getRequestURI();
if(needLogin) {
if(user == null) {
render(response, CodeMsg.SESSION_ERROR);
return false;
}
key += "_" + user.getId();
}else {
}
AccessKey ak = AccessKey.withExpire(seconds);
Integer count = redisService.get(ak, key, Integer.class);
if(count == null) {
redisService.set(ak, key, 1);
}else if(count < maxCount) {
redisService.incr(ak, key);
}else {
render(response, CodeMsg.ACCESS_LIMIT_REACHED);
return false;
}
}
return true;
}
private void render(HttpServletResponse response, CodeMsg cm)throws Exception {
response.setContentType("application/json;charset=UTF-8");
OutputStream out = response.getOutputStream();
String str = JSON.toJSONString(Result.error(cm));
out.write(str.getBytes("UTF-8"));
out.flush();
out.close();
}
private MiaoshaUser getUser(HttpServletRequest request, HttpServletResponse response) {
String paramToken = request.getParameter(MiaoshaUserService.COOKIE_NAME_TOKEN);
String cookieToken = getCookieValue(request, MiaoshaUserService.COOKIE_NAME_TOKEN);
if(StringUtils.isEmpty(cookieToken) && StringUtils.isEmpty(paramToken)) {
return null;
}
String token = StringUtils.isEmpty(paramToken)?cookieToken:paramToken;
return userService.getByToken(response, token);
}
private String getCookieValue(HttpServletRequest request, String cookiName) {
Cookie[] cookies = request.getCookies();
if(cookies == null || cookies.length <= 0){
return null;
}
for(Cookie cookie : cookies) {
if(cookie.getName().equals(cookiName)) {
return cookie.getValue();
}
}
return null;
}
}
2.5 UserContext 封装用户信息 ,ThreadLocal 线程安全,保存当前线程的user,方便下次使用
public class UserContext {
private static ThreadLocal<MiaoshaUser> userThreadLocal = new ThreadLocal<MiaoshaUser>();
public static void setUser(MiaoshaUser user){
userThreadLocal.set(user);
}
public static MiaoshaUser gerUser(){
return userThreadLocal.get();
}
}
2.6 拦截器注册,继承WebMvcConfigurerAdapter后重写addInterceptors()
@Configuration
public class WebConfig extends WebMvcConfigurerAdapter{
@Autowired
UserArgumentResolver userArgumentResolver;
@Autowired
AccessInterceptor accessInterceptor;
@Override
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> argumentResolvers) {
argumentResolvers.add(userArgumentResolver);
}
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(accessInterceptor);
}
}
3.由于拦截器在参数解析器前执行,所以将参数解析器的代码逻辑剪切到拦截器里执行。拦截器把user对象获取后放到线程安全的ThreadLocal中,参数解析器只需要从ThreadLocal中获取即可。