表單重複提價問題html
rpc遠程調用時候 發生網絡延遲 可能有重試機制java
MQ消費者冪等(保證惟一)同樣 mysql
解決方案: tokenweb
令牌 保證惟一的而且是臨時的 過一段時間失效redis
分佈式: redis+tokenspring
注意在getToken() 這種方法代碼必定要上鎖 保證只有一個線程執行 不然會形成token不惟一sql
步驟 調用接口以前生成對應的 token,存放在redis中apache
調用接口的時候,將該令牌放到請求頭中 (獲取請求頭中的令牌)json
接口獲取對應的令牌,若是可以獲取該令牌 (將當前令牌刪除掉),執行該方法業務邏輯 緩存
若是獲取不到對應的令牌。返回提示「老鐵 不要重複提交」
哈哈 若是別人得到了你的token 而後拿去作壞事,採用機器模擬去攻擊。這時候咱們要用驗證碼來搞定。
從代碼開發者的角度看,若是每次請求都要 獲取token 而後進行一統校驗。代碼冗餘啊。若是一百個接口 要寫一百次
因此採用AOP的方式進行開發,經過註解方式。
若是過濾器的話,全部接口都進行了校驗。
框架開發:
自定義一個註解@ 做爲標記
若是哪一個Controller須要進行token的驗證加上註解標記
在執行代碼時候AOP經過切面類中 寫的 做用接口進行 判斷,若是這個接口方法有 自定義的@註解 那麼進行校驗邏輯
校驗結果 要麼提示給用戶 「請勿提交」 要麼經過驗證 繼續往下執行代碼
關於表單重複提交:
在表單有個隱藏域 存放token 使用 getParameter 去獲取token 而後經過返回的結果進行校驗
注意 獲取token的這個代碼 也是用AOP去解決,實現。 不然每一個Controller類都寫這段代碼就冗餘了。前置通知搞定
註解:
首先pom:
<parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.0.0.RELEASE</version> </parent> <dependencies> <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>1.1.1</version> </dependency> <!-- mysql 依賴 --> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> </dependency> <!-- SpringBoot 對lombok 支持 --> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> </dependency> <!-- SpringBoot web 核心組件 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-tomcat</artifactId> </dependency> <!-- SpringBoot 外部tomcat支持 --> <dependency> <groupId>org.apache.tomcat.embed</groupId> <artifactId>tomcat-embed-jasper</artifactId> </dependency> <!-- springboot-log4j --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-log4j</artifactId> <version>1.3.8.RELEASE</version> </dependency> <!-- springboot-aop 技術 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-aop</artifactId> </dependency> <!-- https://mvnrepository.com/artifact/commons-lang/commons-lang --> <dependency> <groupId>commons-lang</groupId> <artifactId>commons-lang</artifactId> <version>2.6</version> </dependency> <!-- https://mvnrepository.com/artifact/org.apache.httpcomponents/httpclient --> <dependency> <groupId>org.apache.httpcomponents</groupId> <artifactId>httpclient</artifactId> </dependency> <!-- https://mvnrepository.com/artifact/com.alibaba/fastjson --> <dependency> <groupId>com.alibaba</groupId> <artifactId>fastjson</artifactId> <version>1.2.47</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency> <dependency> <groupId>javax.servlet</groupId> <artifactId>jstl</artifactId> </dependency> <dependency> <groupId>taglibs</groupId> <artifactId>standard</artifactId> <version>1.1.2</version> </dependency> </dependencies>
二、關於表單提交的註解的封裝
import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; @Target(value = ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface ExtApiIdempotent { String value(); }
AOP:
import java.io.IOException; import java.io.PrintWriter; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.apache.commons.lang.StringUtils; import org.aspectj.lang.JoinPoint; import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.annotation.Around; import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.Before; import org.aspectj.lang.annotation.Pointcut; import org.aspectj.lang.reflect.MethodSignature; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import org.springframework.web.context.request.RequestContextHolder; import org.springframework.web.context.request.ServletRequestAttributes; import com.itmayeidu.ext.ExtApiIdempotent; import com.itmayeidu.ext.ExtApiToken; import com.itmayeidu.utils.ConstantUtils; import com.itmayeidu.utils.RedisTokenUtils; import com.itmayeidu.utils.TokenUtils; @Aspect @Component public class ExtApiAopIdempotent { @Autowired private RedisTokenUtils redisTokenUtils; //須要做用的類 @Pointcut("execution(public * com.itmayiedu.controller.*.*(..))") public void rlAop() { } // 前置通知轉發Token參數 進行攔截的邏輯 @Before("rlAop()") public void before(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) throws Throwable { MethodSignature signature = (MethodSignature) proceedingJoinPoint.getSignature(); ExtApiIdempotent extApiIdempotent = signature.getMethod().getDeclaredAnnotation(ExtApiIdempotent.class); if (extApiIdempotent != null) { //有註解的狀況 有註解的說明須要進行token校驗 return extApiIdempotent(proceedingJoinPoint, signature); } // 放行 Object proceed = proceedingJoinPoint.proceed(); //放行 正常執行後面(Controller)的業務邏輯 return proceed; } // 驗證Token 方法的封裝 public Object extApiIdempotent(ProceedingJoinPoint proceedingJoinPoint, MethodSignature signature) throws Throwable { ExtApiIdempotent extApiIdempotent = signature.getMethod().getDeclaredAnnotation(ExtApiIdempotent.class); if (extApiIdempotent == null) { // 直接執行程序 Object proceed = proceedingJoinPoint.proceed(); return proceed; } // 代碼步驟: // 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(); return proceed; } public void extApiToken() { String token = redisTokenUtils.getToken(); getRequest().setAttribute("token", token); } public HttpServletRequest getRequest() { ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); HttpServletRequest request = attributes.getRequest(); return request; } public 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(); } } }
訂單請求接口:
import javax.servlet.http.HttpServletRequest; import org.apache.commons.lang.StringUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import com.itmayeidu.ext.ExtApiIdempotent; import com.itmayeidu.utils.ConstantUtils; import com.itmayeidu.utils.RedisTokenUtils; import com.itmayeidu.utils.TokenUtils; import com.itmayiedu.entity.OrderEntity; import com.itmayiedu.mapper.OrderMapper; @RestController public class OrderController { @Autowired private OrderMapper orderMapper; @Autowired private RedisTokenUtils redisTokenUtils; // 從redis中獲取Token @RequestMapping("/redisToken") public String RedisToken() { return redisTokenUtils.getToken(); } // 驗證Token @RequestMapping(value = "/addOrderExtApiIdempotent", produces = "application/json; charset=utf-8") @ExtApiIdempotent(value = ConstantUtils.EXTAPIHEAD) public String addOrderExtApiIdempotent(@RequestBody OrderEntity orderEntity, HttpServletRequest request) { int result = orderMapper.addOrder(orderEntity); return result > 0 ? "添加成功" : "添加失敗" + ""; } }
表單提交的請求接口:
import javax.servlet.http.HttpServletRequest; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; import com.itmayeidu.ext.ExtApiIdempotent; import com.itmayeidu.ext.ExtApiToken; import com.itmayeidu.utils.ConstantUtils; import com.itmayiedu.entity.OrderEntity; import com.itmayiedu.mapper.OrderMapper; @Controller public class OrderPageController { @Autowired private OrderMapper orderMapper; @RequestMapping("/indexPage") @ExtApiToken public String indexPage(HttpServletRequest req) { return "indexPage"; } @RequestMapping("/addOrderPage") @ExtApiIdempotent(value = ConstantUtils.EXTAPIFROM) public String addOrder(OrderEntity orderEntity) { int addOrder = orderMapper.addOrder(orderEntity); return addOrder > 0 ? "success" : "fail"; } }
utils:
redis:
import java.util.concurrent.TimeUnit; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.stereotype.Component; @Component public class BaseRedisService { @Autowired private StringRedisTemplate stringRedisTemplate; public void setString(String key, Object data, Long timeout) { if (data instanceof String) { String value = (String) data; stringRedisTemplate.opsForValue().set(key, value); } if (timeout != null) { stringRedisTemplate.expire(key, timeout, TimeUnit.SECONDS); } } public Object getString(String key) { return stringRedisTemplate.opsForValue().get(key); } public void delKey(String key) { stringRedisTemplate.delete(key); } }
常量:
public interface ConstantUtils { static final String EXTAPIHEAD = "head"; static final String EXTAPIFROM = "from"; }
mvc:
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.config.annotation.EnableWebMvc; import org.springframework.web.servlet.view.InternalResourceViewResolver; import org.springframework.web.servlet.view.JstlView; @Configuration @EnableWebMvc @ComponentScan("com.too5.controller") public class MyMvcConfig { @Bean // 出現問題緣由 @bean 忘記添加 public InternalResourceViewResolver viewResolver() { InternalResourceViewResolver viewResolver = new InternalResourceViewResolver(); viewResolver.setPrefix("/WEB-INF/jsp/"); viewResolver.setSuffix(".jsp"); viewResolver.setViewClass(JstlView.class); return viewResolver; } }
redis操做token工具類:
import org.apache.commons.lang.StringUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; @Component public class RedisTokenUtils { private long timeout = 60 * 60; //超時時間 @Autowired private BaseRedisService baseRedisService; // 將token存入在redis public String getToken() { String token = "token" + System.currentTimeMillis(); baseRedisService.setString(token, token, timeout); //key: token value: token 時間 return token; } public synchronize boolean findToken(String tokenKey) { //從redis查詢對應的token 防止沒來得及刪除 只有一個線程操做 其實redis已經能夠防止了 String token = (String) baseRedisService.getString(tokenKey); if (StringUtils.isEmpty(token)) { //要麼被被人使用過了 要麼沒有對應token return false; } // token 獲取成功後 刪除對應tokenMapstoken baseRedisService.delKey(token); return true; //保證每一個接口對應的token只能訪問一次,保證接口冪等性問題 } }
tokenutils:
import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import org.apache.commons.lang.StringUtils; public class TokenUtils { private static Map<String, Object> tokenMaps = new ConcurrentHashMap<String, Object>(); // 1.什麼Token(令牌) 表示是一個零時不容許有重複相同的值(臨時且惟一) // 2.使用令牌方式防止Token重複提交。 // 使用場景:在調用第API接口的時候,須要傳遞令牌,該Api接口 獲取到令牌以後,執行當前業務邏輯,讓後把當前的令牌刪除掉。 // 在調用第API接口的時候,須要傳遞令牌 建議15-2小時 // 代碼步驟: // 1.獲取令牌 // 2.判斷令牌是否在緩存中有對應的數據 // 3.如何緩存沒有該令牌的話,直接報錯(請勿重複提交) // 4.如何緩存有該令牌的話,直接執行該業務邏輯 // 5.執行完業務邏輯以後,直接刪除該令牌。 // 獲取令牌 public static synchronized String getToken() { // 如何在分佈式場景下使用分佈式全局ID實現 String token = "token" + System.currentTimeMillis(); // hashMap好處能夠附帶 tokenMaps.put(token, token); return token; } // generateToken(); public static boolean findToken(String tokenKey) { // 判斷該令牌是否在tokenMap 是否存在 String token = (String) tokenMaps.get(tokenKey); if (StringUtils.isEmpty(token)) { return false; } // token 獲取成功後 刪除對應tokenMapstoken tokenMaps.remove(token); return true; } }
實體類:
public class OrderEntity { private int id; private String orderName; private String orderDes; public int getId() { return id; } public void setId(int id) { this.id = id; } public String getOrderName() { return orderName; } public void setOrderName(String orderName) { this.orderName = orderName; } public String getOrderDes() { return orderDes; } public void setOrderDes(String orderDes) { this.orderDes = orderDes; } }
public class UserEntity { private Long id; private String userName; private String password; public Long getId() { return id; } public void setId(Long id) { this.id = id; } public String getUserName() { return userName; } public void setUserName(String userName) { this.userName = userName; } public String getPassword() { return password; } public void setPassword(String password) { this.password = password; } @Override public String toString() { return "UserEntity [id=" + id + ", userName=" + userName + ", password=" + password + "]"; } }
Mapper:
import org.apache.ibatis.annotations.Insert; import com.itmayiedu.entity.OrderEntity; public interface OrderMapper { @Insert("insert order_info values (null,#{orderName},#{orderDes})") public int addOrder(OrderEntity OrderEntity); }
public interface UserMapper { @Select(" SELECT * FROM user_info where userName=#{userName} and password=#{password}") public UserEntity login(UserEntity userEntity); @Insert("insert user_info values (null,#{userName},#{password})") public int insertUser(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 @ServletComponentScan public class AppB { public static void main(String[] args) { SpringApplication.run(AppB.class, args); } }
總結:
核心就是
自定義註解
controller中的方法註解
aop切面類判斷對象是否有相應的註解 若是有 從parameter或者header獲取參數 進行校驗