我目前正在对我正在为Android制作的游戏进行实时测试。这些服务是用rails 3.1编写的,我使用的是Postgresql。我的一些技术精湛的测试人员能够通过将他们的请求记录到服务器并以高并发性重放它们来操纵游戏。我将尝试简要描述下面的场景,而不会陷入代码中。
所以,他们正在做的是,通过代理记录1个有效请求购买。然后用高并发性重放它,这实际上每次都允许一些额外的漏洞。因此,如果他们将其设置为购买100个数量,他们可以将其提升至300-400,或者如果他们的数量达到15个,他们可以将其提升至120个。
上述购买方法包含在交易中。但是,即使它被包装,它也不会在请求几乎同时执行的某些情况下停止它。我猜这可能需要一些DB锁定。需要知道的另一个变化是,在任何给定时间,rake任务都在针对用户表的cron作业中运行,以更新玩家的健康和能量属性。所以,这也无法阻止。
任何帮助都会非常棒。这是我的小爱好项目,我想确保游戏对每个人都公平有趣。
非常感谢!
控制器操作:
def hire
worker_asset_type_id = (params[:worker_asset_type_id])
quantity = (params[:quantity])
trade = Trade.new()
trade_response = trade.buy_worker_asset(current_user, worker_asset_type_id, quantity)
user = User.find(current_user.id, select: 'money')
respond_to do |format|
format.json {
render json: {
trade: trade,
user: user,
messages: {
messages: [trade_response.to_s]
}
}
}
end
end
交易模型方法:
def buy_worker_asset(user, worker_asset_type_id, quantity)
ActiveRecord::Base.transaction do
if worker_asset_type_id.nil?
raise ArgumentError.new("You did not specify the type of worker asset.")
end
if quantity.nil?
raise ArgumentError.new("You did not specify the amount of worker assets you want to buy.")
end
if quantity <= 0
raise ArgumentError.new("Please enter a quantity above 0.")
end
quantity = quantity.to_i
worker_asset_type = WorkerAssetType.where(id: worker_asset_type_id).first
if worker_asset_type.nil?
raise ArgumentError.new("There is no worker asset of that type.")
end
trade_cost = worker_asset_type.min_cost * quantity
if (user.money < trade_cost)
raise ArgumentError.new("You don't have enough money to make that purchase.")
end
# Get the users first geo asset, this will eventually have to be dynamic
potential_total = WorkerAsset.where(user_id: user.id).length + quantity
# Catch all for most people
if potential_total > 100
raise ArgumentError.new("You cannot have more than 100 dealers at the current time.")
end
quantity.times do
new_worker_asset = WorkerAsset.new()
new_worker_asset.worker_asset_type_id = worker_asset_type_id
new_worker_asset.geo_asset_id = user.geo_assets.first.id
new_worker_asset.user_id = user.id
new_worker_asset.clocked_in = DateTime.now
new_worker_asset.save!
end
self.buyer_id = user.id
self.money = trade_cost
self.worker_asset_type_id = worker_asset_type_id
self.trade_type_id = TradeType.where(name: "market").first.id
self.quantity = quantity
# save trade
self.save!
# is this safe?
user.money = user.money - trade_cost
user.save!
end
end
答案 0 :(得分:4)
听起来您需要幂等请求,因此请求重播无效。在可能的情况下实施操作,以便重复它们无效。如果不可能,请为每个请求提供唯一的请求标识符,并记录请求是否已满足。您可以将请求ID信息保存在PostgreSQL中的UNLOGGED
表中或redis / memcached中,因为您不需要它是持久的。这将阻止整类攻击。
要解决这个问题,请在用户项目表上创建AFTER INSERT OR DELETE ... FOR EACH ROW EXECUTE PROCEDURE
触发器。有这个触发器:
BEGIN
-- Lock the user so only one tx can be inserting/deleting items for this user
-- at the same time
SELECT 1 FROM user WHERE user_id = <the-user-id> FOR UPDATE;
IF TG_OP = 'INSERT' THEN
IF (SELECT count(user_item_id) FROM user_item WHERE user_item.user_id = <the-user-id>) > 100 THEN
RAISE EXCEPTION 'Too many items already owned, adding this item would exceed the limit of 100 items';
END IF;
ELIF TG_OP = 'DELETE' THEN
-- No action required, all we needed to do is take the lock
-- so a concurrent INSERT won't run until this tx finishes
ELSE
RAISE EXCEPTION 'Unhandled trigger case %',TG_OP;
END IF;
RETURN NULL;
END;
或者,您可以在添加或删除任何项目所有权记录之前对客户ID进行行级锁定,从而在Rails应用程序中实现相同的功能。我更喜欢在触发器中做这种事情,你不能忘记在某个地方应用它,但我意识到你可能更喜欢在应用程序级别执行它。请参阅Pessimistic locking。
乐观锁定不适合此应用程序。您可以在添加/删除项目之前通过递增用户的锁定计数器来使用它,但它会导致用户表上的行流失,并且当您的交易无论如何都很短时,这实际上是不必要的。
答案 1 :(得分:0)
除非您向我们展示您的相关架构和查询,否则我们无能为力。我想你做的事情如下:
$ start transaction;
$ select amount from itemtable where userid=? and itemid=?;
15
$ update itemtable set amount=14 where userid=? and itemid=?;
commit;
你应该做的事情如下:
$ start transaction;
$ update itemtable set amount=amount-1 returning amount where userid=? and itemid=?;
14
$ commit;