防重放攻击策略

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