所有文章 > API开发 > 使用 Auth0 向 Sinatra API 添加授权

使用 Auth0 向 Sinatra API 添加授权

Sinatra是开发Web应用程序和API的热门Ruby框架之一,被广泛应用于超过200,000个应用程序中。Sinatra作为一种领域特定语言(DSL),允许开发者在Ruby中直接创建Web应用程序和API。与典型的Model-View-Controller框架不同,Sinatra将特定的URL直接映射到相关的Ruby代码,并将代码的输出作为响应返回。

在这篇博文中,您将学习如何构建Sinatra API,并使用Auth0来保护其端点。我们将一起构建Sinatra Songs API,这是一个歌曲CRUD API。值得一提的是,这个API的命名灵感来源于Frank Sinatra,因为如果没有Frank Sinatra,也就没有 Sinatra API的命名由来!😉

项目要求

对于此项目,您将使用以下版本:

  • Ruby 3.1.2
  • Sinatra 3.0.2
  • 一个 Auth0 帐户

您将从零开始构建Sinatra Songs API。如果遇到困难,您可以查阅项目存储库。该存储库包含两个分支:main分支提供了歌曲CRUD API的代码,而add-authorization分支则包含了连接Auth0并保护终端节点的代码。

构建歌曲 API

首先,在终端中创建一个新项目。新建一个名为“sinatra-auth0-songs-api”的文件夹,并将其设置为当前工作目录。

安装 Sinatra

接下来,安装 Sinatra。为此,您需要在项目的根目录中创建一个名为“Gemfile”的文件,并在其中列出所有依赖项。

# Gemfile 

# frozen_string_literal: true

source 'https://rubygems.org'

ruby File.read('.ruby-version').strip

gem 'sinatra', '~> 3.0', '>= 3.0.2'
gem 'puma'

您可以在.ruby-version文件中指定Ruby版本,这是我个人偏好的常见做法。正如Bundler文档所阐释的,如果您依赖Ruby虚拟机(VM)中的特定功能,这样做能让您的应用程序在遇到不兼容时更快地失败。如此一来,部署服务器上的Ruby VM将与本地VM保持一致。为此,您需要在.ruby-version文件中填入您将使用的Ruby版本号。

3.1.2

最后,通过运行以下命令来安装 Gem:

bundle install

就这样,Sinatra 🎩 被安装了!您还安装了 puma 作为 Web 服务器。

创建歌曲模型

让我们创建一个类来表示一首歌。在该目录中新建一个名为“models”的文件夹,并在其中创建一个新文件“song.rb”。

使用以下代码填充“song.rb”文件。

# models/song.rb

# frozen_string_literal: true

# Class to represent a Song
class Song
attr_accessor :id, :name, :url

def initialize(id, name, url)
@id = id
@name = name
@url = url
end

def to_json(*a)
{
'id' => id,
'name' => name,
'url' => url
}.to_json(*a)
end
end

您正在定义一个名为Song的类,该类具有三个属性:idnameurl。此外,您还将为Song类实现一个更专业的to_json方法,以便在控制器中将歌曲渲染为JSON时,该方法可以作为序列化程序使用。

实施 CRUD API

到目前为止,您主要使用了Ruby语言;现在,是时候亲身体验一下Sinatra框架了。

请在终端中创建一个新文件,命名为api.rb,并将以下内容添加到该文件中,作为API的基本框架。

# api.rb

# frozen_string_literal: true

require 'sinatra'
require 'json'

before do
content_type 'application/json'
end

get '/songs' do
return {todo: :implementation}.to_json
end

get '/songs/:id' do
return {todo: :implementation}.to_json
end

post '/songs' do
return {todo: :implementation}.to_json
end

put '/songs/:id' do
return {todo: :implementation}.to_json
end

delete '/songs/:id' do
return {todo: :implementation}.to_json
end

让我们分解一下“api.rb”文件中的内容。

首先,您需要加载“sinatra”和“json”这两个gem。

require 'sinatra'
require 'json'

与 Rails 不同,在 Sinatra 中,您必须自行加载所有内容。这可能很棒,因为它通过迫使你明确正在使用的东西,从而消除了 Rails 的所有魔力🔮。

接下来,您将定义一个过滤器:before。

before do
content_type 'application/json'
end

正如Sinatra文档所阐述的,before过滤器会在每次请求处理之前进行评估。

在此情境下,您需要将Content-Type头部设置为application/json,以此告知客户端,来自此服务器的所有响应均采用JSON格式。

接下来,我们将着手定义路由。

get '/songs' do
# ...
end
get '/songs/:id' do
# ...
end
post '/songs' do
# ...
end
put '/songs/:id' do
# ...
end
delete '/songs/:id' do
# ...
end

这些路由代表了您将要实现的CRUD(创建、读取、更新、删除)操作。

  • 创建:使用POST方法访问/songs
  • 读取:使用GET方法访问/songs,以及使用GET方法访问带有特定ID的/songs/:id
  • 更新:使用PUT方法访问带有特定ID的/songs/:id
  • 删除:使用DELETE方法访问带有特定ID的/songs/:id

嗯,虽然严格来说它更像是CRRUD(因为读取操作有两个),但您已经明白了主要意思。🫠

有了这个API框架,您就可以启动服务器并测试各个端点了。

要从终端运行服务器:

ruby api.rb

服务器运行后,您的终端将如下所示:

➜  sinatra-auth0-songs-api git:(main) ✗ ruby api.rb 
== Sinatra (v3.0.2) has taken the stage on 4567 for development with backup from Puma
Puma starting in single mode...
* Puma version: 6.0.0 (ruby 3.1.2-p20) ("Sunflower")
* Min threads: 0
* Max threads: 5
* Environment: development
* PID: 98050
* Listening on http://127.0.0.1:4567
* Listening on http://[::1]:4567
Use Ctrl-C to stop

现在,您可以访问终端节点。我创建了一个 POSTMAN Collection,因此您可以自行测试这些终端节点。您也可以像这样使用:http://localhost:4567(通过 curl 命令)。

➜ curl -v http://localhost:4567/songs 

{"todo":"implementation"}%

使用文件填充 API

为了向API中填充一些数据,您可以使用songs.json文件。这个文件是通过LastFM API获取的数据填充的,包含了Frank Sinatra的Top 10 Tracks,并且格式是LastFM的一个简化版本。您可以下载这个文件来使用。

{
"id": 1,
"name": "My Way",
"url": "https://www.last.fm/music/Frank+Sinatra/_/My+Way"
}

让我们实现一个帮助程序,它将在 Sinatra API 启动后负责从 songs.json 文件中读取数据。

为此,请创建一个名为 helpers 的新文件夹,并在其中添加一个名为 songs_helper.rb 的文件。

# helpers/songs_helper.rb

# frozen_string_literal: true

require_relative '../models/song'
require 'json'

# Class to read songs from a JSON file
class SongsHelper
def self.songs
filepath = File.join(File.dirname(__FILE__), '../songs.json')
file = File.read(filepath)
data = JSON.parse(file)['songs']

data.map do |song|
Song.new(song['id'], song['name'], song['url'])
end
end
end

该类实现了一个方法,用于读取文件并将其内容映射到对象数组中。SongsHelper 类读取 songs.json 文件中的歌曲。

接下来,在你的 api.rb 文件中,你可以调用 SongsHelper 类的 songs 方法来加载歌曲。

# api.rb 

# frozen_string_literal: true

require 'sinatra'
require 'json'
# 👇 new code
require_relative 'helpers/songs_helper'

songs ||= SongsHelper.songs
# 👆 new code

# existing code ...

您正在导入文件,并调用其中的方法将数据存储在名为songs的变量中。这个变量是通过helpers/songs_helper模块中的songssongs方法获取的。

请注意,在实际的应用程序中,您会使用一个合适的数据库来存储数据,并且无需执行从文件导入数据的这一步骤。但为了简化本教程,我们没有使用数据库,而是直接处理来自songs.json文件的数据。

现在,您可以使用这个songs变量来管理GET请求,例如获取所有歌曲的请求可以命名为GET /songs

# api.rb 

# frozen_string_literal: true

require 'sinatra'
require 'json'
require_relative 'helpers/songs_helper'

songs ||= SongsHelper.songs

before do
content_type 'application/json'
end

# 👇 new code
get '/songs' do
return songs.to_json
end
# 👆 new code

# existing code ...

该请求现在将使用 curl 命令通过 GET 方法检索一组歌曲,访问的 URL 是 /songs

➜ curl http://localhost:4567/songs 
[
{"id":1,"name":"My Way","url":"https://www.last.fm/music/Frank+Sinatra/_/My+Way"},
{"id":2,"name":"Strangers in the Night","url":"https://www.last.fm/music/Frank+Sinatra/_/Strangers+in+the+Night"},
{"id":3,"name":"Fly Me to the Moon","url":"https://www.last.fm/music/Frank+Sinatra/_/Fly+Me+to+the+Moon"},
{"id":4,"name":"That's Life","url":"https://www.last.fm/music/Frank+Sinatra/_/That%27s+Life"},
{"id":5,"name":"I've Got You Under My Skin","url":"https://www.last.fm/music/Frank+Sinatra/_/I%27ve+Got+You+Under+My+Skin"},
{"id":6,"name":"Come Fly With Me","url":"https://www.last.fm/music/Frank+Sinatra/_/Come+Fly+With+Me"},
{"id":7,"name":"The Way You Look Tonight","url":"https://www.last.fm/music/Frank+Sinatra/_/The+Way+You+Look+Tonight"},
{"id":8,"name":"Fly Me to the Moon (In Other Words)","url":"https://www.last.fm/music/Frank+Sinatra/_/Fly+Me+to+the+Moon+(In+Other+Words)"},
{"id":9,"name":"Theme from New York, New York","url":"https://www.last.fm/music/Frank+Sinatra/_/Theme+from+New+York,+New+York"},
{"id":10,"name":"Jingle Bells","url":"https://www.last.fm/music/Frank+Sinatra/_/Jingle+Bells"}
]%

现在,让我们来实现歌曲详细信息的路由。为此,我们需要引入 helpers 的概念,并创建一个新的 helper。路由将是 /songs/:id

接下来,在您的 api.rb 文件中,添加相应的内容。

# frozen_string_literal: true

require 'sinatra'
require 'json'
require_relative 'helpers/songs_helper'

songs ||= SongsHelper.songs

# 👇 new code
helpers do
def id_param
halt 400, { message: 'Bad Request' }.to_json if params['id'].to_i < 1
params['id'].to_i
end
end
# 👆 new code

# existing code ...

在 Sinatra 中,帮助程序(helpers)指的是那些在路由处理程序和模板中均可使用的顶级方法。

在本例中,您将定义一个帮助程序方法。这个方法首先会检查一个哈希值是否存在,这个哈希值是Sinatra在路由块中自动为您提供的,它包含了来自请求的相关数据。这个特定的哈希键我们称之为id_param

id_param方法中,如果对应的值不是正数,则会返回一个错误提示。如果它是一个有效的正数值,则方法会返回这个值并将其转换为整数类型。您将在所有需要用到id参数的路由中使用这个方法,例如:在GET、PUT和DELETE请求处理/songs/:id这个路由时。

现在,回到api.rb文件,您可以通过使用这个helper方法来实现获取、更新和删除歌曲详细信息的路由,如下所示(注意,这里我们假设id_param方法已经在文件中被正确定义):

  • 对于GET请求/songs/:id,使用id_param来获取并验证歌曲ID。
  • 对于PUT请求/songs/:id(原文中写的是“放”,这里应该是PUT的误写),同样使用id_param
  • 对于DELETE请求/songs/:id,也是使用id_param

在这些路由处理程序中,如果id_param方法检测到无效的ID,可以相应地返回“Bad Request”错误。

# existing code ... 

get '/songs' do
return songs.to_json
end

get '/songs/:id' do
# 👇 new code
song = songs.find { |s| s.id == id_param }
halt 404, { message: 'Song Not Found' }.to_json unless song

return song.to_json
# 👆 new code
end

# existing code ...

您正在使用 Ruby 的 Enumerable#find 方法在数组中查找具有通过 params 发送的 ID 的歌曲。如果未找到对应的歌曲,则返回 404 NOT FOUND 错误。否则,您将以 JSON 格式返回该歌曲的信息。

让我们现在使用:curl

➜ curl http://localhost:4567/songs/1

{"id":1,"name":"My Way","url":"https://www.last.fm/music/Frank+Sinatra/_/My+Way"}%

此时,您已经实现了 Songs API 中的两个读取路由。接下来,是时候实现创建、更新和删除路由了。

让我们从创建(create)路由开始。您可以通过提供一个包含歌曲名称的 JSON 对象来发起 POST 请求。该 POST 请求的 URL 可以通过 curl 命令来发起,如下所示:

curl -X POST 'http://localhost:4567/songs' \
-H 'Content-Type: application/json' \
-d '{
"name": "A new song",
"url": "http://example.com"
}'

在更新歌曲信息时,您需要在请求正文中传递nameurl参数,并确保它们具有正确的JSON格式。这是实现相关帮助程序的一个关键提示。

接下来,我们将实现一个新的帮助程序,命名为json_params,它的作用是检查请求的正文是否符合JSON格式要求。

现在,请在api.rb文件中添加实现json_params帮助程序的代码。

# api.rb

# frozen_string_literal: true

require 'sinatra'
require 'json'
require_relative 'helpers/songs_helper'

songs ||= SongsHelper.songs

helpers do
# existing code ...

# 👇 new code
def json_params
request.body.rewind
@json_params ||= JSON.parse(request.body.read).transform_keys(&:to_sym)
rescue JSON::ParserError
halt 400, { message: 'Invalid JSON body' }.to_json
end
# 👆 new code

# existing code ...
end

# existing code ...

该方法从 request.body 中读取 JSON 数据。如果在使用 JSON.parse 解析时遇到 JSON::ParseError,则表示请求体不是有效的 JSON 格式,此时该方法将返回 400 Bad Request 错误。

此外,您还应该验证请求体中的参数是否仅包含必需的参数:name 和 url。为了实现这一验证逻辑,我们可以创建一个新的 helper 方法。

# api.rb

# existing code ...

helpers do
# existing code ...

def json_params
request.body.rewind
@json_params ||= JSON.parse(request.body.read).transform_keys(&:to_sym)
rescue JSON::ParserError
halt 400, { message: 'Invalid JSON body' }.to_json
end

# 👇 new code
def require_params!
json_params

attrs = %i[name url]

halt(400, { message: 'Missing parameters' }.to_json) if (attrs & @json_params.keys).empty?
end
# 👆 new code

# existing code ...
end

# existing code ...

该方法将是您在路由中使用的主要方法。首先,它调用 require_params! 以初始化实例变量 @json_params,并在上下文中使其可用。然后,该方法验证 @json_params 是否包含 name 键且不包含其他未预期的参数。如果验证失败,它将返回 400 Bad Request 状态码。

require_params! 方法会检查 json_params(通过参数传递或从请求体中解析得到)是否满足要求。

只有在创建和更新歌曲时,才需要 name 参数。为此,您可以创建一个 before 过滤器来完成此验证操作。

接下来,在 api.rb 中,添加以下内容来定义这个 before 过滤器:

# frozen_string_literal: true

require 'sinatra'
require 'json'
require_relative 'helpers/songs_helper'

songs ||= SongsHelper.songs

# 👇 new code
set :method do |*methods|
methods = methods.map { |m| m.to_s.upcase }
condition { methods.include?(request.request_method) }
end
# 👆 new code

helpers do
# ... existing code
end

before do
content_type 'application/json'
end

# 👇 new code
before method: %i[post put] do
require_params!
end
# 👆 new code

让我们进一步分析并明确您的需求。您提到了两个新增的元素:一个是变量(虽然您写的是“a”,但根据上下文,我假设您可能想指的是某个具体的变量或设置项,但此处我们将其忽略,因为重点在于后续内容),另一个是您已经了解的before过滤器。

set方法是Sinatra框架提供的一个功能,它允许您为应用程序设置属性。这个方法接受两个参数:设置名称和对应的值。

在您的情况中,您打算使用set方法来标识HTTP方法。具体做法是,您会设定一个名称(,并将一个包含HTTP方法名称的数组作为参数传递给它。随后,您会利用条件语句来确保before过滤器仅在特定条件下执行,也就是当HTTP方法是POSTPUT时。

before过滤器内部,您会传递一个HTTP方法的符号列表,以指明这段代码应在哪些HTTP请求中执行。紧接着,您可能会调用一个自定义的方法,用于确保请求中包含了必要的参数。

现在,我们来关注api.rb文件中创建和更新歌曲的代码实现。

首先,为了创建新歌曲,您需要处理POST /songs请求。

# existing code ...

before method: %i[post put] do
require_params!
end

# existing code ...

# 👇 new code
post '/songs' do
create_params = @json_params.slice(:name, :url)

if create_params.keys.sort == %i[name url]
new_song = { id: songs.size + 1, name: @json_params[:name], url: @json_params[:url] }
else
halt(400, { message: 'Missing parameters' }.to_json)
end

songs.push(new_song)

return new_song.to_json
end
# 👆new code

# existing code ...
end
# existing code ...

这条路由旨在简化歌曲创建过程。首先,它会验证 params 哈希中是否存在 name 和 url 这两个键;请注意,之前的过滤器已经确保了这两个参数是传递的唯一参数。如果这两个参数都存在,那么可以创建一首新歌曲。这里需要说明的是,您只是简单地将一个标识符递增1,并将新歌曲对象推送到一个数组中;在实际应用中,您通常会在数据库中创建新记录。如果缺少 name 或 url 参数中的任何一个,则路由会返回 400 Bad Request 错误。

接下来,我们继续为 update 路由添加代码。

# api.rb 
# existing code ...

# 👇 new code
put '/songs/:id' do
song = songs.find { |s| s.id == id_param }

halt 404, { message: 'Song Not Found' }.to_json unless song

song.name = @json_params[:name] if @json_params.keys.include? :name
song.url = @json_params[:url] if @json_params.keys.include? :url

return song.to_json
end
# 👆new code

# existing code ...
end

当请求更新歌曲时,您的代码会尝试使用 id_param(从请求中提取的歌曲ID)在歌曲数组中查找该歌曲。如果未找到对应的歌曲,则返回 404 Not Found 错误。如果找到了歌曲,则仅更新请求正文中发送的字段,并以 JSON 格式返回更新后的歌曲信息。

最后,还有删除歌曲的路由需要实现。让我们将其添加到 api.rb 文件中:实现一个 DELETE 请求处理,路径为 /songs/:id

# api.rb 
# existing code ...

# 👇 new code
delete '/songs/:id' do
song = songs.find { |s| s.id == id_param }
halt 404, { message: 'Song Not Found' }.to_json unless song

song = songs.delete(song)

return song.to_json
end
# 👆new code

# existing code ...
end

delete song 方法与 update song 方法非常相似,但它调用 Array#delete 函数并以 JSON 格式呈现歌曲。

您的 Songs API 已完成!但未得到保障😩。此时,您的代码必须与存储库的 main 分支上的代码非常相似。

使用 Auth0 保护您的终端节点

到目前为止,您已经成功构建了一个CRUD Songs API,但目前该API的终端节点对所有人都是开放的。为了确保只有经过授权的用户才能执行创建、更新和删除歌曲的操作,您计划使用Auth0作为身份访问管理(IAM)提供商。

接下来,您将实现已在add-authorization分支中完成的代码,该代码将作为您操作的指南。

将您的 Sinatra API 与 Auth0 连接

要将您的Sinatra API与Auth0进行连接,您需要先在Auth0控制面板的API部分创建一个新的Auth0 API。具体操作步骤如下:

  • 名称:Sinatra Songs API
  • 标识符https://sinatra-auth0-songs-api
  • 签名算法:RS256(这通常是默认选择)
创建新的 Auth0 API

在设置 Sinatra API 时,您需要复制一个值(此处未具体说明是哪个值,可能是指某个配置参数或令牌)。此外,您还需要获取您的 Auth0 域。除非您已配置自定义域,否则此值通常为您的租户名称和区域组成的 URL,形如 https://[TENANT_NAME].[REGION].auth0.com。如果您不确定此值的具体内容,可以打开 API 设置中的“Test”选项卡,并查看“Asking Auth0 for tokens from my application”部分下的代码示例,其中会包含这个参数的示例值:identifier

在 API 的 test 部分查找 Auth0 域

创建 API 后,您可以前往命令行并开始安装依赖项。

安装依赖项

您将需要一些 gem,因此让我们继续将它们添加到Gemfile

gem 'dotenv'
gem 'jwt'

接下来,在终端中运行:

bundle install

您正在安装 dotenv gem,以便从本地文件中读取环境变量。您可以使用 .env.example 文件作为模板,并将其内容复制到项目根目录中的一个名为 .env 的文件中。

请记住上一步,您必须保存您的 Auth0 域和标识符。嗯,这就是您可以在 .env 文件中使用它们的地方。

将 AUTH0_DOMAIN 和 AUTH0_IDENTIFIER 粘贴到您的 .env 文件中。

您还安装了 JWT gem,它是 JWT 标准的 Ruby 实现,稍后将帮助您验证 JWT 令牌,稍后您将了解有关这些令牌的更多信息。

验证访问令牌

为了保护 API 的终端节点,您将使用所谓的基于令牌的授权。基本上,您的 Sinatra Songs API 将接收一个访问令牌;传递的访问令牌会告知 API,令牌的持有者已被授权访问 API 并执行范围指定的特定操作。

最后,您的 API 将通过确保访问令牌具有正确的结构,并且它是由正确的授权服务器(在本例中为 Auth0)颁发的,来验证访问令牌。

创建 Auth0 Client 类

为了验证访问令牌,您需要首先创建一个新的类来专门处理这一过程。

接下来,请在您的文件夹中新建一个文件,命名为helpers/auth0_client_helper.rb,并向其中添加相应的代码。

# helpers/auth0_client_helper.rb

# frozen_string_literal: true

require 'jwt'
require 'net/http'

# AuthoClient helper class to validate JWT access token
class Auth0ClientHelper
# Auth0 Client Objects
Error = Struct.new(:message, :status)
Response = Struct.new(:decoded_token, :error)

# Helper Functions
def self.domain_url
"https://#{ENV['AUTH0_DOMAIN']}/"
end

def self.decode_token(token, jwks_hash)
JWT.decode(token, nil, true, {
algorithm: 'RS256',
iss: domain_url,
verify_iss: true,
aud: (ENV['AUTH0_AUDIENCE']).to_s,
verify_aud: true,
jwks: { keys: jwks_hash[:keys] }
})
end

def self.get_jwks
jwks_uri = URI("#{domain_url}.well-known/jwks.json")
Net::HTTP.get_response jwks_uri
end

# Token Validation
def self.validate_token(token)
jwks_response = get_jwks

unless jwks_response.is_a? Net::HTTPSuccess
error = Error.new(message: 'Unable to verify credentials', status: :internal_server_error)
return Response.new(nil, error)
end

jwks_hash = JSON.parse(jwks_response.body).transform_keys(&:to_sym)

decoded_token = decode_token(token, jwks_hash)
Response.new(decoded_token, nil)
rescue JWT::VerificationError, JWT::DecodeError
error = Error.new('Bad credentials', 401)
Response.new(nil, error)
end
end

这个类中包含了一些内容,这些内容在《Rails API 授权示例指南》中得到了详尽的解释,特别是在“Auth0Client 类在后台的运作机制”这一章节下的“验证 JSON Web Token(JWT)”部分。当然,我已经做了一些调整,将原本适用于 Rails 的代码修改为适用于 Sinatra,但核心思想依然保持不变。

为了深入了解这些安全相关的概念,你可以参考《Rails 认证示例指南》和《Rails 授权示例指南》(后者还介绍了基于角色的访问控制(RBAC)的概念)。

不过,现在让我们聚焦于这个类中的一个核心方法:validate_token 方法。

def self.validate_token(token)
jwks_response = get_jwks

unless jwks_response.is_a? Net::HTTPSuccess
error = Error.new(message: 'Unable to verify credentials', status: :internal_server_error)
return Response.new(nil, error)
end

jwks_hash = JSON.parse(jwks_response.body).transform_keys(&:to_sym)

decoded_token = decode_token(token, jwks_hash)
Response.new(decoded_token, nil)
rescue JWT::VerificationError, JWT::DecodeError
error = Error.new('Bad credentials', 401)
Response.new(nil, error)
end

让我们分解一下validate_token方法的作用:

  1. 首先,您调用该方法,该方法总之调用 Auth0 的终端节点,并返回用于验证租户的所有 Auth0 颁发的 JWT 的 JSON Web 密钥集 (JWKS)。如果获取 JWKS 时出错,则引发错误,因为无法验证令牌。
  2. 接下来,将 JWKS 解析为哈希值,以便更轻松地在 Ruby 中使用。
  3. 最后,调用validate_token 方法,该方法使用 JWT Gem 对访问令牌进行解码,如下所示:
JWT.decode(token, nil, true, {
algorithm: 'RS256',
iss: domain_url,
verify_iss: true,
aud: (ENV['AUTH0_AUDIENCE']).to_s,
verify_aud: true,
jwks: { keys: jwks_hash[:keys] }
})

这会从环境变量中获取 u,并在值中设置 u。最后,在参数中传递您之前创建的 domain_urlAUTH0_DOMAINAUTH0_AUDIENCE 和 jwks_hashjwks

要了解有关参数的更多信息,可以参考 Rails API Authorization By Example Developer Guide 中的“Auth0Client 类在后台做什么?”以及 JWT.decode

创建帮助程序

创建帮助程序 authorize!,该类已经在执行大部分工作来验证访问令牌。现在,您需要在要保护的终端节点中实际调用它。使用 Auth0ClientHelper,类似于您之前使用它的方式。

转到您的 api.rb 文件并添加相关代码。

# api.rb 

# existing code ...

helpers do
# existing code ...

# 👇 new code
def authorize!
token = token_from_request

validation_response = Auth0ClientHelper.validate_token(token)

return unless (error = validation_response.error)

halt error.status, { message: error.message }.to_json
end

def token_from_request
authorization_header_elements = request.env['HTTP_AUTHORIZATION']&.split

halt 401, { message: 'Requires authentication' }.to_json unless authorization_header_elements

unless authorization_header_elements.length == 2
halt 401, { message: 'Authorization header value must follow this format: Bearer access-token' }.to_json
end

scheme, token = authorization_header_elements

halt 402, { message: 'Bad credentials' }.to_json unless scheme.downcase == 'bearer'

token
end
# 👆 new code
end

# existing code ...

好吧,您实际上定义了两个帮助程序,但这两个帮助程序是互为辅助的。token_from_request 帮助程序通过调用另一个方法从请求中提取令牌。该方法会检查 HTTP 标头,并对其进行解析以验证其格式是否正确。而 authorize! 帮助程序则依赖于 token_from_request 来获取令牌。

使用 Bearer 模式时,格式正确的 Authorization 标头如下所示:

Authorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImlhdCI6MTUxNjIzOTAyMn0.NHVaYe26MbtOYhSKkoKYdFVomg4i8ZJd8_-RU8VNbftc4TSMb4bXP3l3YlNWACwyXPGffz5aXHc6lty1Y2t4SWRqGteragsVdZufDn5BlnJl9pdR_kdVFUsra2rWKEofkZeIC4yWytE58sMIihvo9H1ScmmVwBcQP6XETqYd0aSHp1gOa9RdUPDvoXQ5oqygTqVtxaDr6wUFKrKItgBMzWIdNZ6y7O9E0DhEPTbE9rfBo6KTFsHAZnMg4k68CDp2woYIaXbmYTWcvbzIuHO7_37GT79XdIwkm95QJ7hYC9RiwrV7mesbY4PAahERJawntho0my942XheVLmGwLMBkQ

然后,该方法会验证请求标头中是否存在 Authorization,令牌是否存在,以及它是否具有正确的格式(例如,以 “Bearer ” 开头)。如果任何一项检查失败,它将返回 401 Unauthorized 状态码。

从请求标头中检索到令牌后,帮助程序(我们假设它是一个辅助方法或类)将调用 Auth0ClientHelper 类中的 authorize! 方法来验证令牌。如果令牌经过验证且没有错误,则 authorize! 方法将正常完成其执行流程。如果在验证过程中出现任何错误,authorize! 方法会返回一个包含正确状态码和错误消息的 Error 对象。

使用帮助程序保护您的 API 端点

要使用这个帮助程序保护您的 API 端点,您需要在任何客户端尝试调用这些端点之前调用 authorize! 方法。

保护终端节点的最后一步是实现一个过滤器,该过滤器会在请求到达端点之前被触发。在 Sinatra 中,这通常是通过 before 过滤器来实现的。

在您的 api.rb 文件中,您可能已经有一个可以重复使用的过滤器,因此让我们修改它,以便在调用任何需要保护的端点之前都执行 authorize! 方法。

# api.rb 
# existing code ...

# old code
# before method: %i[post put] do
# require_params!
# end
# old code

# 👇 new code
before method: %i[post put delete] do
require_params!
authorize!
end
# 👆 new code
# existing code ...

首先,您将该方法添加到过滤器中,因为您希望只有授权用户能够创建、更新和删除歌曲。deletebefore

接着,在过滤器中调用将执行授权验证的帮助程序 authorize!

就是这样!现在,您已经设置好了授权验证,可以使用 curl 命令来测试相关的终端节点。

curl -X POST 'http://localhost:4567/songs' \
-H 'Authorization: Bearer YOUR_TOKEN' \
-d '{
"name": "A new song"
"url": "http://example.com"
}'

将占位符替换为有效的访问令牌后,此请求的结果将如下所示:

{"id":11,"name":"A new song","url":"http://example.com"}

要获取 API 的有效访问令牌,请按照将 Sinatra API 与 Auth0 连接部分中显示的步骤进行操作。

总结

在这篇博文中,您了解了 Ruby 框架 Sinatra,并学习了如何创建一个基本的 CRUD API 来管理 Frank Sinatra 的歌曲。

您首先通过控制面板创建了一个新的 Auth0 账户和一个新的 API。接着,您使用了 JWT gem 来验证由 Auth0 颁发的访问令牌。最后,您通过实施基于令牌的授权(采用 Bearer 方案)保护了用于创建、更新和删除歌曲的 API 终端节点。

我希望您喜欢这篇文章。您是否使用过其他的 Ruby 框架呢?请在评论中告诉我!

感谢阅读!

原文链接:https://auth0.com/blog/add-authorization-to-sinatra-api-using-auth0/