A-A+

API接口 防并发、防重放、防篡改等攻击的机制

2020年04月08日 11:12 汪洋大海 暂无评论 共4733字 (阅读1,027 views次)

我们在设计接口的时候,最怕一个接口被用户截取用于重放攻击。重放攻击是什么呢?就是把你的请求原封不动地再发送一次,两次...n次,一般正常的请求都会通过验证进入到正常逻辑中,如果这个正常逻辑是插入数据库操作,那么一旦插入数据库的语句写的不好,就有可能出现多条重复的数据。一旦是比较慢的查询操作,就可能导致数据库堵住等情况。

这里就有一种防重放的机制来做请求验证。

timestamp+nonce

我们常用的防止重放的机制是使用timestamp和nonce来做的重放机制。

timestamp用来表示请求的当前时间戳,这个时间戳当然要和服务器时间戳进行校正过的。我们预期正常请求带的timestamp参数会是不同的(预期是正常的人每秒至多只会做一个操作)。每个请求带的时间戳不能和当前时间超过一定规定的时间。比如60s。这样,这个请求即使被截取了,你也只能在60s内进行重放攻击。过期失效。

但是这样也是不够的,还有给攻击者60s的时间。所以我们就需要使用一个nonce,随机数。

nonce是由客户端根据足够随机的情况生成的,比如 md5(timestamp+rand(0, 1000)); 它就有一个要求,正常情况下,在短时间内(比如60s)连续生成两个相同nonce的情况几乎为0。

服务端

服务端第一次在接收到这个nonce的时候做下面行为:

  1. 去redis中查找是否有key为nonce:{nonce}的string
  2. 如果没有,
    1. 考虑做时间戳判断,即认为客户端和服务端的时间差异不应该大于阈值(上述的60s,根据业务场景设置合理值),注意控制好时区Locale。
    2. 则创建这个key,把这个key失效的时间和验证timestamp失效的时间一致,比如是60s。
  3. 如果有,说明这个key在60s内已经被使用了,那么这个请求就可以判断为重放请求。

示例

那么比如,下面这个请求:

http://a.com?uid=123×tamp=1480556543&nonce=43f34f33&sign=80b886d71449cb33355d017893720666

这个请求中国的uid是我们真正需要传递的有意义的参数

timestamp,nonce,sign都是为了签名和防重放使用。

timestamp是发送接口的时间,nonce是随机串,sign是对uid,timestamp,nonce(对于一些rest风格的api,我建议也把url放入sign签名)。签名的方法可以是md5({秘要}key1=val1&key2=val2&key3=val3...)

服务端接到这个请求:

    1. 先验证sign签名是否合理,证明请求参数没有被中途篡改
    2. 再验证timestamp是否过期,证明请求是在最近60s被发出的
    3. 最后验证nonce是否已经有了,证明这个请求不是60s内的重放请求

 
 
-----------防并发攻击-------------
 
近半个月过得很痛苦,主要是产品上线后,引来无数机器用户恶意攻击,不停的刷新产品各个服务入口,制造垃圾数据,消耗资源。他们的最好成绩,1秒钟 可以并发6次,赶在Database入库前,Cache进行Missing Loading前,强占这其中十几毫秒的时间,进行恶意攻击。

为了应对上述情况,做了如下调整:

更新数据时,先写Cache,然后写Database(双写),如果可以,写操作交给队列后续完成。
限制统一帐号,同一动作,同一秒钟并发次数,超过1次不做做动作,返回操作失败。
限制统一用户,每日动作次数,超限返回操作失败。
要完成上述操作,同事给我支招。用Memcached的add方法,就可以很快速的解决问题。不需要很繁琐的开发,也不需要依赖数据库记录,完全内存操作。
以下实现一个判定冲突的方法:

Java代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
/**
 * 冲突延时 1秒
 */
public static final int MUTEX_EXP = 1;
/**
 * 冲突键
 */
public static final String MUTEX_KEY_PREFIX = "MUTEX_";
 
/**
 * 冲突判定
 *
 * @param key
 */
public boolean isMutex(String key) {
    return isMutex(key, MUTEX_EXP);
}
 
/**
 * 冲突判定
 *
 * @param key
 * @param exp
 * @return true 冲突
 */
public boolean isMutex(String key, int exp) {
    boolean status = true;
    try {
        if (memcachedClient.add(MUTEX_KEY_PREFIX + key, exp, "true")) {
            status = false;
        }
    } catch (Exception e) {
        logger.error(e.getMessage(), e);
    }
    return status;
}

做个说明:

选项 说明
add 仅当存储空间中不存在键相同的数据时才保存
replace 仅当存储空间中存在键相同的数据时才保存
set 与add和replace不同,无论何时都保存
也就是说,如果add操作返回为true,则认为当前不冲突!

回归场景,恶意用户1秒钟操作6次,遇到上述这个方法,只有乖乖地1秒后再来。别小看这1秒钟,一个数据库操作不过几毫秒。1秒延迟,足以降低系统负载,增加恶意用户成本。

附我用到的基于XMemcached实现:

Java代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
import net.rubyeye.xmemcached.MemcachedClient;
 
import org.apache.log4j.Logger;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
 
/**
 *
 * @author Snowolf
 * @version 1.0
 * @since 1.0
 */
@Component
public class MemcachedManager {
 
    /**
     * 缓存时效 1天
     */
    public static final int CACHE_EXP_DAY = 3600 * 24;
 
    /**
     * 缓存时效 1周
     */
    public static final int CACHE_EXP_WEEK = 3600 * 24 * 7;
 
    /**
     * 缓存时效 1月
     */
    public static final int CACHE_EXP_MONTH = 3600 * 24 * 30 * 7;
 
    /**
     * 缓存时效 永久
     */
    public static final int CACHE_EXP_FOREVER = 0;
 
    /**
     * 冲突延时 1秒
     */
    public static final int MUTEX_EXP = 1;
    /**
     * 冲突键
     */
    public static final String MUTEX_KEY_PREFIX = "MUTEX_";
    /**
     * Logger for this class
     */
    private static final Logger logger = Logger
            .getLogger(MemcachedManager.class);
 
    /**
     * Memcached Client
     */
    @Autowired
    private MemcachedClient memcachedClient;
 
    /**
     * 缓存
     *
     * @param key
     * @param value
     * @param exp
     *            失效时间
     */
    public void cacheObject(String key, Object value, int exp) {
        try {
            memcachedClient.set(key, exp, value);
        } catch (Exception e) {
            logger.error(e.getMessage(), e);
        }
        logger.info("Cache Object: [" + key + "]");
    }
 
    /**
     * Shut down the Memcached Cilent.
     */
    public void finalize() {
        if (memcachedClient != null) {
            try {
                if (!memcachedClient.isShutdown()) {
                    memcachedClient.shutdown();
                    logger.debug("Shutdown MemcachedManager...");
                }
            } catch (Exception e) {
                logger.error(e.getMessage(), e);
            }
        }
    }
 
    /**
     * 清理对象
     *
     * @param key
     */
    public void flushObject(String key) {
        try {
            memcachedClient.deleteWithNoReply(key);
        } catch (Exception e) {
            logger.error(e.getMessage(), e);
        }
        logger.info("Flush Object: [" + key + "]");
    }
 
    /**
     * 冲突判定
     *
     * @param key
     */
    public boolean isMutex(String key) {
        return isMutex(key, MUTEX_EXP);
    }
 
    /**
     * 冲突判定
     *
     * @param key
     * @param exp
     * @return true 冲突
     */
    public boolean isMutex(String key, int exp) {
        boolean status = true;
        try {
            if (memcachedClient.add(MUTEX_KEY_PREFIX + key, exp, "true")) {
                status = false;
            }
        } catch (Exception e) {
            logger.error(e.getMessage(), e);
        }
        return status;
    }
 
    /**
     * 加载缓存对象
     *
     * @param key
     * @return
     */
    public <T> T loadObject(String key) {
        T object = null;
        try {
            object = memcachedClient.<T> get(key);
        } catch (Exception e) {
            logger.error(e.getMessage(), e);
        }
        logger.info("Load Object: [" + key + "]");
        return object;
    }
 
}

文章来源:https://www.cnblogs.com/bingosblog/p/10688256.html
https://blog.csdn.net/ddlgyqddlgyq/article/details/84599058

布施恩德可便相知重

微信扫一扫打赏

支付宝扫一扫打赏

×

给我留言