Spring boot AOP 同步锁 防止表单重复提交(严格测试)

本文介绍如何使用SpringBoot AOP防止表单重复提交,包括单块系统中利用Session和分布式系统中利用Redis实现的方法。通过注解、拦截器和token机制确保请求唯一性。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

Spring boot AOP 同步锁 防止表单重复提交(严格测试)

防止表单重复提交,一直是软件架构的基础,网上的思路经严格测试, 多不靠谱, 现提供两种思路。
单块系统:利用Session机制实现。
分布式系统:利用redis缓存机制实现。

先上图看看效果

不上锁.效果截图 , 7次 请求 6次 成功.
同步锁防止表单重复提交
上锁效果截图, 7次 请求只有 1次 成功.
同步锁防止表单重复提交

单块系统

实现思路:
表单加载后,通过ajax获取token,设置到session,并填写到表单hidden token中
表单提交时,携带token参数
利用aop拦截器校验是否携带此token
携带token是否与session中token值相同,不同抛异常,相同从session移除.

1.pom.xml导入AOP

<!-- aop -->
<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-aop</artifactId>
</dependency>

2.编写注解 TokenCheck

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
 * 避免重复提交
 * 
 * @author 冯晓东
 */
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface TokenCheck {

}

3.定义@Aspect拦截器

import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;

import com.fxd.cachetest.util.WebUtil;

/**
 * 重复提交aop
 * 
 * @author 冯晓东
 */
@Aspect
@Component
public class TokenAspect {

	private static final Logger logger = LoggerFactory.getLogger(TokenAspect.class);

    /**
	 * @param jp
	 * 
	 *           经测试,会按照单个浏览器,并行执行. 无需担心同步问题.
	 */
	@Before("@annotation(com.fxd.cachetest.base.token.TokenCheck)")
	public void before(JoinPoint jp) throws Throwable {
		String token = WebUtil.getRequest().getParameter("token");
		logger.info("token --> {}", token);
		if (token == null || "".equals(token.trim())) {
			throw new RuntimeException("500:Parameter <token> can not be null.");
		}
		if (!token.contains("@@")) {
			throw new RuntimeException("500:Parameter <token> is invalid key.");
		}

		String key = token.split("@@")[0];
		String snToken = WebUtil.getAsyncToken("token@@" + key);
		
		logger.info("session token --> {}", snToken);
		if (!token.equals(snToken)) {
			throw new RuntimeException("500:Token is invalid.");
		}

	}
}

4.页面表单加载前,通过 /get_token 获取token到隐藏域 “token”

/**
	 * 
	 * @param key
	 * @return
	 */
	@GetMapping("/get_token")
	public String get_token(String key) {
		if (key == null || "".equals(key.trim())) {
			throw new RuntimeException("500:Parameter <key> can not be null.");
		}

		// 设置token值
		String token = key + "@@" + UUID.randomUUID();
		WebUtil.getSession().setAttribute("token@@" + key, token);
		return token;
	}

<input type='hidden' name='token' />

5.提交表单时,加入@TokenCheck注解

@RestController
public class WebTest {

	@RequestMapping("/test")
	@TokenCheck
	public String test() {

		return new Timestamp(System.currentTimeMillis()).toString();
	}
}

分布式系统

思路是相同的,只是将值存储在redis中(设置合理时间)
通过 redisTemplate代替session实现.这里不再赘述.

详细代码可私聊我获取.

Spring Boot中,可以使用Token令牌来防止重复提交。 具体实现方式如下: 1. 在前端页面中,可以使用Token令牌来防止重复提交。在表单中添一个隐藏域,用于存储Token值。 ```html <form action="/demo" method="post"> <input type="hidden" name="token" value="${token}"> <input type="text" name="name"> <button type="submit">Submit</button> </form> ``` 2. 在Controller层方法中,判断Token是否有效,如果有效则将Token值删除,否则返回错误信息。可以通过拦截器或者AOP实现。 ```java @RestController public class DemoController { @PostMapping("/demo") @TokenAnnotation // 自定义注解,用于拦截器或者AOP public String demo(@RequestParam("token") String token) { // do something return "success"; } } ``` ```java @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD) public @interface TokenAnnotation { } ``` ```java @Component public class TokenInterceptor implements HandlerInterceptor { private static final ConcurrentHashMap<String, Object> TOKEN_MAP = new ConcurrentHashMap<>(); @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { if (handler instanceof HandlerMethod) { HandlerMethod handlerMethod = (HandlerMethod) handler; Method method = handlerMethod.getMethod(); if (method.isAnnotationPresent(TokenAnnotation.class)) { String token = request.getParameter("token"); if (StringUtils.isBlank(token) || !TOKEN_MAP.containsKey(token)) { throw new RuntimeException("Invalid Token!"); } TOKEN_MAP.remove(token); } } return true; } @Override public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception { if (handler instanceof HandlerMethod) { HandlerMethod handlerMethod = (HandlerMethod) handler; Method method = handlerMethod.getMethod(); if (method.isAnnotationPresent(TokenAnnotation.class)) { String token = UUID.randomUUID().toString(); TOKEN_MAP.put(token, new Object()); request.setAttribute("token", token); } } } } ``` 在拦截器中,可以在preHandle方法中判断Token是否有效,如果有效则将Token值删除,否则抛出异常。在postHandle方法中,可以为每个请求生成一个新的Token值,并将其添到请求的Attribute中。在Controller层方法中,通过自定义注解@TokenAnnotation来标识需要拦截的方法。当请求到达Controller层方法时,拦截器会对其进行拦截,判断Token是否有效。如果有效,则将Token值删除,否则抛出异常。
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

冯晓东x

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值