Java後端防止接口被同一個賬號在同一臺設備短時間內重複調用的一種解決方案

       對於前後端分離的項目,後端人員通常都要對發起請求的用戶的合法性和權限進行審覈(比如用戶每次請求都要攜帶token,token校驗通過的才放行),只要審覈通過了,基本上都允許用戶的後續操作。可是這樣就安全了嗎?任何一個在後端開發浸淫多年的人,都會不知不覺間往數據安全方面傾注更多的精力。筆者作爲某小公司中的唯一後端開發,在數據安全這塊也是操碎了心的,先是重構了登錄註冊接口,防止用戶賬號密碼被暴力破解;接着又引入了token令牌機制,對所有訪問網站資源的請求的進行安全過濾;緊接着又對支付系統進行了重構,對金額和密碼進行了加密傳輸,···。咱也是不得不關心啊,身在其位就得謀其事,想象一下,未來哪天項目終於上了正軌,開始盈利,你正準備迎接成功的時候,突然用戶賬號被頻繁盜取或者網站接口被人惡意調用導致服務器崩潰了等等,那時才蛋疼。

       回到前面的話題,只是對用戶的合法性校驗通過就足夠了嗎?如果是合法用戶進行惡意操作呢?說的通俗一點,一個通過正常流程註冊的合法的賬號,在1秒鐘內10次調用了同一個接口,這種請求合理嗎?如果1秒鐘內調用了100次呢?如果這個接口是類似搶單或支付的接口呢?退一步說,這個社會是很和諧美好的,好人總會比壞人多,但是如果你遇到的是一個不靠譜的前端開發人員呢?他在調用後端接口的時候,不會對必傳字段做空值判斷,更不會在用戶點擊按鈕之後,後端還沒有返回處理結果的那不算很長但用戶足以再多點幾次按鈕的時間內將按鈕禁用掉,這樣也會造成接口被重複調用。

       因此,防止接口被重複調用也是後端開發人員需要認真對待的問題。

小弟不才,僅能在已有的知識範疇內勉強給出一種臨時解決方案,也算對得起自己現今所處的崗位了,下面是代碼:

 

package interceptors;

import java.io.PrintWriter;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;

import org.apache.commons.lang.StringUtils;
import org.apache.log4j.Logger;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;

import com.alibaba.fastjson.JSON;

//使用spring的攔截器來實現攔截重複請求的功能
public class UserPermissionInterceptor implements HandlerInterceptor {

	private static final Logger logger = Logger.getLogger(UserPermissionInterceptor.class);

	/**
	 * 完成頁面的render後調用
	 */
	@Override
	public void afterCompletion(HttpServletRequest request,
			HttpServletResponse response, Object object, Exception exception)
			throws Exception {
		
	}

	/**
	 * 在調用controller具體方法後攔截
	 */
	@Override
	public void postHandle(HttpServletRequest request,
			HttpServletResponse response, Object object,
			ModelAndView modelAndView) throws Exception {
		
		//請求處理後,刪除標識
		String requestKey =(String) request.getAttribute("ACCESS_KEY");
		if(requestKey!=null){
			RedisUtils.del(requestKey);
			request.removeAttribute("ACCESS_KEY");
		}
		
	}
	
	/**
	 * 在調用controller具體方法前攔截
	 */
	@Override
	public boolean preHandle(HttpServletRequest request,
			HttpServletResponse response, Object object) throws Exception {
		
		String requestUri = request.getRequestURI();
		String url = requestUri.substring(request.getContextPath().length());//獲取此次請求訪問的是哪個接口

		String ip=MobileUtil.getIpAddr(request);//獲取當前的ip地址值
		
		String token = request.getParameter("TOKEN");//獲取用戶令牌,合法請求必然攜帶
		
		//沒有攜帶令牌,說明是非法請求
		if(StringUtils.isBlank(token)){
			return false;
		}

		//獲取此次請求的用戶設備信息,判斷是pc端、app、還是公衆號端發來的請求,這個需要前端人員在調用接口時設置指定參數
		int device = MobileUtil.judgeDevice(request);
		if(device==0){//沒有設備標識,說明是非法請求
			return false;
		}
				
		//判斷是否是重複請求
		String requestKey=device+"_"+ip+"_"+token+"_"+url;
		boolean b = RedisUtils.transactSet(requestKey, "1", 5);//設置5秒,只是爲了防止攔截器的後置處理方法沒執行到(比如突然斷電),導致後續的同類請求都不能執行
		if(!b){
			Map<String,Object> map=new HashMap<>();
			map.put("success",false);
			map.put("result","請求太頻繁,請稍後再試");			
			PrintWriter out = response.getWriter();
			out.print(JSON.toJSONString(map));
			out.close();
			return false;
		}
		
		//如果請求允許,就記住key,請求處理完後,還要刪除標識
		request.setAttribute("ACCESS_KEY", requestKey);
				
		//此處省略一些校驗令牌合法性的操作

		
		return true;
	}

}

 

我的項目用的是SSM架構,在spring-mvc.xml配置文件中需要加上下面這段配置:

	<!-- 攔截器 -->
	<mvc:interceptors>
		<mvc:interceptor>
			<mvc:mapping path="/**" />
			<bean class="interceptors.UserPermissionInterceptor"></bean>
		</mvc:interceptor>
	</mvc:interceptors>

 

 

       MobileUtil類如下:

package util.mobile;

import java.util.regex.Pattern;

import javax.servlet.http.HttpServletRequest;

import org.apache.commons.lang.StringUtils;

public class MobileUtil {
	
	/**
	 * 根據自定義的請求頭參數判斷當前登錄的設備
	 */
	public static int judgeDevice(HttpServletRequest request) {
		
		//登錄的設備
		String device = request.getHeader("ACCESS-DEVICE");
		
		if(StringUtils.isBlank(device)){
			String userAgent = request.getHeader("user-agent");
			if(userAgent!=null&&userAgent.toLowerCase().contains("mozilla")){
				return 1;//pc端的登錄
			}else{
				return 0;//非法請求
			}			
		}else{
			switch (device.toLowerCase()) {
				case "wechat_coming_snafu"://微信公衆號
					return 2;
				case "ios_coming_snafu"://ios
					return 3;
				case "android_coming_snafu"://android
					return 4;
				default:
					return 0;
			}
		}
	}
	
	/**
	 * 獲取請求的客戶端的ip地址
	 * 
	 * @param request
	 * @return
	 */
	public static String getIpAddr(HttpServletRequest request) {
		String ipAddress = request.getHeader("x-forwarded-for");
		if (ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) {
			ipAddress = request.getHeader("Proxy-Client-IP");
		}
		if (ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) {
			ipAddress = request.getHeader("WL-Proxy-Client-IP");
		}
		if (ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) {
			ipAddress = request.getRemoteAddr();
			if (ipAddress.equals("127.0.0.1") || ipAddress.equals("0:0:0:0:0:0:0:1")) {
				// 根據網卡取本機配置的IP
				InetAddress inet = null;
				try {
					inet = InetAddress.getLocalHost();
					ipAddress = inet.getHostAddress();
				} catch (Exception e) {
					e.printStackTrace();
				}
			}
		}
		
		// 對於通過多個代理的情況,第一個IP爲客戶端真實IP,多個IP按照','分割
		if (ipAddress != null && ipAddress.length() > 15 && ipAddress.indexOf(",") > 0 ) { // "***.***.***.***".length()
			ipAddress = ipAddress.substring(0, ipAddress.indexOf(","));
		}
		
		return ipAddress;
	}
}

 

RedisUtils類詳見我的另一篇博客Jedis常用工具類,包含一些具有事務的設置值的方法

 

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