构建一个Rails控制器很简单,而且有很好的文档。遵循 "Rails方式",你的生活就很容易了。然而,webhook端点需要一个不同的方法。在这篇文章中,你将学习如何构建可维护和安全的webhook端点。
Webhook路由和控制器
首先你需要一个路由。通常,外部应用程序在调用webhook端点时,会发出POST请求。在这种情况下,我们使用一个通用的 /webhooks端点:
1
post '/webhooks', to: 'webhooks#create'
你还需要一个控制器/动作来处理webhook请求:
1
2
3
4
class WebhooksController < ApplicationController
def create
end
end
参数的条件逻辑
webhook控制器/动作需要一些逻辑。在这种情况下,假设我们想根据webhook请求体的内容创建一个用户。如果请求体包含一个用户ID,运行一些逻辑,如果不包含,返回一个错误:
1
2
3
4
5
6
7
8
9
10
11
12
class WebhooksController < ApplicationController
def create
if params.dig(:user, :username)
User.create!(username: params.dig(:user, :username))
# some additional logic
render json: {}, status: :created
else
render json: {}, status: :unprocessable_entity
end
end
end
如果用户ID是存在的,那么一个 User记录就会被创建。可能会有一些额外的逻辑要运行,正如注释所指出的。
如果用户ID丢失,端点将返回一个无法处理的实体错误。
头部的条件性逻辑
从webhook请求中接收数据的另一种方式是通过头信息。在这个例子中,外部服务发送了一个 X-Person-Event标头,表明用户的行动。这里是你如何根据头信息添加条件逻辑的:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class WebhooksController < ApplicationController
def create
if request.headers['X-Person-Event'] == 'purchase'
Purchase.create!(amount: params.dig(:purchase, :amount))
# some additional logic
render json: {}, status: :created
elsif params.dig(:user, :username)
User.create!(username: params.dig(:user, :username))
# some additional logic
render json: {}, status: :created
else
render json: {}, status: :unprocessable_entity
end
end
end
使用服务对象模式进行重构
以这种方式进行,控制器将变得难以维护。服务对象模式是解决这个问题的一个好方法。这个想法是把每个条件里面的逻辑抽象成一个PORO(普通的Ruby对象)。这简化了控制器,并产生了专门的、可重复使用的对象。
下面是重构后的控制器的样子:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class WebhooksController < ApplicationController
def create
if request.headers['X-Person-Event'] == 'purchase'
PurchaseHandlerService.call(params)
render json: {}, status: :created
elsif params.dig(:user, :username)
UserHandlerService.call(params)
render json: {}, status: :created
else
render json: {}, status: :unprocessable_entity
end
end
end
构建服务对象很简单。我们建议每次都采用相同的模式:一个描述性的类名和一个单一的公共 call方法。我们还建议添加一个 self.call方法来提供一个漂亮的速记。这允许调用者直接使用服务,而无需初始化:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
class PurchaseHandlerService
def self.call(params)
new(params).call
end
def initialize(params)
@params = params
end
def call
Purchase.create!(amount: params.dig(:purchase, :amount))
# some additional logic
end
end
1
2
3
4
5
6
7
8
9
10
11
12
13
14
class UserHandlerService
def self.call(params)
new(params).call
end
def initialize(params)
@params = params
end
def call
User.create!(username: params.dig(:user, :username))
# some additional logic
end
end
跳过verify_authenticity_token
在这个控制器可以部署到生产中之前,你需要删除CSRF检查。默认情况下,Rails控制器会检查CSRF令牌。外部webhook请求不会有这个令牌,所以必须跳过这个检查:
1
2
class WebhooksController < ApplicationController
skip_before_action :verify_authenticity_token
你可能想知道这对安全的影响,这是个好问题。任何人都可以向这个端点发出请求,这是个问题。有几个解决方案,我们将简要介绍一下。
IP白名单是指API供应商发布一个或多个IP地址,网络钩子端点可望从这些地址被调用。使用这些地址,你可以添加逻辑,检查每个传入请求的来源IP地址。如果一个特定的请求不是来自白名单的IP,就忽略它。
签名请求是一种技术,API提供者在请求中添加一个加密的哈希值,通常是在头中。哈希值是用一种算法(例如:HMAC)生成的,使用API提供者和webhook接收者都知道的秘密。你可以添加逻辑来验证签名,使用该秘密。如果签名丢失或未经授权,则忽略它。