互聯網API開放平臺安全設計(二)--API接口冪等框架

API接口冪等性設計方案

MVCC方案

 多版本併發控制,該策略主要使用 update with condition(更新帶條件來防止)來保證多次外部請求調用對系統的影響是一致的。在系統設計的過程中,合理的使用樂觀鎖,通過 version 或者 updateTime(timestamp)等其他條件,來做樂觀鎖的判斷條件,這樣保證更新操作即使在併發的情況下,也不會有太大的問題。例如

  select * from tablename where condition=#condition# // 取出要跟新的對象,帶有版本 versoin

  update tableName set name=#name#,version=version+1 where version=#version#

 在更新的過程中利用 version 來防止,其他操作對對象的併發更新,導致更新丟失。爲了避免失敗,通常需要一定的重試機制。

去重表

在插入數據的時候,插入去重表,利用數據庫的唯一索引特性,保證唯一的邏輯。

悲觀鎖

select for update,整個執行過程中鎖定該訂單對應的記錄。注意:這種在 DB 讀大於寫的情況下儘量少用。

Token機制,防止頁面重複提交

業務要求:頁面的數據只能被點擊提交一次

發生原因:由於重複點擊或者網絡重發,或者 nginx 重發等情況會導致數據被重複提交

解決辦法:

集羣環境:採用 token 加 redis(redis 單線程的,處理需要排隊)

單 JVM 環境:採用 token 加 redis 或 token 加 jvm 內存

處理流程:

數據提交前要向服務的申請 token,token 放到 redis 或 jvm 內存,token 有效時間

提交後後臺校驗 token,同時刪除 token,生成新的 token 返回

token 特點:要申請,一次有效性,可以限流

基於Token方式防止API接口冪等

客戶端每次在調用接口的時候,需要在請求頭中,傳遞令牌參數,每次令牌只能用一次。

一旦使用之後,就會被刪除,這樣可以有效防止重複提交。

步驟:

1.生成令牌接口

2. 接口中獲取令牌驗證

生成令牌接口

public class TokenUtils {

	private static Map<String, Object> tokenMap = new ConcurrentHashMap<String, Object>();

	// 獲取token
	public static synchronized String getToken() {
		// 1.生成令牌
		String token = "token-" + System.currentTimeMillis();
		// 2.存入tokenMap
		tokenMap.put(token, token);
		return token;
	}

	// 驗證token,並且刪除對應的token
	public static Boolean exisToken(String token) {
		// 1.從集合中獲取token
		Object result = tokenMap.get(token);
		if (result == null) {
			return false;
		}
		// 2.刪除對應的token
		tokenMap.remove(token);
		return true;
	}
}

 接口中獲取令牌驗證

 

@RestController
public class OrderController {

	@Autowired
	private OrderMapper orderMapper;

	// 獲取Token
	@RequestMapping("/getToken")
	public String getToken() {
		return TokenUtils.getToken();
	}

	// 驗證Token
	@RequestMapping(value = "/addOrder", produces = "application/json; charset=utf-8")
	public String addOrder(@RequestBody OrderEntity orderEntity, HttpServletRequest request) {
		String token = request.getHeader("token");
		if (StringUtils.isEmpty(token)) {
			return "參數錯誤!";
		}
		if (!TokenUtils.exisToken(token)) {
			return "請勿重複提交!";
		}
		int result = orderMapper.addOrder(orderEntity);
		return result > 0 ? "添加成功" : "添加失敗" + "";
	}

}

 互聯網API接口冪等設計

BaseRedisService封裝Redis

@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);
	}

}

RedisTokenUtils工具類

@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);
		return token;
	}

	public boolean findToken(String tokenKey) {
		String token = (String) baseRedisService.getString(tokenKey);
		if (StringUtils.isEmpty(token)) {
			return false;
		}
		// token 獲取成功後 刪除對應tokenMapstoken
		baseRedisService.delKey(token);
		return true;
	}

}

自定義Api冪等註解和切面

@Target(value = ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface ExtApiIdempotent {
	String value();
}

 

@Aspect
@Component
public class ExtApiAopIdempotent {
	@Autowired
	private RedisTokenUtils redisTokenUtils;

	@Pointcut("execution(public * com.itmayiedu.controller.*.*(..))")
	public void rlAop() {
	}

	@Around("rlAop()")
	public Object doBefore(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
		MethodSignature signature = (MethodSignature) proceedingJoinPoint.getSignature();
		ExtApiIdempotent extApiIdempotent = signature.getMethod().getDeclaredAnnotation(ExtApiIdempotent.class);
		if (extApiIdempotent == null) {
			// 直接執行程序
			Object proceed = proceedingJoinPoint.proceed();
			return proceed;
		}
		// 代碼步驟:
		// 1.獲取令牌 存放在請求頭中
		HttpServletRequest request = getRequest();
		String token = request.getHeader("token");
		if (StringUtils.isEmpty(token)) {
			response("參數錯誤!");
			return null;
		}
		// 2.判斷令牌是否在緩存中有對應的令牌
		// 3.如何緩存沒有該令牌的話,直接報錯(請勿重複提交)
		// 4.如何緩存有該令牌的話,直接執行該業務邏輯
		// 5.執行完業務邏輯之後,直接刪除該令牌。
		if (!redisTokenUtils.findToken(token)) {
			response("請勿重複提交!");
			return null;
		}
		Object proceed = proceedingJoinPoint.proceed();
		return proceed;
	}

	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();
		}

	}

}

冪等註解使用

	// 從redis中獲取Token
	@RequestMapping("/redisToken")
	public String RedisToken() {
		return redisTokenUtils.getToken();
	}

	// 驗證Token
	@RequestMapping(value = "/addOrderExtApiIdempotent", produces = "application/json; charset=utf-8")
	@ExtApiIdempotent
	public String addOrderExtApiIdempotent(@RequestBody OrderEntity orderEntity, HttpServletRequest request) {
		int result = orderMapper.addOrder(orderEntity);
		return result > 0 ? "添加成功" : "添加失敗" + "";
	}

封裝生成token註解

@Target(value = ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface ExtApiToken {

}

改造ExtApiAopIdempotent

@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) {
			return extApiIdempotent(proceedingJoinPoint, signature);
		}
		// 放行
		Object proceed = proceedingJoinPoint.proceed();
		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();
		String valueType = extApiIdempotent.value();
		if (StringUtils.isEmpty(valueType)) {
			response("參數錯誤!");
			return null;
		}
		String token = null;
		if (valueType.equals(ConstantUtils.EXTAPIHEAD)) {
			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();
		}

	}

}

API接口保證冪等性

@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 ? "添加成功" : "添加失敗" + "";
	}
}

 頁面防止重複提交

@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";
	}

}

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章