赞
踩
在传统的web
项目比如使用SSM
和SSH
框架开发的时候,涉及表单提交时,可能会产生表单重复提交
问题,还有分布式开发中rpc
远程调用、MQ
消费者幂等(保证唯一)、甚至常见的在网络产生延迟
的情况下,都可能产生重复请求,这时候会涉及到表单重复提交
,这种情况我们称之为幂等性
;
幂等性
,其实说白了就是一次请求的唯一性
,按照以前常用的做法是:
第一种
是在前端由前端工程师使用JS
控制,比如提交完请求之后将提交按钮置灰,让用户不能够再点击发送请求,这样其实是不专业的;第二种``Token+Redis
机制处理,这种做法在大型项目中较为流行,比较专业,其简要原理是后端生成一个唯一的提交令牌(token)
,并存储在服务端(Redis缓存)
。页面提交请求携带这个提交令牌,后端验证,并在第一次验证后删除该令牌(Redis缓存中)
,保证提交请求的唯一性,这种常见的后端存储是采用NoSQL
数据库,比如Redis
、MongoDB
等等缓存中。token
然后进行一统校验,如果一百个接口,要写一百次,代码太过冗余,所以针对Token+Redis
方案我们可以通过注解方式
,使用AOP
技术对注解进行解析
,这样就事半功倍了。如果过滤器的话,所有接口都进行了校验。
1. 提交按钮点击多次
2. 点击刷新按钮
3. 使用浏览器后退按钮重复之前的操作
4. 使用浏览器历史记录重复提交表单
5. 浏览器重复的HTTP请求
6. nginx重发等情况
7. nginx重发
8. 分布式开发中rpc远程调用重试
9. MQ消费者幂等
10.网络产生延迟
在传统的项目中,采用redis+token
,注意点是:令牌 保证唯一的并且是临时的 过一段时间失效
这种方式是解决Api接口幂等性
问题比较常使用的一种方式。
(1). 注意在获取Token
这种方法代码一定要上分布式锁,需要保证只有一个线程执行
,否则会造成token不唯一
,如果多个线程,造成步骤乱套了;
(2). 调用业务接口前先调用接口获取token
,然后前端调用接口发起请求的时候,将该令牌携带放到请求头中 ,后端收到请求后,获取请求头中的Token
令牌,拿到该令牌后,去Redis
中查找,如果能够从Redis
中获取到该令牌 ,立即将当前令牌删除掉,然后执行该方法业务逻辑 ,如果获取不到对应的令牌。返回提示“请不要重复提交!” 信息。
(3). 如果别人获得了你的token
,然后拿去做坏事,采用机器模拟去攻击,这是很危险的,这时候我们要用验证码来解决。
在数据库层次,常见的控制表单重复提交就是给数据库增加唯一键约束
,通过设置唯一键
,确保数据库只可以添加一条数据,保证相同数据不会插入第二条,但这种方式仅仅是数据库层面,通过数据库加唯一键约束能有效避免数据库重复插入相同数据。但无法阻止恶意用户重复提交表单(攻击网站),服务器大量执行sql插入语句,增加服务器和数据库负荷,并且这种方式不治本。
思路:
1. 自定义注解 @NoRepeatSubmit
标记所有Controller
中的提交请求
2. 通过AOP
对所有标记了 @NoRepeatSubmit
的方法拦截解析
3. 在业务方法执行前,获取当前用户的 token
(或者JSessionId
)+ 当前请求地址,作为一个唯一 KEY
,去获取 Redis
分布式锁(如果此时并发获取,只有一个线程会成功获取锁)
4. 业务方法执行后,释放锁
依据上述步骤,我们开始搭建Api接口幂等性解决方案:
1. 引入以下依赖
<parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.2.2.RELEASE</version> <relativePath/> </parent> <groupId>com.thinkingcao</groupId> <artifactId>springboot-aop-annotation</artifactId> <version>0.0.1-SNAPSHOT</version> <name>springboot-aop-annotation</name> <description>Demo project for Spring Boot</description> <!--编码设置--> <properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding> <java.version>1.8</java.version> </properties> <dependencies> <!-- 引入Web组件 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!-- 引入热加载工具 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-devtools</artifactId> <scope>runtime</scope> <optional>true</optional> </dependency> <!-- 引入Aop组件--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-aop</artifactId> </dependency> <!-- 引入mybatis组件 --> <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>2.1.1</version> </dependency> <!-- 引入Redis组件 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency> <!-- 引入Lombok代码简化插件--> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> <!-- 引入mysql链接驱动 --> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> </dependency> <!--druid连接池--> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid-spring-boot-starter</artifactId> <version>1.1.9</version> </dependency> <!--引入spring-jdbc组件 --> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-jdbc</artifactId> </dependency> <!-- commons-lang组件 --> <dependency> <groupId>commons-lang</groupId> <artifactId>commons-lang</artifactId> <version>2.6</version> </dependency> <!-- alibaba json解析工具类 --> <dependency> <groupId>com.alibaba</groupId> <artifactId>fastjson</artifactId> <version>1.2.47</version> </dependency> <!-- 引入测试组件 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> <exclusions> <exclusion> <groupId>org.junit.vintage</groupId> <artifactId>junit-vintage-engine</artifactId> </exclusion> </exclusions> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build>
spring: datasource: #数据库连接配置 url: jdbc:mysql://localhost:3306/springboot-aop-annotation?serverTimezone=GMT%2B8&useUnicode=true&characterEncoding=utf-8&useSSL=true #数据库连接账号 username: root #数据库连接密码 password: 123456 #数据库连接驱动 driver-class-name: com.mysql.cj.jdbc.Driver #连接池配置 druid: #初始化时建立物理连接的个数。初始化发生在显示调用init方法,或者第一次getConnection时 initialSize: 1 #最小连接池数量 minIdle: 1 #最大连接池数量 maxActive: 20 #获取连接时最大等待时间,单位毫秒。 maxWait: 60000 #配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒 timeBetweenEvictionRunsMillis: 60000 #配置一个连接在池中最小生存的时间,单位是毫秒 minEvictableIdleTimeMillis: 300000 #验证数据库连接的查询语句 validationQuery: SELECT 1 testWhileIdle: true testOnBorrow: true testOnReturn: false #Redis连接配置 redis: #Redis数据库索引(默认为0) database: 1 #Redis服务器地址 host: 127.0.0.1 #Redis服务器连接端口 port: 6379 #Redis服务器连接密码(默认为空) password: jedis: pool: #连接池最大连接数(使用负值表示没有限制) max-active: 8 #连接池最大阻塞等待时间(使用负值表示没有限制) max-wait: -1 #连接池中的最大空闲连接 max-idle: 8 #连接池中的最小空闲连接 min-idle: 0 # 连接超时时间(毫秒) timeout: 10000
package com.thinkingcao.springboot.aop.annotation;
import java.lang.annotation.*;
/**
* @desc: 对解决接口幂等性、网络延迟、表单重复提交的注解的封装
* type表示token获取方式
* @author: cao_wencao
* @date: 2019-12-17 22:01
*/
@Documented
@Inherited
@Target(value = {ElementType.METHOD,ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface NoRepeatSubmit {
String type() default "";
}
注意: 项目中引用了Redis,需要编写redis配置类,使用RedisTemplate需要注入Bean到Spring容器,类似Jedis
package com.thinkingcao.springboot.aop.config; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.data.redis.connection.RedisConnectionFactory; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer; import org.springframework.data.redis.serializer.StringRedisSerializer; import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder; /** * @desc: redis配置类,使用RedisTemplate需要注入Bean到Spring容器,类似Jedis * @author: cao_wencao * @date: 2019-12-13 12:17 */ @Configuration public class RedisConfig { @Bean public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory, Jackson2ObjectMapperBuilder jackson2ObjectMapperBuilder) { RedisTemplate<String, Object> template = new RedisTemplate<String, Object>(); template.setConnectionFactory(redisConnectionFactory); Jackson2JsonRedisSerializer<Object> jsonRedisSerializer = new Jackson2JsonRedisSerializer<>(Object.class); jsonRedisSerializer.setObjectMapper(jackson2ObjectMapperBuilder.build()); StringRedisSerializer stringSerializer = new StringRedisSerializer(); //=======设置下key和value的序列化方式,避免出现二进制数据显示========= //key采用String的序列化方式 template.setKeySerializer(stringSerializer); //hash的key也采用String的序列化方式 template.setValueSerializer(jsonRedisSerializer); //value序列化方式采用jackson template.setHashKeySerializer(stringSerializer); //hash的value序列化方式采用jackson template.setHashValueSerializer(jsonRedisSerializer); template.afterPropertiesSet(); return template; } }
以订单(Order)为例,作为本次文章研究测试的例子
package com.thinkingcao.springboot.aop.entity; import lombok.Data; import java.io.Serializable; /** * @desc: 订单实体类 * @author: cao_wencao * @date: 2019-12-05 14:22 */ @Data public class Order implements Serializable { private int orderId; //订单编号id private double orderMoney; //订单金额 private String receiverAddress; //收货地址 private String receiverName; //收货姓名 private String receiverPhone; //手机号 }
订单接口,写一个新增订单的例子
package com.thinkingcao.springboot.aop.mapper; import com.thinkingcao.springboot.aop.entity.Order; import org.apache.ibatis.annotations.Insert; import org.apache.ibatis.annotations.Mapper; import org.apache.ibatis.annotations.Options; /** * @desc: 订单mapper * @auth: cao_wencao * @date: 2019/12/19 22:20 */ public interface OrderMapper { @Insert("INSERT INTO `t_order` (order_id,order_money,receiver_address,receiver_name,receiver_phone) VALUES (#{orderId},#{orderMoney},#{receiverAddress},#{receiverName},#{receiverPhone});") @Options(keyProperty="order.orderId",keyColumn="order_id",useGeneratedKeys=true) public int addOrder(Order order); }
因为做测试,这里Service层就不封装了,直接Controller层调Ordermapper接口即可;
package com.thinkingcao.springboot.aop.controller; import com.thinkingcao.springboot.aop.annotation.NoRepeatSubmit; import com.thinkingcao.springboot.aop.entity.Order; import com.thinkingcao.springboot.aop.mapper.OrderMapper; import com.thinkingcao.springboot.aop.result.ResponseCode; import com.thinkingcao.springboot.aop.service.OrderService; import com.thinkingcao.springboot.aop.utils.Constant; import com.thinkingcao.springboot.aop.utils.RedisUtil; import com.thinkingcao.springboot.aop.utils.TokenUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; /** * @desc: 订单Controller * @author: cao_wencao * @date: 2019-12-18 0:01 */ @RestController public class OrderController { @Autowired private OrderMapper orderMapper; @Autowired private RedisUtil redisUtil; @RequestMapping(value = "/getToken") public ResponseCode getToken(){ String token = TokenUtils.getToken(); //将生成的token存进redis key:token value: token time : 30分钟 redisUtil.set(token, token, redisUtil.TOKEN_EXPIRE_TIME); return ResponseCode.success("获取token成功",token); } /** * @desc: 新增订单 * @auth: cao_wencao * @date: 2019/12/19 22:27 */ @RequestMapping(value = "/addOrder", produces = "application/json; charset=utf-8") @NoRepeatSubmit(type = Constant.EXTAPIHEAD) public ResponseCode addOrder(@RequestBody Order order) { int result = orderMapper.addOrder(order); if (result > 0) { return ResponseCode.success("添加成功!"); } return ResponseCode.error("添加失败!"); } }
封装一个ResponseCode工具类用来返回Api接口统一JSON格式数据;
package com.thinkingcao.springboot.aop.result; import lombok.AllArgsConstructor; import lombok.Data; import java.util.HashMap; /** * @desc: 接口响应工具类 * @auth: cao_wencao * @date: 2019/11/21 18:00 */ @Data @AllArgsConstructor public class ResponseCode extends HashMap<String, Object> { private static final long serialVersionUID = 1L; public static final String CODE_TAG = "code"; public static final String MSG_TAG = "msg"; public static final String DATA_TAG = "data"; /** * 状态类型 */ public enum Type { /** 成功 */ SUCCESS(200), /** 警告 */ WARN(400), /** 错误 */ ERROR(500); private final int value; Type(int value) { this.value = value; } public int value() { return this.value; } } /** 状态类型 */ private Type type; /** 状态码 */ private int code; /** 返回内容 */ private String msg; /** 数据对象 */ private Object data; /** * 初始化一个新创建的 AjaxResult 对象 * * @param type 状态类型 * @param msg 返回内容 */ public ResponseCode(Type type, String msg) { super.put(CODE_TAG, type.value); super.put(MSG_TAG, msg); } /** * 初始化一个新创建的 AjaxResult 对象 * * @param type 状态类型 * @param msg 返回内容 * @param data 数据对象 */ public ResponseCode(Type type, String msg, Object data) { super.put(CODE_TAG, type.value); super.put(MSG_TAG, msg); if (data !=null) { super.put(DATA_TAG, data); } } /** * 返回成功消息 * * @return 成功消息 */ public static ResponseCode success() { return ResponseCode.success("操作成功"); } /** * 返回成功数据 * * @return 成功消息 */ public static ResponseCode success(Object data) { return ResponseCode.success("操作成功", data); } /** * 返回成功消息 * * @param msg 返回内容 * @return 成功消息 */ public static ResponseCode success(String msg) { return ResponseCode.success(msg, null); } /** * 返回成功消息 * * @param msg 返回内容 * @param data 数据对象 * @return 成功消息 */ public static ResponseCode success(String msg, Object data) { return new ResponseCode(Type.SUCCESS, msg, data); } /** * 返回警告消息 * * @param msg 返回内容 * @return 警告消息 */ public static ResponseCode warn(String msg) { return ResponseCode.warn(msg, null); } /** * 返回警告消息 * * @param msg 返回内容 * @param data 数据对象 * @return 警告消息 */ public static ResponseCode warn(String msg, Object data) { return new ResponseCode(Type.WARN, msg, data); } /** * 返回错误消息 * * @return */ public static ResponseCode error() { return ResponseCode.error("操作失败"); } /** * 返回错误消息 * * @param msg 返回内容 * @return 警告消息 */ public static ResponseCode error(String msg) { return ResponseCode.error(msg, null); } /** * 返回错误消息 * * @param msg 返回内容 * @param data 数据对象 * @return 警告消息 */ public static ResponseCode error(String msg, Object data) { return new ResponseCode(Type.ERROR, msg, data); } }
1. Constant.java
package com.thinkingcao.springboot.aop.utils;
/**
* @desc: 表明从请求头取token或者从参数取,二选一
* @author: cao_wencao
* @date: 2019-12-17 23:46
*/
public interface Constant {
public static final String EXTAPIHEAD = "head";
public static final String EXTAPIFROM = "from";
}
2. HttpServletRequestUtil .java
package com.thinkingcao.springboot.aop.utils; import org.springframework.web.context.request.RequestContextHolder; import org.springframework.web.context.request.ServletRequestAttributes; import javax.servlet.http.HttpServletRequest; /** * @desc: 获取当前请求的HttpServletRequest对象 * @author: cao_wencao * @date: 2019-12-17 23:16 */ public class HttpServletRequestUtil { public static HttpServletRequest getRequest() { ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); HttpServletRequest request = attributes.getRequest(); return request; } }
3. HttpServletResponseUtil.java
package com.thinkingcao.springboot.aop.utils; import org.springframework.web.context.request.RequestContextHolder; import org.springframework.web.context.request.ServletRequestAttributes; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.io.PrintWriter; /** * @desc: * @author: cao_wencao * @date: 2019-12-17 23:18 */ public class HttpServletResponseUtil { public static void response(String msg) throws IOException { ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); HttpServletResponse response = attributes.getResponse(); response.setHeader("Content-type", "text/html;charset=UTF-8"); PrintWriter writer = response.getWriter(); try { writer.println(msg); } catch (Exception e) { } finally { writer.close(); } } }
4. RedisUtil.java
package com.thinkingcao.springboot.aop.utils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.stereotype.Component; import org.springframework.util.CollectionUtils; import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.TimeUnit; /** * @desc: redis工具类,使用@Component注解将RedisUtils交给Spring容器实例化,使用时直接注解注入即可。 * @author: cao_wencao * @date: 2019-12-13 14:04 */ @Component public class RedisUtil { // 默认缓存时间 public static final Long TOKEN_EXPIRE_TIME = 30 * 60L; @Autowired private RedisTemplate<String, Object> redisTemplate; // ============================common============================= /** * 指定缓存失效时间 * * @param key 键 * @param time 时间(秒) * @return */ public boolean expire(String key, long time) { try { if (time > 0) { redisTemplate.expire(key, time, TimeUnit.SECONDS); } return true; } catch (Exception e) { e.printStackTrace(); return false; } } /** * 根据key 获取过期时间 * * @param key 键 不能为null * @return 时间(秒) 返回0代表为永久有效 */ public long getExpire(String key) { return redisTemplate.getExpire(key, TimeUnit.SECONDS); } /** * 判断key是否存在 * * @param key 键 * @return true 存在 false不存在 */ public boolean hasKey(String key) { try { return redisTemplate.hasKey(key); } catch (Exception e) { e.printStackTrace(); return false; } } /** * 删除缓存 * * @param key 可以传一个值 或多个 */ @SuppressWarnings("unchecked") public boolean del(String... key) { if (key != null && key.length > 0) { if (key.length == 1) { redisTemplate.delete(key[0]); } else { redisTemplate.delete(CollectionUtils.arrayToList(key)); } return true; } return false; } /** * 普通缓存获取 * * @param key 键 * @return 值 */ public Object get(String key) { return key == null ? null : redisTemplate.opsForValue().get(key); } /** * 普通缓存放入 * * @param key 键 * @param value 值 * @return true成功 false失败 */ public boolean set(String key, Object value) { try { redisTemplate.opsForValue().set(key, value); return true; } catch (Exception e) { e.printStackTrace(); return false; } } /** * 普通缓存放入并设置时间 * * @param key 键 * @param value 值 * @param time 时间(秒) time要大于0 如果time小于等于0 将设置无限期 * @return true成功 false 失败 */ public boolean set(String key, Object value, long time) { try { if (time > 0) { redisTemplate.opsForValue().set(key, value, time, TimeUnit.SECONDS); } else { set(key, value); } return true; } catch (Exception e) { e.printStackTrace(); return false; } } }
5. TokenUtils.java
package com.thinkingcao.springboot.aop.utils; import sun.misc.BASE64Encoder; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.util.UUID; /** * @desc: token工具类 * @auth: cao_wencao * @date: 2019/12/16 13:54 */ public class TokenUtils { private static final String MEMBER_TOKEN = "member_token"; //token生成 public static String getToken() { String tokenStr = UUID.randomUUID().toString().replace("-", "");; BASE64Encoder base64 = new BASE64Encoder(); String token = base64.encode(tokenStr.getBytes()); return token; } public static void main(String[] args) throws NoSuchAlgorithmException { for (int i = 0; i < 100; i++) { //System.out.println(UUID.randomUUID().toString().replace("-", "")); System.out.println(TokenUtils.getToken()); } } }
package com.thinkingcao.springboot.aop.aspect; import com.thinkingcao.springboot.aop.annotation.NoRepeatSubmit; import com.thinkingcao.springboot.aop.utils.Constant; import com.thinkingcao.springboot.aop.utils.HttpServletRequestUtil; import com.thinkingcao.springboot.aop.utils.HttpServletResponseUtil; import com.thinkingcao.springboot.aop.utils.RedisUtil; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang.StringUtils; import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.annotation.Around; import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.reflect.MethodSignature; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import javax.servlet.http.HttpServletRequest; /** * @desc: 表单重复提交AOP切面,解析注解@NoRepeatSubmit * @author: cao_wencao * @date: 2019-12-17 22:09 */ @Aspect @Component @Slf4j public class RepeatSubmitAop { //注入Redis工具类 @Autowired private RedisUtil redisUtil; //定义切入点, 拦截controller的所有请求 private final String POINTCUT = "execution(public * com.thinkingcao.springboot.aop.controller.*.*(..))"; //前置通知 /*@Before(POINTCUT) public void before(JoinPoint point) { MethodSignature signature = (MethodSignature) point.getSignature(); NoRepeatSubmit noRepeatSubmit = signature.getMethod().getDeclaredAnnotation(NoRepeatSubmit.class); if (null != noRepeatSubmit) { //获取type参数的value String typeValue = noRepeatSubmit.type(); } }*/ //环绕通知 @Around(POINTCUT) public Object doAround(ProceedingJoinPoint joinPoint) throws Throwable { //1. 使用AOP环绕通知拦截所有请求(controller) //POINTCUT //2. 判断方法上是否有@NoRepeatSubmit MethodSignature signature = (MethodSignature) joinPoint.getSignature(); NoRepeatSubmit noRepeatSubmit = signature.getMethod().getDeclaredAnnotation(NoRepeatSubmit.class); //3. 如果方法上有@NoRepeatSubmit if (null != noRepeatSubmit) { //获取type参数的value String typeValue = noRepeatSubmit.type(); String token = null; HttpServletRequest request = HttpServletRequestUtil.getRequest(); //如果存在header中,从头中获取 if (typeValue.equals(Constant.EXTAPIHEAD)) { token = request.getHeader("token"); } else { 否则从 请求参数获取 token = request.getParameter("token"); } if (StringUtils.isEmpty(token)) { HttpServletResponseUtil.response("参数错误!"); return null; } //如果redis中token不存在,则为重复提交 String redisToken = (String) redisUtil.get(token); if (StringUtils.isEmpty(redisToken)) { HttpServletResponseUtil.response("请勿重复提交!"); return null; } //redis不为空,则为第一次请求 redisUtil.del(redisToken); } //放行 Object proceed = joinPoint.proceed(); return proceed; } }
注意: 注解式mybatis的sql需要加上@MapperScan扫描,xml形式的mybatis的sql不需要
package com.thinkingcao.springboot.aop; import org.mybatis.spring.annotation.MapperScan; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; /** * @desc: 启动类; 注意: 注解式mybatis的sql需要加上@MapperScan扫描,xml形式的mybatis的sql不需要 * @auth: cao_wencao * @date: 2019/12/20 17:14 */ @SpringBootApplication @MapperScan(value = "com.thinkingcao.springboot.aop.mapper") public class SpringbootAopApplication { public static void main(String[] args) { SpringApplication.run(SpringbootAopApplication.class, args); } }
先调用接口获取token: http://127.0.0.1:8080/getToken
获取token的同时将token存入了redis,打开RedisDesktopManager工具查看下:
RedisDesktopManager下载地址: https://github.com/uglide/RedisDesktopManager/releases
先调用新增订单接口: http://127.0.0.1:8080/addOrder
注意以下两点:
1. 请求头需要带上token值
2. Post请求JSON数据:
{
“orderId”:10,
“orderMoney”:1.0,
“receiverAddress”:“上海”,
“receiverName”:“曹”,
“receiverPhone”:“13027180989”
}
在测试新增订单之前,数据库的数据:
然后来调用请求订单接口:
在测试新增订单之后,数据库的数据:
在测试新增订单之后,Redis中存储的token值已经被删除了,如图:
然后我们短时间内立即将上述新增订单的请求再次请求一次,会显示新增不成功,token值不存在,这样就达到了类似Form表单请求重复提交问题
源码: https://github.com/Thinkingcao/SpringBootLearning/tree/master/springboot-aop-annotation
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。