我想在Google AppEngine的Memcache中存储身份验证挑战,我通过随机整数进行索引。所以,例如我有以下条目:
5932 -> IUH#(*HKJSBOHFBAHV&EG*Y$#)*HF739r7fGA$74gflUSAB
11234 -> (*&YGy3g87gfGYJKY#GRO&Fta9p8yfdlhuH$UT#K&GAk&#G
-3944 -> 8yU#*&GfgKGF&Y@dy;0aU#(Y9fyhgyL$BVGAZD(O$fg($&G
.
:
如果客户端然后尝试验证请求,它将向我发送质询ID(例如-3944)和相应的计算响应。
现在,我的服务器需要从列表中获取挑战号-3944并标记它已使用或(更好)立即删除它以防止重放攻击(第二个请求通过相同的挑战进行身份验证)。然后,服务器计算响应应该是什么,并接受或拒绝基于(错误)匹配的身份验证。
出于性能和配额的原因,我希望避免使用DataStore来应对挑战。我将有一个系统允许客户端请求更多的挑战并重试请求,因此Memcache被清除的罕见情况也不会成为问题。
有没有办法在Memcache中执行原子获取和删除操作,它将返回给定键的条目一次,并返回 null 以查找相同键之后的任何请求(除非它已被重新设定)?对于任何从未设置过的键,它也应该返回 null 。
PS:我正在使用Java。
答案 0 :(得分:5)
睡了一晚后,我提出了几种方法。其中任何一个都不是特别优雅,但让我把它们放在这里作为思考的食物:
MemCache提供(至少)4个命令,以某种方式原子地修改条目并返回有关其先前状态的一些信息(其中两个也被@Moshe Shaham指出):
让我为每个人提供一个可能的实施方案。前3个将特定于整数键,并且将要求实际条目具有正键(因为它们将使用相应的否定键作为标记条目)。
注意:下面给出的示例本质上是概念性的。他们中的一些人可能仍然允许竞争条件,我实际上没有测试任何一个。所以,带上一粒盐。
这里是:
<强> 1。删除强>
首次存储条目时,将标记与其一起存储(使用派生的相应键)。根据尝试删除此标记的成功,对获取和删除进行门控。
public void putForAtomicGnD(MemcacheService memcache, int key, Object value) {
assert key>=0;
memcache.put(key, value); // Put entry
memcache.put(-key-1, null); // Put a marker
}
public Object atomicGetAndDelete(MemcacheService memcache, int key) {
assert key>=0;
if (!memcache.delete(-key-1)) return null; // Are we first to request it?
Object result = memcache.get(key); // Grab content
memcache.delete(key); // Delete entry
return result;
}
可能的竞争条件:putForAtomicGnD可能会覆盖正在读取的值。可以使用ADD_ONLY_IF_NOT_PRESENT政策进行投放,millisNoReAdd进行删除。
可能的优化:使用putAll。
<强> 2。增量强>
让每个条目与读取和删除相关联的读取计数器。
public Object atomicGetAndDelete(MemcacheService memcache, int key) {
assert key>=0;
if (memcache.increment(-key-1, 1L, 0L) != 1L) return null; // Are we 1st?
Object result = memcache.get(key); // Grab content
memcache.delete(key); // Delete entry
return result;
}
注意:如此处所示,此代码仅允许每个键使用一次。要重复使用,需要删除相应的标记,这会导致竞争条件(再次通过millisNoReAdd可解析)。
第3。放强>
具有与每个条目获取和删除的条目相关联的读取标志(不存在标记条目)。基本上是方法的反向“1.删除”。
public Object atomicGetAndDelete(MemcacheService memcache, int key) {
assert key>=0;
if (!memcache.put(-key-1, null, null, SetPolicy.ADD_ONLY_IF_NOT_PRESENT))
return null; // Are we 1st?
Object result = memcache.get(key); // Grab content
memcache.delete(key); // Delete entry
memcache.delete(-key-1, 10000); // Delete marker with millisNoReAdd
return result;
}
可能的竞争条件:另一个put可能会覆盖正在读取的值。同样,使用millisNoReAdd可解析。
可能的优化:使用deleteAll。
<强> 4。 putIfUntouched 强>
我当前的最爱:尝试获取一个条目,并在模拟交易中将其设置为 null ,并使用该条目成功删除。
public Object atomicGetAndDelete(MemcacheService memcache, Object key) {
IdentifiableValue result = memcache.getIdentifiable(key);
if (result==null) return null; // Entry didn't exist
if (result.getValue()==null) return null; // Someone else got it
if (!memcache.putIfUntouched(key, result, null)) return null; // Still 1st?
memcache.delete(key); // Delete entry
return result.getValue();
}
注意:这种方法几乎完全通用(对键类型没有限制),因为它不需要从给定的标记对象派生标记对象的键。它唯一的限制是它不支持实际的 null 条目,因为这个值被保留以表示“已经进入的条目”。
可能的竞争条件?我目前没有看到任何东西,但也许我错过了什么?
可能的优化:putIfUntouched with immediate Expiration(find out how here!)而不是删除。
<强>结论强>
似乎有很多方法可以做到这一点,但到目前为止,我提出的那些方法看起来都不是特别优雅。请使用此处给出的示例主要作为思考的食物,让我们看看我们是否可以提出一个更清洁的解决方案,或者至少让自己相信上述其中一个(putIfUntouched?)实际上可以正常工作。
答案 1 :(得分:1)
为了管理并发和竞争条件,你有两种内置在memcache中的机制:
increment:您可以围绕此原子增量函数构建机制。只需在开始处理某个值时递增计数器,在完成时递减计数器,并且在计数器递增时不允许任何其他线程工作。
putIfUntouched:如果因为您阅读后更改了内存缓存值,则此函数不会允许您更改内存缓存值。在这种情况下,它将返回false。