通过新建Springmvc 拦截器,防止表单重复提交、或者防止某个页面重复刷新,限制并发访问流量,防止恶意刷单,减少缓存或数据库不必要的负担。
1. 用户访问 demo 路径下的所有页面,通过新建拦截器拦截请求访问该地址下的所有请求;
2. 在controller中,需要生成token的方法上增加注解@FormToken(save=true),当页面加载完毕时,已经生成此次请求对应的token在当前的HTTPSession中;
3. 在需要检查重复提交的controller的方法上添加注解@FormToken(remove=true);
4. 在完成填写表单后,点击提交时,此时拦截器会先校验此次提交是否是重复提交,主要通过验证当前的请求中是否有指定的 token,并判断是否与 HttpSession 中的 token 一致;
5. 提交的请求中包含的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中配置此拦截器
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.前端页面添加:
注意在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 rateLimiter = CacheBuilder
.newBuilder().maximumSize(10000)
.expireAfterWrite(10, TimeUnit.HOURS)
.build(new CacheLoader() {
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