防重放攻擊策略
1.分析重放攻擊的特征:
重放攻擊的基本原理就是把以前竊聽到的數據原封不動地重新發送給接收方,防止黑客盜取請求,進行重復請求,造成服務器的負擔。
2. 根據重放攻擊的特性進行代碼邏輯設計
首先防重放攻擊策略需要對每個請求進行判斷,所以根據這種特征選擇過濾器去對請求做過濾
3.由於需要判斷請求傳遞過來的nonce參數與nonce集合進行對比,所以如果使用數據庫技術可能過慢,因此為了解決該問題選擇redis緩存技術來解決,將請求傳遞過來的nonce參數保存到緩存中進行讀取
代碼實現:
在web.xml中配置過濾器
<filter>
<filter-name>securityFilter</filter-name>
<filter-class>com.cesec.utils.rif.util.SecurityFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>securityFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
定義操作redis的接口以及實現類:
接口類:RedisBaseTakes
public interface RedisBaseTakes<H,K,V> { //根據key查詢value public String get(K key); //增加一個鍵值對 public void add(K key,String value); //清空redis中所有數據 public String flushDB(); //增加有效期的key public void add(byte[] key, byte[] value, long liveTime); //添加key value 並且設置存活時間 public void add(String key, String value, long liveTime); //查詢數據庫中是否含有k值 public boolean contains(K key); }
實現類:RedisBaseTakesImpl
@Component("redisBaseTakesImpl")
public class RedisBaseTakesImpl implements RedisBaseTakes{
@Resource(name="redisTemplate")
private RedisTemplate redisTemplate;
private Logger logger = Logger.getLogger(String.valueOf(RedisBaseTakesImpl.class));
@Override
public String get(Object key) {
if(redisTemplate==null){
logger.warning("redisTemplate 實例化失敗");
return null;
}else{
String value = (String) redisTemplate.opsForValue().get(key);
return value;
}
}
@Override
public void add(Object key, String value) {
if(redisTemplate==null){
logger.warning("redisTemplate 實例化失敗");
return;
}else{
redisTemplate.opsForValue().set(key,value);
}
}
@Override
public String flushDB() {
if(redisTemplate==null){
logger.warning("redisTemplate 實例化失敗");
return null;
}else{
return (String)redisTemplate.execute(new RedisCallback() {
@Override
public String doInRedis(RedisConnection connection) throws DataAccessException {
connection.flushDb();
return "ok";
}
});
}
}
@Override
public void add(final byte[] key, final byte[] value, final long liveTime) {
if(redisTemplate==null){
logger.warning("redisTemplate 實例化失敗");
}else{
redisTemplate.execute(new RedisCallback() {
@Override
public Long doInRedis(RedisConnection connection) throws DataAccessException {
connection.set(key, value);
if (liveTime > 0) {
connection.expire(key, liveTime);
}
return 1L;
}
});
}
}
@Override
public void add(String key, String value, long liveTime) {
this.add(key.getBytes(), value.getBytes(), liveTime);
}
@Override
public boolean contains(Object key) {
if(redisTemplate==null){
logger.warning("redisTemplate 實例化失敗");
return false;
}else{
return redisTemplate.hasKey(key);
}
}
}
springmvc整合redis
web.xml需要將配置文件spring-redis.xml加載進入context
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>
classpath:spring-mvc.xml
classpath:spring-mybatis.xml
classpath:spring-redis.xml
</param-value>
</context-param>
spring-redis.xml
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:p="http://www.springframework.org/schema/p" xmlns:tx="http://www.springframework.org/schema/tx" xmlns:context="http://www.springframework.org/schema/context" xsi:schemaLocation=" http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-3.0.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.0.xsd"> <context:property-placeholder location="classpath:*.properties"/> <!--設置數據池--> <bean id="poolConfig" class="redis.clients.jedis.JedisPoolConfig"> <property name="maxIdle" value="${redis.maxIdle}"></property> <property name="minIdle" value="${redis.minIdle}"></property> <property name="testOnBorrow" value="${redis.testOnBorrow}"></property> </bean> <!--鏈接redis--> <bean id="connectionFactory" class="org.springframework.data.redis.connection.jedis.JedisConnectionFactory"> <property name="hostName" value="${redis.host}"></property> <property name="port" value="${redis.port}"></property> <property name="password" value="${redis.password}"></property> <property name="poolConfig" ref="poolConfig"></property> </bean> <bean id="redisTemplate" class="org.springframework.data.redis.core.RedisTemplate" p:connection-factory-ref="connectionFactory" > <!--以下針對各種數據進行序列化方式的選擇--> <property name="keySerializer"> <bean class="org.springframework.data.redis.serializer.StringRedisSerializer" /> </property> <property name="valueSerializer"> <bean class="org.springframework.data.redis.serializer.StringRedisSerializer" /> </property> <property name="hashKeySerializer"> <bean class="org.springframework.data.redis.serializer.StringRedisSerializer" /> </property> <!--<property name="hashValueSerializer"> <bean class="org.springframework.data.redis.serializer.StringRedisSerializer" /> </property>--> </bean> </beans>
redis的配置文件redis.properties
redis.maxIdle=連接池最大連接數 redis.minIdle=連接池最小連接數 redis.maxWaitMillis=最大連接等待時間 redis.testOnBorrow=true redis.maxTotal=500 redis.host=127.0.0.1 redis.port=6379 redis.password=123456
過濾器設計:
將過濾設計在doFilter方法中:
//獲取請求的request HttpServletRequest request=(HttpServletRequest)servletRequest; //獲取服務器的response HttpServletResponse response = (HttpServletResponse) servletResponse; //獲取request的URL String requestUri = request.getRequestURI();
開始對請求進行過濾:
首先對於后台之間的請求不需要處理:
//后台請求不做處理 if(requestUri.endsWith(".do")){ filterChain.doFilter(request, response); return; }
判斷請求的參數是否完整
String time=request.getHeader("time");
//獲取請求nonce
String nonce=request.getHeader("nonce");
//獲取token碼
String token=request.getHeader("token");
//獲取sign
String sign=request.getHeader("sign");
if(null == time||nonce == null || token == null || sign == null){
log.error("當前請求非法,請重新發送");
return;
}
判斷請求是否過期
//判斷時間戳是否超時 Date date = new Date(); SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMddHHmmss"); String str = sdf.format(date); if(difference(str,time)>60){ log.error("當前請求超時"); return; } public int difference(String now,String past){ Date nowtime=timeStampToDate(now); Date pasttime=timeStampToDate(past); return Math.abs((int)(nowtime.getTime()- pasttime.getTime())/1000); }
判斷請求是否重復
//判斷是否是重復請求 if(exist(nonce)){ log.error("當前請求重復"); return; } private boolean exist(String nonce) { return redisBaseTakesImpl.contains(nonce); }
判斷數字簽名是否正確
Md5Util md5 = new Md5Util(""); if(!sign.equals(md5.compute(token+time+nonce))){ System.out.println("數字簽名認證失敗"); return; } //md5方法 public class Md5Util { private static String inStr; private static MessageDigest md5; public Md5Util(String inStr) { this.inStr = inStr; try { this.md5 = MessageDigest.getInstance("MD5"); } catch (Exception e) { e.printStackTrace(); } } // md5 加密(32位) public String compute(String plainText) { String result = null; try { MessageDigest md = MessageDigest.getInstance("MD5"); md.update(plainText.getBytes()); byte b[] = md.digest(); int i; StringBuffer buf = new StringBuffer(""); for (int offset = 0; offset < b.length; offset++) { i = b[offset]; if (i < 0) i += 256; if (i < 16) buf.append("0"); buf.append(Integer.toHexString(i)); } result = buf.toString(); } catch (NoSuchAlgorithmException e) { e.printStackTrace(); } return result; } }
//將nonce加入到集合中,並設置有效期為60s
redisBaseTakesImpl.add(nonce,"1",60);
使用redis工具類插入nonce參數並設置nonce參數在redis中的存活時間,這樣做有這樣一個好處:為每個nonce都設置一個存活期,這樣可以動態的管理每個nonce的生存周期,這樣就不要定時清空nonce集合了,如果60s內沒有請求,那么所有的nonce參數都會失效。
所有這些請求處理完成之后,開始處理合法請求
filterChain.doFilter(servletRequest, servletResponse);
補記:出現這樣一個問題:
使用注解依賴注入redis操作的實現類
private RedisBaseTakes redisBaseTakesImpl;
但是服務器會報一個錯誤:服務器遇到內部錯誤,實現一個服務器實例失敗,很奇怪
調試了半天發現服務器中有這樣一個特征:服務器的執行順序
監聽器 > 過濾器 > 攔截器
所以在filter使用自動注入方法會失敗,因此調用
ServletContext sc = request.getSession().getServletContext(); XmlWebApplicationContext cxt = (XmlWebApplicationContext) WebApplicationContextUtils.getWebApplicationContext(sc); if(cxt != null && cxt.getBean("redisBaseTakesImpl") != null && redisBaseTakesImpl == null) { redisBaseTakesImpl = (RedisBaseTakesImpl) cxt.getBean("redisBaseTakesImpl"); }
