热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

SpringBoot+Token实现接口幂等性|防止表单重复提交

一、概念幂等性,通俗的说就是一个接口,多次发起同一个请求,必须保证操作只能执行一次比如:订单接口,不能多次创建订单支付接口,重复支付同一笔订单只能扣一次钱支付宝回调接

 


一、概念

幂等性, 通俗的说就是一个接口, 多次发起同一个请求, 必须保证操作只能执行一次 比如:


  • 订单接口, 不能多次创建订单






  • 支付接口, 重复支付同一笔订单只能扣一次钱


  • 支付宝回调接口, 可能会多次回调, 必须处理重复回调


  • 普通表单提交接口, 因为网络超时等原因多次点击提交, 只能成功一次 等等



二、常见解决方案



  1. 唯一索引 -- 防止新增脏数据


  2. token机制 -- 防止页面重复提交


  3. 悲观锁 -- 获取数据的时候加锁(锁表或锁行)


  4. 乐观锁 -- 基于版本号version实现, 在更新数据那一刻校验数据


  5. 分布式锁 -- redis(jedis、redisson)或zookeeper实现


  6. 状态机 -- 状态变更, 更新数据时判断状态



三、本文实现

本文采用第2种方式实现, 即通过token机制实现接口幂等性校验。(假如是分布式环境,可以考虑将生成的token由JVM内存(session)转移到redis等,可参考:https://mp.weixin.qq.com/s/v_iyZVd5ldixnhaxkdSArA)

四、实现思路

为保证幂等性,每一次请求(创建订单)接口都生成一个新的唯一标识 token,  并将此 token存入session, 同时返回token给其前端,下次请求(下单)接口时, 将此 token放到header或者作为请求参数带过来, 后端(下单)接口判断当前session中的token与前端传递过来的token是否相等:


  • 当前session中是否存在此token


  • 前端请求参数中是否携带有token


  • 如果都存在, 并且相等,正常处理业务逻辑, 并从session中删除此 token, 那么, 如果是重复请求, 由于 token已被删除, 则不能通过校验, 返回 请勿重复操作提示


  • 如果不存在, 说明参数不合法或者是重复请求, 返回提示即可


集群环境:采用token加redis(redis单线程) 

单JVM环境:采用token加redis或token加jvm内存

五、项目简介



  • SpringBoot


  • Thymeleaf (Spring Boot 推荐使用 Thymeleaf 来代替 JSP) 


  • 自定义注解@Token注解 + 拦截器对请求进行拦截


  • 继承WebMvcConfigurationSupport ,在其中配置拦截器



六、项目实战

1、先创建一个SpringBoot工程,并引入Thymeleaf 视图模板依赖


org.springframework.boot
spring-boot-starter-thymeleaf

2、自定义注解@Token ,只需要在具体的请求接口方法添加即可。

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Token {

/**
* 是否创建新的token
*/
boolean generate() default false;
/**
* 是否移除token
*/
boolean remove() default false;
}

3、创建拦截器,并且继承 HandlerInterceptorAdapter ,或者实现 HandlerInterceptor 接口,建议使用HandlerInterceptorAdapter,因为可以按需进行方法的覆盖,不用实现所有方法。

/**
* @description: 表单提交--token拦截器
* @author: xianhao_gan
* @date: 2019/08/16
**/
@Slf4j
public class TokenInterceptor extends HandlerInterceptorAdapter {

/** The Constant TOKEN. 放在session中的token */
private static final String TOKEN = "token";

/**
* 拦截处理程序的执行。在HandlerMapping之后调用,确定适当的处理程序对象,但是在HandlerAdapter调用处理程序之前调用。
* @param request
* @param response
* @param handler
* @return
* @throws Exception
*/
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response,
Object handler) throws Exception {
if (handler instanceof HandlerMethod) {
Method method = ((HandlerMethod) handler).getMethod();
Token tokenAnnotation = method.getAnnotation(Token.class);
if (tokenAnnotation != null) {
HttpSession session = request.getSession();

// 创建新的表单提交令牌token,防止表单重复提交
boolean isGenerate = tokenAnnotation.generate();
if (isGenerate) {
String formToken = UUID.randomUUID().toString();
session.setAttribute(TOKEN, formToken);
log.info("创建表单提交令牌成功,token:" + formToken);
return true;
}

// 删除token令牌
boolean isRemove = tokenAnnotation.remove();
if (isRemove) {
if (isRepeatSubmit(request)) {
log.warn("表单不能重复提交:" + request.getRequestURL());
return false;
}
session.removeAttribute(TOKEN);
}
}
} else {
return super.preHandle(request, response, handler);
}
return true;
}
}

其中,我们只需要覆写preHandle方法即可。

说明:


  • preHandle  方法会在请求处理之前进行调用(Controller方法调用之前)

  • postHandle  请求处理之后进行调用,但是在视图被渲染之前(Controller方法调用之后)

  • afterCompletion  在整个请求结束之后被调用,也就是在DispatcherServlet 渲染了对应的视图之后执行(主要是用于进行资源清理工作)

isRepeatSubmit校验token方法如下:

/**
* 表单是否重复提交校验
* @param request
* @return
*/
private boolean isRepeatSubmit(HttpServletRequest request) {
//session中token
String token = (String) request.getSession().getAttribute(TOKEN);
if (StringUtils.isEmpty(token)) {
return true;
}
//请求头中获取token
String reqToken = request.getHeader(TOKEN);
if (StringUtils.isEmpty(reqToken)) {
//请求参数request中获取token
reqToken = request.getParameter(TOKEN);
if (StringUtils.isEmpty(reqToken)) {
return true;
}
}
//对比session与前端传递过来的token是否相等
if (!token.equals(reqToken)) {
return true;
}
return false;
}

4、配置拦截器

5、新增OrderController 控制器,分别提供创建订单(跳转订单)、提交订单(下单)两个http接口。

并且在创建订单接口方法加上注解@Token(generate = true)

在提交订单接口方法加上注解@Token(remove = true)

package com.stwen.token.controller;

import com.stwen.token.annotation.Token;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

/**
* @description: 订单控制器
* @author: xianhao_gan
* @date: 2019/08/16
**/
@Controller
@RequestMapping("/order")
@Slf4j
public class OrderController {

@RequestMapping("/")
public String index(){
return "index";
}

/**
* 跳转订单详情页面--下单
* @param request
* @param response
* @return
*/
@RequestMapping("/detail")
@Token(generate = true)
public String orderPage(HttpServletRequest request, HttpServletResponse response){

//TODO 调用具体业务逻辑-生成订单

log.info("打开订单详情...");
return "order_detail";
}

/**
* 提交订单
* @param request
* @param response
* @return
*/
@RequestMapping("/submit")
@Token(remove = true)
public String orderSubmit(HttpServletRequest request, HttpServletResponse response){

//TODO 调用具体业务逻辑--提交订单

log.info("hello,订单提交成功。");
return "success";
}

}

注意:在接口方法中可以具体调用自己的业务逻辑,但是需要考虑异常情况:在你处理具体业务逻辑时发生异常,比如创建订单-跳转订单接口业务逻辑发生异常,但是拦截器 preHandle 方法已经创建好了token放在session中,这时就需要手动删除session中的token,或者实现一个切面@Aspect,在@AfterThrowing 中捕获异常时,清除session中token等。

6、新增3个html测试页面:index.html 、order_detail.html、success.html 

由 index.html 跳转到 order_detail.html 时,会被拦截创建一个token,返回放到input 隐藏域,当点击提交时,会把该token一并带过去。提交订单成功,将返回success 成功页面。

7、测试

运行项目,配置的是8080 端口,访问:localhost:8080/  显示首页,点击如下,便会跳转到订单详情-下单


推荐阅读
  • ### 优化后的摘要本学习指南旨在帮助读者全面掌握 Bootstrap 前端框架的核心知识点与实战技巧。内容涵盖基础入门、核心功能和高级应用。第一章通过一个简单的“Hello World”示例,介绍 Bootstrap 的基本用法和快速上手方法。第二章深入探讨 Bootstrap 与 JSP 集成的细节,揭示两者结合的优势和应用场景。第三章则进一步讲解 Bootstrap 的高级特性,如响应式设计和组件定制,为开发者提供全方位的技术支持。 ... [详细]
  • 本文详细介绍了Java代码分层的基本概念和常见分层模式,特别是MVC模式。同时探讨了不同项目需求下的分层策略,帮助读者更好地理解和应用Java分层思想。 ... [详细]
  • 解决Only fullscreen opaque activities can request orientation错误的方法
    本文介绍了在使用PictureSelectorLight第三方框架时遇到的Only fullscreen opaque activities can request orientation错误,并提供了一种有效的解决方案。 ... [详细]
  • 应用链时代,详解 Avalanche 与 Cosmos 的差异 ... [详细]
  • 本文介绍了如何利用HTTP隧道技术在受限网络环境中绕过IDS和防火墙等安全设备,实现RDP端口的暴力破解攻击。文章详细描述了部署过程、攻击实施及流量分析,旨在提升网络安全意识。 ... [详细]
  • 本文介绍了如何在 macOS 上安装 HL-340 USB 转串口驱动,并提供了详细的步骤和注意事项。包括下载驱动、关闭系统完整性保护、安装驱动以及验证安装的方法。 ... [详细]
  • CentOS 7 中 iptables 过滤表实例与 NAT 表应用详解
    在 CentOS 7 系统中,iptables 的过滤表和 NAT 表具有重要的应用价值。本文通过具体实例详细介绍了如何配置 iptables 的过滤表,包括编写脚本文件 `/usr/local/sbin/iptables.sh`,并使用 `iptables -F` 清空现有规则。此外,还深入探讨了 NAT 表的配置方法,帮助读者更好地理解和应用这些网络防火墙技术。 ... [详细]
  • 深入解析Struts、Spring与Hibernate三大框架的面试要点与技巧 ... [详细]
  • Web开发框架概览:Java与JavaScript技术及框架综述
    Web开发涉及服务器端和客户端的协同工作。在服务器端,Java是一种优秀的编程语言,适用于构建各种功能模块,如通过Servlet实现特定服务。客户端则主要依赖HTML进行内容展示,同时借助JavaScript增强交互性和动态效果。此外,现代Web开发还广泛使用各种框架和库,如Spring Boot、React和Vue.js,以提高开发效率和应用性能。 ... [详细]
  • Cosmos生态系统为何迅速崛起,波卡作为跨链巨头应如何应对挑战?
    Cosmos生态系统为何迅速崛起,波卡作为跨链巨头应如何应对挑战? ... [详细]
  • SSL 错误:目标主机名与备用证书主题名称不匹配
    在使用 `git clone` 命令时,常见的 SSL 错误表现为:无法访问指定的 HTTPS 地址(如 `https://ip_or_domain/xxxx.git`),原因是目标主机名与备用证书主题名称不匹配。这通常是因为服务器的 SSL 证书配置不正确或客户端的证书验证设置有问题。建议检查服务器的 SSL 证书配置,确保其包含正确的主机名,并确认客户端的证书信任库已更新。此外,可以通过临时禁用 SSL 验证来排查问题,但请注意这会降低安全性。 ... [详细]
  • 提升Android开发效率:Clean Code的最佳实践与应用
    在Android开发中,提高代码质量和开发效率是至关重要的。本文介绍了如何通过Clean Code的最佳实践来优化Android应用的开发流程。以SQLite数据库操作为例,详细探讨了如何编写高效、可维护的SQL查询语句,并将其结果封装为Java对象。通过遵循这些最佳实践,开发者可以显著提升代码的可读性和可维护性,从而加快开发速度并减少错误。 ... [详细]
  • 在探讨Hibernate框架的高级特性时,缓存机制和懒加载策略是提升数据操作效率的关键要素。缓存策略能够显著减少数据库访问次数,从而提高应用性能,特别是在处理频繁访问的数据时。Hibernate提供了多层次的缓存支持,包括一级缓存和二级缓存,以满足不同场景下的需求。懒加载策略则通过按需加载关联对象,进一步优化了资源利用和响应时间。本文将深入分析这些机制的实现原理及其最佳实践。 ... [详细]
  • 在本文中,我们将为 HelloWorld 项目添加视图组件,以确保控制器返回的视图路径能够正确映射到指定页面。这一步骤将为后续的测试和开发奠定基础。首先,我们将介绍如何配置视图解析器,以便 SpringMVC 能够识别并渲染相应的视图文件。 ... [详细]
  • 深入解析InnoDB中的多版本并发控制机制
    多版本并发控制(MVCC)是InnoDB存储引擎中的一项关键技术,通过维护数据在不同时间点的多个版本,确保了事务的隔离性和一致性。每个读取操作都能获得一个与事务启动时一致的数据视图,从而提高了并发性能并减少了锁竞争。此外,MVCC还支持多种隔离级别,如可重复读和读已提交,进一步增强了系统的灵活性和可靠性。 ... [详细]
author-avatar
mobiledu2502886217
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有