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实现.这里不再赘述.