(16)SprintBoot 2.X 接口限流防刷

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 {
                //do nothing
            }
            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;

    /**
     * SpringMVC框架回调addArgumentResolvers,然后给Controller的参数赋值
     * @param argumentResolvers
     */
    @Override
    public void addArgumentResolvers(List<HandlerMethodArgumentResolver> argumentResolvers) {
        argumentResolvers.add(userArgumentResolver);
    }

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(accessInterceptor);
    }
}

3.由于拦截器在参数解析器前执行,所以将参数解析器的代码逻辑剪切到拦截器里执行。拦截器把user对象获取后放到线程安全的ThreadLocal中,参数解析器只需要从ThreadLocal中获取即可。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值