SpringBoot中使用AOP+Redis校验重复提交

SpringBoot结合Redis处理重复提交

数据重复提交导致多次请求服务、入库,产生脏数据、冗余数据等情况。禁止重复提交使我们保证数据准确性及安全性的必要操作。

实际上,造成这种情况的场景不少:
1.网络波动:因为网络波动,造成重复请求。
2.用户的重复性操作:用户误操作,或者因为接口响应慢,而导致用户耐性消失,有意多次触发请求。
3.重试机制:这种情况,经常出现在调用三方接口的时候。对可能出现的异常情况抛弃,然后进行固定次数的接口重复调用,直到接口返回正常结果。
4.分布式消息消费:任务发布后,使用分布式消息服务来进行消费。

方案

方案:同一客户端2s内请求同样的url,即视为重复提交。
在请求进入业务方法之前,进入切面。以sessionId+url作为key,在redis中查询,看是否存在。存在即为重复提交。
若重复提交则抛出异常。否则正常处理业务逻辑。

AOP

AOP为Aspect Oriented Programming的缩写,意为:面向切面编程,通过预编译方式和运行期动态代理实现程序功能的统一维护的一种技术。AOP是Spring框架中的一个重要内容,它通过对既有程序定义一个切入点,然后在其前后切入不同的执行内容,比如常见的有:打开数据库连接/关闭数据库连接、打开事务/关闭事务、记录日志等。基于AOP不会破坏原来程序逻辑,因此它可以很好的对业务逻辑的各个部分进行隔离,从而使得业务逻辑各部分之间的耦合度降低,提高程序的可重用性,同时提高了开发的效率。

本文面向切面拦截的是类的元数据(包、类、方法名、参数等)
相对于拦截器更加细致,而且非常灵活,拦截器只能针对URL做拦截,而AOP针对具体的代码,能够实现更加复杂的业务逻辑。

自定义注解

自定义一个注解用于拦截标识(作用到方法上的注解)

/**
 * 防止重复提交注解
 */
@Target(ElementType.METHOD) //作用到方法上
@Retention(RetentionPolicy.RUNTIME)
public @interface NoRepeatSubmit
{
	String value() default "";
}

切面

AOP的默认配置属性,其中spring.aop.auto属性默认是开启的,也就是说只要引入了AOP依赖后,默认已经增加了@EnableAspectJAutoProxy。

@Component
@Aspect
public class NoRepeatSubmitAop {
	@Autowired
	TokenService tokenService;

	@Pointcut("execution(* com.example.demo9.*.*(..)) && @annotation(com.example.demo9.NoRepeatSubmit) && !execution(public * com.example.demo9.TestController.*(..))")
	public void verifyRequestToken()
	{
	}

	/***
	 * 校验是否为重复提交
	 * @param joinPoint
	 * @throws Exception
	 */
	@Before("verifyRequestToken()")
	public void executeVerify(JoinPoint joinPoint) throws Exception
	{
		ServletRequestAttributes attrubutes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
		String sessionId = RequestContextHolder.getRequestAttributes().getSessionId();
		HttpServletRequest request = attrubutes.getRequest();
		// 判断是否重复提交(使用sessionId+url机制)
		String key = sessionId + "-" + request.getServletPath();
		tokenService.checkTokenBySessionAndUrl(key);
	}
}

Controller

@RestController
public class TestController {
	@Autowired
	TokenService tokenService;
	
	@RequestMapping("/test")
	public String test(String token) throws Exception
	{
		tokenService.checkTokenBySessionAndUrl(token);
		return "test duplicate request";
	}
}

TokenServiceImpl

@Service
public class TokenServiceImpl implements TokenService {
	@Autowired
	StringRedisTemplate redisTemplate;
	
	@Override
	public void checkTokenBySessionAndUrl(String key) throws Exception
	{
		/**
		 * 提交的session+url已经存在redis 视为重复提交
		 * @param key
		 */
		if(StringUtils.isEmpty(key)){
			System.out.println("key 为空");
		}
		if(redisTemplate.opsForValue().get(key) == null)
		{
			//放入redis缓存,设置超时时间3s
			redisTemplate.opsForValue().setIfAbsent(key, key, 3, TimeUnit.SECONDS);
			System.out.println("key 放入redis缓存");
			
		}else {
			System.out.println("throw new Exception 重复提交");
			//throw new Exception("[Exception]重复提交");
		}
	}
}

Redis Cluster查询

[root@localhost ~]# redis-cli -c -h 192.168.236.128 -p 6379 -a 123456
192.168.236.128:6379> keys *
1) "token123"
192.168.236.128:6379> keys * (3秒后)
(empty list or set)


[root@localhost ~]# redis-cli -c -h 192.168.236.128 -p 6380 -a 123456
192.168.236.128:6380> keys * 
1) "token456"
192.168.236.128:6380> keys * (3秒后)
(empty list or set)

参考

springBoot+redis禁止重复提交
https://blog.csdn.net/wanglin3316/article/details/89186772

拦截器相关设置
https://blog.csdn.net/wanglin3316/article/details/89186772

Spring AOP中pointcut expression表达式解析 及匹配多个条件
https://www.cnblogs.com/rainy-shurun/p/5195439.html

RequestContextHolder简析
https://www.jianshu.com/p/80165b7743cf

你可能感兴趣的:(springboot)