SpringMVC 防刷限流

主要目的

通过新建Springmvc 拦截器,防止表单重复提交、或者防止某个页面重复刷新,限制并发访问流量,防止恶意刷单,减少缓存或数据库不必要的负担。

主要流程

1. 用户访问 demo 路径下的所有页面,通过新建拦截器拦截请求访问该地址下的所有请求;

2. 在controller中,需要生成token的方法上增加注解@FormToken(save=true),当页面加载完毕时,已经生成此次请求对应的token在当前的HTTPSession中;

3. 在需要检查重复提交的controller的方法上添加注解@FormToken(remove=true);

4. 在完成填写表单后,点击提交时,此时拦截器会先校验此次提交是否是重复提交,主要通过验证当前的请求中是否有指定的 token,并判断是否与 HttpSession 中的 token 一致;

5. 提交的请求中包含的token是在前端页面<inputtype inputtype="hidden" name="formToken" value="${formToken}" /> 中加载token,并在提交时的Ajax中设置参数data: {'formToken': "${formToken}"}, 包含请求参数的token;

6. 为了确保拦截效果,可以尝试在controller中设置再一次校验token,按照同样的规则生成token与请求参数中的token对比、或者添加一个有效期,当token过期后,再一次拦截;

7. 通过CacheBuilder,CacheLoader与LoadingCache模块进行限流,控制并发数量;

详细代码

1.新建自定义注解类

// @Target(ElementType.METHOD) 方法的注解
@Target(ElementType.METHOD)
// Retention(RetentionPolicy.RUNTIME) 注解不仅被保存到class文件中,jvm加载class文件之后,仍然存在;
@Retention(RetentionPolicy.RUNTIME)
public @interface FormToken {
    // 保存表单 token,默认不保存
    boolean save() default false;
    // 移除表单 token,默认删除
    boolean remove() default false;
}

2.新建拦截器

// 新建拦截器,拦截指定路径下的所有请求,在 dispacher-servlet.xml 中配置,继承自 HandlerInterceptorAdapter
public class FormTokenInterceptor extends HandlerInterceptorAdapter {
    // 加密密钥 key
    protected String KEY = "XXX";

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        // 这个 handler 对象是否是 HandlerMethod 或它的子类的一个实例
        if (handler instanceof HandlerMethod) {
            HandlerMethod handlerMethod = (HandlerMethod) handler;
            Method method = handlerMethod.getMethod();
            // 实例化注解
            FormToken annotation = method.getAnnotation(FormToken.class);
            if (annotation != null) {
                // 调用注解保存 token 的 save(),判断当前 request 是否需要保存 token
                boolean needSaveSession = annotation.save();
                // 需要保存 token
                if (needSaveSession) {
                    // 按照自定方式生成 token,并用 MD5 加密
                    String token = XXXX;
                    // 保存当前时间至 session,在后台 controller 中作为加密对比的参数之一
                    request.getSession(false).setAttribute("formTime", currentTime);
                    // 在 session 中生成 token
                    request.getSession(false).setAttribute("formToken", MD5(token));
                }
                // 调用注解删除 token 的 remove(),判断当前 request 是否需要删除 token
                boolean needRemoveSession = annotation.remove();
                // 需要删除 token
                if (needRemoveSession) {
                    // 判断当前 request 是否重复提交,若重复提交,则拦截当前请求
                    if (isRepeatSubmit(request)) {
                        return false;
                    }
                    // 从当前请求的 HttpSession 中删除指定的 token
                    request.getSession(false).removeAttribute("formToken");
                }
            }
            return true;
        } else {
            // 其他正常请求不做拦截
            return super.preHandle(request, response, handler);
        }
    }

    // 判断当前请求是否重复提交。
    // 其实就是验证当前的请求中是否有指定的 token,并判断是否与 HttpSession 中的 token 一致
    private boolean isRepeatSubmit(HttpServletRequest request) {
        String serverToken = (String) request.getSession(false).getAttribute("formToken");
        if (StringUtils.isEmpty(serverToken)) {
            return true;
        }
        String clinetToken = request.getParameter("formToken");
        if (StringUtils.isEmpty(clinetToken)) {
            return true;
        }
        if (!serverToken.equals(clinetToken)) {
            return true;
        }
        return false;
    }

}

3. 配置拦截器:

后台controller 映射地址:@RequestMapping("XXX/"),在dispatcher-servlet.xml中配置此拦截器

	<mvc:interceptors>
		<mvc:interceptor>
			<mvc:mapping path="/XXX/**"/>
			<bean class="XXX/XXX.FormTokenInterceptor"/>
		</mvc:interceptor>
	</mvc:interceptors>

3.controller 验证

        // ....

        String formToken = request.getParameter("formToken");

        // 先从session中获取时间戳
        String currentTimeFromPage = (String) request.getSession(false).getAttribute("formTime");

        String seqNo = request.getParameter("seqNo");
        String newToken = XXX; // 参考拦截器中的token生成方式生成新的token校验
        boolean flag1 = StringUtils.isEmpty(formToken);
        boolean flag2 = StringUtils.equals(formToken, newToken);

        // 验证session中的token不为空,且token一致
        if (flag1 || !flag2) {
            _logger.error(String.format("token验证不一致!待验证token:%s, 提交推荐意愿评分生成的token:%s", formToken, newToken));
            return XXX;
        }

4.前端页面添加:

<inputtype inputtype="hidden" name="formToken" value="${formToken}" />

注意在ajax提交时 要加上 formToken参数

        //ajax请求
        $.ajax({
            url:'XXX',
            data: {'formToken': "${formToken}"}, // 重点!
            type:'get',
            dataType: 'json',
            timeout: 60000,
            success: function(data){
                //ajax返回
                if("0000" == data.isSucceed){
                    // 跳转到提交页面
                    location.href = 'XXX/XXX';
                }else {
                    // 错误处理
                    // ...
                }
            },
            error: function(){
                alert('系统错误或网络异常,请稍后再试!');
            }
        });

5.限流部分

    private static final LoadingCache<String, RateLimiter> rateLimiter = CacheBuilder
            .newBuilder().maximumSize(10000)
            .expireAfterWrite(10, TimeUnit.HOURS)
            .build(new CacheLoader<String, RateLimiter>() {
                public RateLimiter load(String key) {
                    _logger.info("创建RateLimiter key:" + key);
                    return RateLimiter.create(50); // 限流50
                }
            });

在需要防止重复刷新的方法中添加限流措施:

        // 限流
        try {
            if (!rateLimiter.get("XXXX").tryAcquire()) { // XXXX为需要限流的地址路径
                _logger.error("调用频次达到上限");
                return "XXX/XXX"; // 调用次数达上限后需要跳转的地址
            }
        } catch (Exception e) {
            _logger.error("限流器调用失败", e);
            return "XXX/XXX"; // 发生异常需要跳转的地址
        }

至此,防刷限流工作完毕。

参考链接:

https://blog.csdn.net/u011191463/article/details/78180538

http://blog.csdn.net/u013378306/article/details/52944780

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值