注解 + 拦截器:解决表单重复提交
前言
学习 Spring Boot 中,我想将我在项目中添加几个我在 SpringMVC 框架中常用的工具类(主要都是涉及到 Spring AOP 部分知识)。比如,表单重复提交,?秒防刷新,全局异常捕抓类,IP黑名单(防爬虫设置)…………等等。接下来的时间,我尝试将这些框架整合到 Spring Boot 中(尽可能完成),毕竟项目开发中这些工具是非常有用的。
注意,这些工具基本上都是我以前在 github 之类开源平台找到的小工具类,作者的信息什么的许多都忘了。先说声不好意思了。若有相关信息,麻烦提醒一下~
介绍
这里就不详细介绍相应的知识了,主要提及有关涉及到的术语:
-
拦截器
Spring 拦截器有两种实现方法。一种是继承HandlerInterceptorAdapter
,拥有preHandle
(业务处理器处理请求之前被调用),postHandle
(在业务处理器处理请求执行完成后,生成视图之前执行),afterCompletion
(在完全处理完请求后被调用,可用于清理资源等)三个方法。
另一种就是调用 Spring AOP 的方法来实现。而且,我觉得这种方法更加灵活方便,所以我比较经常使用这种方法。 -
AOP( AspectJ— 注解 风格)
AOP 就是 Aspect Oriented Programming(面向方面编程)。
1. 连接点(Joinpoint):表示需要在程序中插入横切关注点的扩展点,连接点可能是类初始化、方法执行、方法调用、字段调用或处理异常等等,Spring只支持方法执行连接点。
2. 前置通知(@Before):在某连接点(join point)之前执行的通知,但这个通知不能阻止连接点前的执行(除非它抛出一个异常)。
3. 抛出异常后通知(@AfterThrowing):方法抛出异常退出时执行的通知
附上:大神开涛的有关 Spring AOP 博客:http://jinnianshilongnian.iteye.com/blog/1474325
解决问题
什么是表单重复提交?
服务器认为是同一个表单,在短时间内重复(不止一次)提交,或者提交异常。比如,在服务器还没有响应前我们不断点击刷新网页上一个提交按钮,或者通过 ajax 不断对服务器发送请求报文!
防止情况
- 不通过正常路径访问页面表单;
- session 失效情况下提交表单;
- 短时间内不止一次提交表单。
解决方案
一般情况下,是在服务器利用 session 来防止这个问题的。
流程图:
1. 网页点击事件,网页提交发送申请;
2. 服务器收到申请,并产生令牌(Token),并存于 Session 中;
3. 服务器将令牌返回给页面,页面将令牌与表单真正提交给服务器。
这种就是 structs 的令牌方式。还有其他方法,就是重定向方法或设置页面过期(前端部分不太了解),不过还是感觉强制跳转不是特别友好,同时也不够灵活多用。
前期准备
新建一个 spring boot 项目(建议 1.3.X 以上版本)。
加入 aop 依赖,默认设置就行了:
1 2 3 4 | <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-aop</artifactId> </dependency> |
正式开工
- 注解类 Token.java
1 2 3 4 5 6 7 8 9 | @Retention (RetentionPolicy.RUNTIME) @Target (ElementType.METHOD) @Documented public @interface Token { //生成 Token 标志 boolean save() default false ; //移除 Token 值 boolean remove() default false ; } |
- 表单异常类 FormRepeatException.java
1 2 3 4 5 6 | public class FormRepeatException extends RuntimeException { public FormRepeatException(String message){ super (message);} public FormRepeatException(String message, Throwable cause){ super (message, cause);} } |
- 拦截器 TokenContract.java
注意:@Aspect
与@Component
两个注解!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 | @Aspect @Component public class TokenContract { private static final Logger logger = LoggerFactory.getLogger(TokenContract. class ); @Before ( "within(@org.springframework.stereotype.Controller *) && @annotation(token)" ) public void testToken( final JoinPoint joinPoint, Token token){ try { if (token != null ) { //获取 joinPoint 的全部参数 Object[] args = joinPoint.getArgs(); HttpServletRequest request = null ; HttpServletResponse response = null ; for ( int i = 0 ; i < args.length; i++) { //获得参数中的 request && response if (args[i] instanceof HttpServletRequest) { request = (HttpServletRequest) args[i]; } if (args[i] instanceof HttpServletResponse) { response = (HttpServletResponse) args[i]; } } boolean needSaveSession = token.save(); if (needSaveSession){ String uuid = UUID.randomUUID().toString(); request.getSession().setAttribute( "token" , uuid); logger.debug( "进入表单页面,Token值为:" +uuid); } boolean needRemoveSession = token.remove(); if (needRemoveSession) { if (isRepeatSubmit(request)) { logger.error( "表单重复提交" ); throw new FormRepeatException( "表单重复提交" ); } request.getSession( false ).removeAttribute( "token" ); } } } catch (FormRepeatException e){ throw e; } catch (Exception e){ logger.error( "token 发生异常 : " +e); } } private boolean isRepeatSubmit(HttpServletRequest request) throws FormRepeatException { String serverToken = (String) request.getSession( false ).getAttribute( "token" ); if (serverToken == null ) { //throw new FormRepeatException("session 为空"); return true ; } String clinetToken = request.getParameter( "token" ); if (clinetToken == null || clinetToken.equals( "" )) { //throw new FormRepeatException("请从正常页面进入!"); return true ; } if (!serverToken.equals(clinetToken)) { //throw new FormRepeatException("重复表单提交!"); return true ; } logger.debug( "校验是否重复提交:表单页面Token值为:" +clinetToken + ",Session中的Token值为:" +serverToken); return false ; } } |
Controller类
访问 http://localhost:8080/savetoken 来获得令牌值
访问 http://localhost:8080/removetoken?token=XXX 来提交真正的表单
1 2 3 4 5 6 7 8 9 10 11 12 13 | @Token (save = true ) @RequestMapping ( "/savetoken" ) @ResponseBody public String getToken(HttpServletRequest request, HttpServletResponse response){ return (String) request.getSession().getAttribute( "token" ); } @Token (remove = true ) @RequestMapping ( "/removetoken" ) @ResponseBody public String removeToken(HttpServletRequest request, HttpServletResponse response){ return "success" ; } |
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· Linux系列:如何用heaptrack跟踪.NET程序的非托管内存泄露
· 开发者必知的日志记录最佳实践
· winform 绘制太阳,地球,月球 运作规律
· 超详细:普通电脑也行Windows部署deepseek R1训练数据并当服务器共享给他人
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· AI 智能体引爆开源社区「GitHub 热点速览」
· 写一个简单的SQL生成工具