如果你想在互联网上公开暴露一个API,授权迟早会成为你的要求。你要验证消费API的客户是否有适当的权限。
本指南正是关于这一点。我们将使用Ruby On Rails来保护一个API,用Auth0作为授权服务器。
在Github上有一个代码库,你可以跟着它走。
保护API的安全
我还没有解释我所说的保护API是什么意思。从本质上讲,我们要确保受保护的路由只能被有足够权限的用户访问。
当涉及到安全问题时,一般认为推出自己的定制实现是个坏主意。相反,我正在使用OAuth,一个经过战斗考验的、广泛使用的网络应用程序授权框架。
在这种情况下,Auth0扮演了授权服务器的角色,并将很大一部分工作从我们这里抽象出来。这样一来,我们就可以专注于为我们的用户提供价值。
在不深入了解OAuth如何工作的情况下,我们假设对我们的API的调用将包括一个使用行业标准的JWT格式的承载令牌。该令牌包含一系列关于其发行者的声明,它的有效期是多久,以及它授予了什么权利。让我们来看看一个样本令牌。
{
"iss": "https://yourTenant.eu.auth0.com/",
"sub": "zHwnsh0j2sTj4u3ss6YedSFrzyb2",
"aud": "https://targetAudience.com",
"iat": 1621369130,
"exp": 1791455530,
"azp": "ThEkgdG1NndLlWoNMcEdEr2KJIs9vKad",
"scope": "openid profile read:admin-messages",
"permissions": ["read:admin-messages"]
}
我们将实现对该令牌的验证,并拒绝不具备所需权限的请求。
入门
我们将从我们的基础应用程序开始,用Rails 6引导。这个分支是一个很好的起点。你可以通过在终端窗口运行以下命令来下载它。
git clone -b starter --single-branch https://github.com/auth0-blog/rails-api-auth0.git
该API有三个端点,具有不同的保护级别。
/api/messages/public:公共路线。/api/messages/protected:需要一个有效的访问令牌。/api/messages/admin:需要一个有效的访问令牌。由于Auth0使用JWT作为其访问令牌格式,我们可以检查它,并确保它有一个permissions,其中包含范围read:admin-messages.
运行应用程序
要运行该应用程序,我们首先需要正确的ruby版本。最简单的方法是使用一个版本管理器,如Rbenv。一旦你安装了它,在版本库内运行这个命令来安装正确的ruby版本。
rbenv install
安装应用程序的依赖项。
bundle install
最后,运行该应用程序。
bin/rails s
你可以用curl 来验证该应用程序是否正常工作。
curl localhost:6060/api/messages/public
该命令将返回一个200的代码,外加信息。
{"message": "The API doesn't require an access token to share this message."}
在Auth0上创建一个API
为了保证Auth0的API安全,你需要一个Auth0账户。如果你还没有,你现在就可以免费注册了。在Auth0仪表板的API部分,点击创建API。为你的API提供一个名称和一个标识符。稍后在配置访问令牌验证时,你将使用该标识符作为audience 。将签名算法设为RS256。

一旦你创建了API,去API细节中的权限标签,添加一个名为 read:admin-messages.

注意:当你在Auth0仪表板上时,注意你的Auth0域。你很快就会需要它。这是一个字符串,其形式为
YOUR-TENANT-NAME.auth0.com其中YOUR-TENANT-NAME是你在Auth0创建账户时提供的名称。欲了解更多信息,请查看文档。
准备应用程序的验证
好了,我们的应用程序已经准备好了,而且急需一些安全保障。在这之前,我们需要添加一些配置。
创建API后,你尽职尽责地存储了domain 和audience ,对吗?让我们来使用它们。在Rails世界中,惯例是在config 文件夹中添加这个,使用YAML。该文件被称为 config/auth0.yml.
我们不想在代码中存储凭证,所以我们将把这些值导出为环境变量,命名为 AUTH0_DOMAIN和 AUTH0_AUDIENCE.该配置使用这些值,并使它们安全地远离源码控制!
development:
issuerUri: <%= ENV["AUTH0_DOMAIN"] %>
audience: <%= ENV["AUTH0_AUDIENCE"] %>
现在,设置 AUTH0_DOMAIN和 AUTH0_AUDIENCE环境变量设置为domain 和audience ,作为你的API的值。
有了这些,应用程序就可以与Auth0连接了。让我们继续执行。
验证访问令牌
在我们开始之前,让我们先谈谈rails的认证框架。生态系统中有一堆替代方案,比如CanCanCan或devise。
然而,我们的情况对这些框架来说是一个尴尬的选择。我们不想接管用户管理。我们只想验证由授权服务器发出的令牌。在这种情况下,使用一个框架不会像Spring Security那样给我们带来很多好处。这就是为什么我们只依赖一个额外的宝石,jwt。
实现授权检查的代码存在于add-authorization分支中。让我们看看它是如何工作的
利用JWT库
jwt 库带来了很多功能,使我们的API能够验证和管理JWT格式的令牌,比如由Auth0发布的令牌。我们需要做三件事。
- 我们需要从Auth0获取JWKS配置,这样我们就知道验证访问令牌签名的公钥是什么。
- 有了公钥、发行者和受众,我们要验证令牌。
- 在验证令牌后,我们要对其进行解码,以访问
permissions(或令牌中包含的任何其他信息,真的)。
解码令牌
让我们进入正题。我们在lib 文件夹中实现验证/解码逻辑,文件名为 json_web_token.rb.我们从获取JWKS的数据开始。
# lib/json_web_token.rb
require 'jwt'
require 'net/http'
class JsonWebToken
class << self
def algorithm
'RS256'
end
def key(header)
jwks_hash[header['kid']]
end
def jwks_hash
jwks_raw = Net::HTTP.get URI("#{issuer}.well-known/jwks.json")
jwks_keys = Array(JSON.parse(jwks_raw)['keys'])
jwks_keys.map do |k|
[
k['kid'],
OpenSSL::X509::Certificate.new(Base64.decode64(k['x5c'].first)).public_key
]
end.to_h
end
def issuer
"https://#{Rails.application.config.x.auth0.issuerUri}/"
end
end
end
正如你所看到的,通过issuerUri ,我们知道在哪里可以找到数据。然后,我们对密钥进行解析,以便我们可以在库中消费它。
下一步是给类添加一个额外的方法,verify ,它同时验证和解码令牌。
# lib/json_web_token.rb
class JsonWebToken
class << self
def verify(token)
JWT.decode(token, nil,
true, # Verify the signature of this token
algorithm: algorithm,
iss: Rails.application.config.x.auth0.issuerUri,
verify_iss: true,
aud: Rails.application.config.x.auth0.audience,
verify_aud: true) do |header|
key(header)
end
end
# ... existing code ...
end
end
这就是我们实际验证所需的所有代码。很好,不是吗?但我们的控制器仍然是不安全的。不过,不会太久了。
保护端点
为了尽可能的方便,我们将创建一个钩子,在任何需要验证的路由之前运行。为了让每个控制器都能使用它,它位于 app/controllers/application_controller.rb:
# app/controllers/application_controller.rb
require 'json_web_token'
class ApplicationController < ActionController::API
def authorize!
valid, result = verify(raw_token(request.headers))
head :unauthorized unless valid
@token ||= result
end
private
def verify(token)
payload, = JsonWebToken.verify(token)
[true, payload]
rescue JWT::DecodeError => e
[false, e]
end
def raw_token(headers)
return headers['Authorization'].split.last if headers['Authorization'].present?
nil
end
end
我们从标准的授权头中获取令牌。然后,我们用上一节的代码来验证它。运行后有两个可能的路径 authorize!:
- 如果令牌不在那里,或者由于任何原因无效,处理就会在那里停止,调用会返回401
- 如果令牌是有效的,我们继续执行,并将令牌存储在
@token,这样它就可以使用了。
我们用这个钩子同时保护 protected和admin 路径,这要感谢before_action。
# app/controllers/api/messages_controller.rb
module Api
class MessagesController < ApplicationController
before_action :authorize!, except: %i[public]
end
自定义错误信息
知道应用程序不再暴露私人的东西,你不觉得更好吗?我肯定会这样做。但我们必须帮助我们的用户。一个空白的401信息可能无法给他们足够的信息来调试问题。让我们扩展这个钩子,返回一些更明确的信息。
# app/controllers/application_controller.rb
class ApplicationController < ActionController::API
def authorize!
valid, result = verify(raw_token(request.headers))
# 👇 old code
# head :unauthorized unless valid
# 👇 new code
render json: { message: result }.to_json, status: :unauthorized unless valid
@token ||= result
end
现在我们得到一个比没有正文的HTTP代码更有用的信息。
测试受保护的端点
让我们测试一下受保护的端点。
curl localhost:6060/api/messages/protected
该命令将返回401,因为我们没有被授权。我们需要从Auth0获得一个有效的令牌。
最简单的方法是再次访问你的Auth0仪表板的API部分,选择你之前创建的API,然后点击测试标签。在这个部分,你可以通过点击复制令牌图标获得一个临时令牌来测试你的网络API,如下图所示。

让我们把令牌附加到请求中。
export TOKEN=the-test-token
curl localhost:6060/api/messages/protected -H "Authorization: Bearer $TOKEN"
请求现在可以工作了!
{"message": "The API successfully validated your access token."}
检查权限
有一个要求有待满足。正如你可能记得的,对于 /api/messages/admin一个有效的token是不够的。相反,我们希望在permissions 要求中有正确的权限。这个额外的检查允许我们为我们的端点定义更精细的访问标准。这个功能在add-rbac分支中实现。
我们在application_controller ,添加一个新的方法,check_permissions 。它接收解码后的令牌,并寻找一个给定的权限。
# app/controllers/application_controller.rb
require 'json_web_token'
class ApplicationController < ActionController::API
# ... existing code ...
def can_read_admin_messages!
check_permissions(@token, 'read:admin-messages')
end
def check_permissions(token, permission)
permissions = token['permissions'] || []
permissions = permissions.split if permissions.is_a? String
unless permissions.include?(permission)
render json: { message: 'Access is denied' }.to_json,
status: :unauthorized
end
end
# ... existing code ...
end
我们再次使用一个before_action ,以保护admin 端点。
# app/controllers/api/messages_controller.rb
module Api
class MessagesController < ApplicationController
before_action :can_read_admin_messages!, only: %i[admin]
# ... existing code ...
end
有了这个,我们的示例应用程序中的每个路由都是安全的,是我们想要的方式。
测试权限
如果你试图调用 /api/messages/admin端点,它将返回401。我们的令牌是有效的,但没有必要的权限。让我们创建一个新的!
回到Auth0仪表板上的API配置页面,选择 "机器对机器 "应用程序标签。你会得到一个应用程序的列表,其中一些名称中带有*(测试应用程序)的标签。当你在那里创建一个API时,这些应用程序是由Auth0自动创建的。它们是作为你的API的测试客户端。其中一个应该有授权的*开关。点击它旁边的下拉菜单,检查 read:admin-messages权限。

然后,点击更新按钮,再次移动到测试标签,并复制已经为你生成的新令牌。让我们再试一次。
export TOKEN=the-new-test-token
curl localhost:6060/api/messages/admin -H "Authorization: Bearer $TOKEN"
请求现在可以工作了
{"message":"The API successfully recognized you as an admin."}
总结
在本指南中,我们通过使用Auth0作为授权服务器为Rails API添加了授权。多亏了jwt gem,我们不需要投入太多的精力。我们使用该库来解码API收到的承载令牌。之后,就是在每次请求前检查相关的路由。