表单重复提价问题
rpc远程调用时候 发生网络延迟 可能有重试机制
MQ消费者幂等(保证唯一)一样
解决方案: token
令牌 保证唯一的并且是临时的 过一段时间失效
分布式: redis+token
注意在getToken() 这种方法代码一定要上锁 保证只有一个线程执行 否则会造成token不唯一
步骤 调用接口之前生成对应的 token,存放在redis中
调用接口的时候,将该令牌放到请求头中 (获取请求头中的令牌)
接口获取对应的令牌,如果能够获取该令牌 (将当前令牌删除掉),执行该方法业务逻辑
如果获取不到对应的令牌。返回提示“老铁 不要重复提交”
哈哈 如果别人获得了你的token 然后拿去做坏事,采用机器模拟去攻击。这时候我们要用验证码来搞定。
从代码开发者的角度看,如果每次请求都要 获取token 然后进行一统校验。代码冗余啊。如果一百个接口 要写一百次
所以采用AOP的方式进行开发,通过注解方式。
如果过滤器的话,所有接口都进行了校验。
框架开发:
自定义一个注解@ 作为标记
如果哪个Controller需要进行token的验证加上注解标记
在执行代码时候AOP通过切面类中 写的 作用接口进行 判断,如果这个接口方法有 自定义的@注解 那么进行校验逻辑
校验结果 要么提示给用户 “请勿提交” 要么通过验证 继续往下执行代码
关于表单重复提交:
在表单有个隐藏域 存放token 使用 getParameter 去获取token 然后通过返回的结果进行校验
注意 获取token的这个代码 也是用AOP去解决,实现。 否则每个Controller类都写这段代码就冗余了。前置通知搞定
注解:
首先pom:
org.springframework.boot
spring-boot-starter-parent
2.0.0.RELEASE
org.mybatis.spring.boot
mybatis-spring-boot-starter
1.1.1
mysql
mysql-connector-java
org.projectlombok
lombok
org.springframework.boot
spring-boot-starter-web
org.springframework.boot
spring-boot-starter-tomcat
org.apache.tomcat.embed
tomcat-embed-jasper
org.springframework.boot
spring-boot-starter-log4j
1.3.8.RELEASE
org.springframework.boot
spring-boot-starter-aop
commons-lang
commons-lang
2.6
org.apache.httpcomponents
httpclient
com.alibaba
fastjson
1.2.47
org.springframework.boot
spring-boot-starter-data-redis
javax.servlet
jstl
taglibs
standard
1.1.2
2、关于表单提交的注解的封装
importjava.lang.annotation.ElementType;importjava.lang.annotation.Retention;importjava.lang.annotation.RetentionPolicy;importjava.lang.annotation.Target;
@Target(value=ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)public @interfaceExtApiIdempotent {
String value();
}
AOP:
importjava.io.IOException;importjava.io.PrintWriter;importjavax.servlet.http.HttpServletRequest;importjavax.servlet.http.HttpServletResponse;importorg.apache.commons.lang.StringUtils;importorg.aspectj.lang.JoinPoint;importorg.aspectj.lang.ProceedingJoinPoint;importorg.aspectj.lang.annotation.Around;importorg.aspectj.lang.annotation.Aspect;importorg.aspectj.lang.annotation.Before;importorg.aspectj.lang.annotation.Pointcut;importorg.aspectj.lang.reflect.MethodSignature;importorg.springframework.beans.factory.annotation.Autowired;importorg.springframework.stereotype.Component;importorg.springframework.web.context.request.RequestContextHolder;importorg.springframework.web.context.request.ServletRequestAttributes;importcom.itmayeidu.ext.ExtApiIdempotent;importcom.itmayeidu.ext.ExtApiToken;importcom.itmayeidu.utils.ConstantUtils;importcom.itmayeidu.utils.RedisTokenUtils;importcom.itmayeidu.utils.TokenUtils;
@Aspect
@Componentpublic classExtApiAopIdempotent {
@AutowiredprivateRedisTokenUtils redisTokenUtils;//需要作用的类
@Pointcut("execution(public * com.itmayiedu.controller.*.*(..))")public voidrlAop() {
}//前置通知转发Token参数 进行拦截的逻辑
@Before("rlAop()")public voidbefore(JoinPoint point) {//获取并判断类上是否有注解
MethodSignature signature = (MethodSignature) point.getSignature();//统一的返回值
ExtApiToken extApiToken = signature.getMethod().getDeclaredAnnotation(ExtApiToken.class);//参数是注解的那个
if (extApiToken != null) { //如果有注解的情况
extApiToken();
}
}//环绕通知验证参数
@Around("rlAop()")public Object doAround(ProceedingJoinPoint proceedingJoinPoint) throwsThrowable {
MethodSignature signature=(MethodSignature) proceedingJoinPoint.getSignature();
ExtApiIdempotent extApiIdempotent= signature.getMethod().getDeclaredAnnotation(ExtApiIdempotent.class);if (extApiIdempotent != null) { //有注解的情况 有注解的说明需要进行token校验
returnextApiIdempotent(proceedingJoinPoint, signature);
}//放行
Object proceed = proceedingJoinPoint.proceed(); //放行 正常执行后面(Controller)的业务逻辑
returnproceed;
}//验证Token 方法的封装
publicObject extApiIdempotent(ProceedingJoinPoint proceedingJoinPoint, MethodSignature signature)throwsThrowable {
ExtApiIdempotent extApiIdempotent= signature.getMethod().getDeclaredAnnotation(ExtApiIdempotent.class);if (extApiIdempotent == null) {//直接执行程序
Object proceed =proceedingJoinPoint.proceed();returnproceed;
}//代码步骤://1.获取令牌 存放在请求头中
HttpServletRequest request =getRequest();//value就是获取类型 请求头之类的
String valueType =extApiIdempotent.value();if(StringUtils.isEmpty(valueType)) {
response("参数错误!");return null;
}
String token= null;if (valueType.equals(ConstantUtils.EXTAPIHEAD)) { //如果存在header中 从头中获取
token = request.getHeader("token"); //从头中获取
} else{
token= request.getParameter("token"); //否则从 请求参数获取
}if(StringUtils.isEmpty(token)) {
response("参数错误!");return null;
}if (!redisTokenUtils.findToken(token)) {
response("请勿重复提交!");return null;
}
Object proceed=proceedingJoinPoint.proceed();returnproceed;
}public voidextApiToken() {
String token=redisTokenUtils.getToken();
getRequest().setAttribute("token", token);
}publicHttpServletRequest getRequest() {
ServletRequestAttributes attributes=(ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request=attributes.getRequest();returnrequest;
}public void response(String msg) throwsIOException {
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();
}
}
}
订单请求接口:
importjavax.servlet.http.HttpServletRequest;importorg.apache.commons.lang.StringUtils;importorg.springframework.beans.factory.annotation.Autowired;importorg.springframework.web.bind.annotation.RequestBody;importorg.springframework.web.bind.annotation.RequestMapping;importorg.springframework.web.bind.annotation.RestController;importcom.itmayeidu.ext.ExtApiIdempotent;importcom.itmayeidu.utils.ConstantUtils;importcom.itmayeidu.utils.RedisTokenUtils;importcom.itmayeidu.utils.TokenUtils;importcom.itmayiedu.entity.OrderEntity;importcom.itmayiedu.mapper.OrderMapper;
@RestControllerpublic classOrderController {
@AutowiredprivateOrderMapper orderMapper;
@AutowiredprivateRedisTokenUtils redisTokenUtils;//从redis中获取Token
@RequestMapping("/redisToken")publicString RedisToken() {returnredisTokenUtils.getToken();
}//验证Token
@RequestMapping(value = "/addOrderExtApiIdempotent", produces = "application/json; charset=utf-8")
@ExtApiIdempotent(value=ConstantUtils.EXTAPIHEAD)publicString addOrderExtApiIdempotent(@RequestBody OrderEntity orderEntity, HttpServletRequest request) {int result =orderMapper.addOrder(orderEntity);return result > 0 ? "添加成功" : "添加失败" + "";
}
}
表单提交的请求接口:
importjavax.servlet.http.HttpServletRequest;importorg.springframework.beans.factory.annotation.Autowired;importorg.springframework.stereotype.Controller;importorg.springframework.web.bind.annotation.RequestMapping;importcom.itmayeidu.ext.ExtApiIdempotent;importcom.itmayeidu.ext.ExtApiToken;importcom.itmayeidu.utils.ConstantUtils;importcom.itmayiedu.entity.OrderEntity;importcom.itmayiedu.mapper.OrderMapper;
@Controllerpublic classOrderPageController {
@AutowiredprivateOrderMapper orderMapper;
@RequestMapping("/indexPage")
@ExtApiTokenpublicString indexPage(HttpServletRequest req) {return "indexPage";
}
@RequestMapping("/addOrderPage")
@ExtApiIdempotent(value=ConstantUtils.EXTAPIFROM)publicString addOrder(OrderEntity orderEntity) {int addOrder =orderMapper.addOrder(orderEntity);return addOrder > 0 ? "success" : "fail";
}
}
utils:
redis:
importjava.util.concurrent.TimeUnit;importorg.springframework.beans.factory.annotation.Autowired;importorg.springframework.data.redis.core.StringRedisTemplate;importorg.springframework.stereotype.Component;
@Componentpublic classBaseRedisService {
@AutowiredprivateStringRedisTemplate stringRedisTemplate;public voidsetString(String key, Object data, Long timeout) {if (data instanceofString) {
String value=(String) data;
stringRedisTemplate.opsForValue().set(key, value);
}if (timeout != null) {
stringRedisTemplate.expire(key, timeout, TimeUnit.SECONDS);
}
}publicObject getString(String key) {returnstringRedisTemplate.opsForValue().get(key);
}public voiddelKey(String key) {
stringRedisTemplate.delete(key);
}
}
常量:
public interfaceConstantUtils {static final String EXTAPIHEAD = "head";static final String EXTAPIFROM = "from";
}
mvc:
importorg.springframework.context.annotation.Bean;importorg.springframework.context.annotation.ComponentScan;importorg.springframework.context.annotation.Configuration;importorg.springframework.web.servlet.config.annotation.EnableWebMvc;importorg.springframework.web.servlet.view.InternalResourceViewResolver;importorg.springframework.web.servlet.view.JstlView;
@Configuration
@EnableWebMvc
@ComponentScan("com.too5.controller")public classMyMvcConfig {
@Bean//出现问题原因 @bean 忘记添加
publicInternalResourceViewResolver viewResolver() {
InternalResourceViewResolver viewResolver= newInternalResourceViewResolver();
viewResolver.setPrefix("/WEB-INF/jsp/");
viewResolver.setSuffix(".jsp");
viewResolver.setViewClass(JstlView.class);returnviewResolver;
}
}
redis操作token工具类:
importorg.apache.commons.lang.StringUtils;importorg.springframework.beans.factory.annotation.Autowired;importorg.springframework.stereotype.Component;
@Componentpublic classRedisTokenUtils {private long timeout = 60 * 60; //超时时间
@AutowiredprivateBaseRedisService baseRedisService;//将token存入在redis
publicString getToken() {
String token= "token" +System.currentTimeMillis();
baseRedisService.setString(token, token, timeout); //key: token value: token 时间returntoken;
}public synchronize booleanfindToken(String tokenKey) { //从redis查询对应的token 防止没来得及删除 只有一个线程操作 其实redis已经可以防止了
String token=(String) baseRedisService.getString(tokenKey);if(StringUtils.isEmpty(token)) { //要么被被人使用过了 要么没有对应tokenreturn false;
}//token 获取成功后 删除对应tokenMapstoken
baseRedisService.delKey(token);return true; //保证每个接口对应的token只能访问一次,保证接口幂等性问题
}
}
tokenutils:
importjava.util.Map;importjava.util.concurrent.ConcurrentHashMap;importorg.apache.commons.lang.StringUtils;public classTokenUtils {private static Map tokenMaps = new ConcurrentHashMap();//1.什么Token(令牌) 表示是一个零时不允许有重复相同的值(临时且唯一)//2.使用令牌方式防止Token重复提交。//使用场景:在调用第API接口的时候,需要传递令牌,该Api接口 获取到令牌之后,执行当前业务逻辑,让后把当前的令牌删除掉。//在调用第API接口的时候,需要传递令牌 建议15-2小时//代码步骤://1.获取令牌//2.判断令牌是否在缓存中有对应的数据//3.如何缓存没有该令牌的话,直接报错(请勿重复提交)//4.如何缓存有该令牌的话,直接执行该业务逻辑//5.执行完业务逻辑之后,直接删除该令牌。//获取令牌
public static synchronizedString getToken() {//如何在分布式场景下使用分布式全局ID实现
String token = "token" +System.currentTimeMillis();//hashMap好处可以附带
tokenMaps.put(token, token);returntoken;
}//generateToken();
public static booleanfindToken(String tokenKey) {//判断该令牌是否在tokenMap 是否存在
String token =(String) tokenMaps.get(tokenKey);if(StringUtils.isEmpty(token)) {return false;
}//token 获取成功后 删除对应tokenMapstoken
tokenMaps.remove(token);return true;
}
}
实体类:
public classOrderEntity {private intid;privateString orderName;privateString orderDes;public intgetId() {returnid;
}public void setId(intid) {this.id =id;
}publicString getOrderName() {returnorderName;
}public voidsetOrderName(String orderName) {this.orderName =orderName;
}publicString getOrderDes() {returnorderDes;
}public voidsetOrderDes(String orderDes) {this.orderDes =orderDes;
}
}
public classUserEntity {privateLong id;privateString userName;privateString password;
publicLong getId() {returnid;
}
public voidsetId(Long id) {this.id =id;
}
publicString getUserName() {returnuserName;
}
public voidsetUserName(String userName) {this.userName =userName;
}
publicString getPassword() {returnpassword;
}
public voidsetPassword(String password) {this.password =password;
}@OverridepublicString toString() {return "UserEntity [id=" + id + ", userName=" + userName + ", password=" + password + "]";
}
}
Mapper:
importorg.apache.ibatis.annotations.Insert;importcom.itmayiedu.entity.OrderEntity;public interfaceOrderMapper {
@Insert("insert order_info values (null,#{orderName},#{orderDes})")public intaddOrder(OrderEntity OrderEntity);
}
public interfaceUserMapper {
@Select(" SELECT * FROM user_info where userName=#{userName} and password=#{password}")publicUserEntity login(UserEntity userEntity);
@Insert("insert user_info values (null,#{userName},#{password})")public intinsertUser(UserEntity userEntity);
}
yml:
spring:
mvc:
view:
# 页面默认前缀目录
prefix: /WEB-INF/jsp/
# 响应页面默认后缀
suffix: .jsp
spring:
datasource:
url: jdbc:mysql://localhost:3306/test?useUnicode=true&characterEncoding=UTF-8
username: root
password: root
driver-class-name: com.mysql.jdbc.Driver
test-while-idle: true
test-on-borrow: true
validation-query: SELECT 1 FROM DUAL
time-between-eviction-runs-millis: 300000
min-evictable-idle-time-millis: 1800000
redis:
database: 1
host: 106.15.185.133
port: 6379
password: meitedu.+@
jedis:
pool:
max-active: 8
max-wait: -1
max-idle: 8
min-idle: 0
timeout: 10000
domain:
name: www.toov5.com
启动类:
@MapperScan(basePackages = { "com.tov5.mapper"})
@SpringBootApplication
@ServletComponentScanpublic classAppB {public static voidmain(String[] args) {
SpringApplication.run(AppB.class, args);
}
}
总结:
核心就是
自定义注解
controller中的方法注解
aop切面类判断对象是否有相应的注解 如果有 从parameter或者header获取参数 进行校验