秒杀功能(9)安全优化三

接口限流防刷

为了防止恶意用户在短时间(如1秒内)刷网页访问太多次(如几百次),我们做了这个接口限流防刷的优化。

初步版本(Redis)

思路是:限制用户在若干秒内访问秒杀网站若干次之内。(如限制用户在5秒内只能访问网站5次)

代码逻辑:

  1. 在用户点击“立即秒杀”按钮时,会先访问/miaosha/path路径获取随机秒杀地址,在这之前会进行是否访问次数过多的判断;
  2. 在某用户首次访问时现在redis中设置次数:1次,有效期设置为5秒(通过redis自身的属性设置了限定时间);
  3. 当查询redis发现用户的次数小于5次时,则更新redis,进入秒杀环节,若大于5次则直接返回错误。

MiaoshaController中的相关方法的代码:

	@RequestMapping(value = "/path", method = RequestMethod.GET)
    @ResponseBody
    public Result<String> getMiaoshaPath(HttpServletResponse response, HttpServletRequest request, @RequestParam("goodsId") long goodsId,
                                         @CookieValue(value = MiaoshaUserService.COOKI_NAME_TOKEN,required = false) String cookieToken,
                                         @RequestParam(value = MiaoshaUserService.COOKI_NAME_TOKEN,required = false) String paramToken,
                                         @RequestParam(value = "verifyCode", defaultValue = "0") int verifyCode) {
        if (StringUtils.isEmpty(cookieToken) && StringUtils.isEmpty(paramToken)) {
            return Result.error(CodeMsg.SESSION_ERROR);//token不存在或失效
        }
        String token = StringUtils.isEmpty(paramToken) ? cookieToken : paramToken;
        MiaoshaUser user = userService.getByToken(response, token);//从token中读用户信息

        //查询访问的次数(5秒中访问不超过5次)
        String uri = request.getRequestURI();
        String key = uri + "_" + user.getId();
        Integer count = redisService.get(AccessKey.access,key,Integer.class);
        if (count == null){
            redisService.set(AccessKey.access,key,1); //时间设的5秒
        }else if (count < 5){ //这样的设置是指5秒内访问5次是正常,否则返回失败
            redisService.incr(AccessKey.access,key);
        }else {
            return Result.error(CodeMsg.ACCESS_LIMIT_REACHED);
        }


        //验证码校验
        boolean check = miaoshaService.checkVerifyCode(user, goodsId, verifyCode);
        if (!check) {
            return Result.error(CodeMsg.REQUEST_ILLEGAL);
        }
        String path = miaoshaService.createMiaoshaPath(user, goodsId);
        return Result.success(path);
    }

其中涉及到的AccessKey定义如下:

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");
    }

    public static AccessKey access = new AccessKey(5,"access");
}

该方法有个问题是不够通用,如果需求是在/path接口5秒内限制访问5次,而在另一接口10秒访问8次,等等;这样的需求无法用这个方法普遍适用。所以下面介绍更常用的方法。

拦截器版本

我们需要更通用的版本来实现上述功能,可以使用注解/拦截器的实现方式。

因为拦截用户是在用户在点击“立即秒杀”后,请求真实秒杀地址前先验证的,所以先改写MiaoshaController层中的getMiaoshaPath()方法。

//第二版:通过注解限流
    @AccessLimit(seconds = 5, maxCount = 5, needLogin = true)
    @RequestMapping(value = "/path", method = RequestMethod.GET)
    @ResponseBody
    public Result<String> getMiaoshaPath(HttpServletResponse response, @RequestParam("goodsId") long goodsId,
                                         @CookieValue(value = MiaoshaUserService.COOKI_NAME_TOKEN,required = false) String cookieToken,
                                         @RequestParam(value = MiaoshaUserService.COOKI_NAME_TOKEN,required = false) String paramToken,
                                         @RequestParam(value = "verifyCode", defaultValue = "0") int verifyCode) {
        if (StringUtils.isEmpty(cookieToken) && StringUtils.isEmpty(paramToken)) {
            return Result.error(CodeMsg.SESSION_ERROR);//token不存在或失效
        }
        String token = StringUtils.isEmpty(paramToken) ? cookieToken : paramToken;
        MiaoshaUser user = userService.getByToken(response, token);//从token中读用户信息

        //验证码校验
        boolean check = miaoshaService.checkVerifyCode(user, goodsId, verifyCode);
        if (!check) {
            return Result.error(CodeMsg.REQUEST_ILLEGAL);
        }
        String path = miaoshaService.createMiaoshaPath(user, goodsId);
        return Result.success(path);
    }

上面只加了一个自定义注解:@AccessLimit

  1. 下面要先定义该注解。
@Retention(RUNTIME)  //运行时
@Target(METHOD)  //作用于方法体上
public @interface AccessLimit {
    //三个参数
    int seconds();

    int maxCount();

    boolean needLogin() default true;
}
  1. 实现拦截器
@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.COOKI_NAME_TOKEN);
        String cookieToken = getCookieValue(request, MiaoshaUserService.COOKI_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;
    }

}

该拦截器继承HandlerInterceptorAdapter,具体流程为:
1)先获取用户信息(从token中),将用户设置为ThreadLocal变量,它是本地线程的副本,与线程绑定,存取只会存取在本地线程中;
2)拿到方法的注解,如果注解needLogin,则需要判断用户是否已登陆;
3)从redis中取出 由url和用户id组成的键,并判断是否大于设定次数;

其中,UserContext的定义如下:

public class UserContext {

    private static ThreadLocal<MiaoshaUser> userHolder = new ThreadLocal<MiaoshaUser>();

    public static void setUser(MiaoshaUser user) {
        userHolder.set(user);
    }

    public static MiaoshaUser getUser() {
        return userHolder.get();
    }

}
  1. 将拦截器注册到WebConfig中,这个类实现WebMvcConfigurer。
@Configuration
public class WebConfig implements WebMvcConfigurer {

    @Autowired
    AccessInterceptor accessInterceptor;

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

}

至此就完成了接口安全优化。
秒杀的项目也结束了。

  • 2
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值