表單重復提價問題
rpc遠程調用時候 發生網絡延遲 可能有重試機制
MQ消費者冪等(保證唯一)一樣
解決方案: token
令牌 保證唯一的並且是臨時的 過一段時間失效
分布式: redis+token
注意在getToken() 這種方法代碼一定要上鎖 保證只有一個線程執行 否則會造成token不唯一
步驟 調用接口之前生成對應的 token,存放在redis中
調用接口的時候,將該令牌放到請求頭中 (獲取請求頭中的令牌)
接口獲取對應的令牌,如果能夠獲取該令牌 (將當前令牌刪除掉),執行該方法業務邏輯
如果獲取不到對應的令牌。返回提示“老鐵 不要重復提交”
哈哈 如果別人獲得了你的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>
2、關於表單提交的注解的封裝
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獲取參數 進行校驗