教程 1 - 金钥匙

作者:

begriffs

教程 0 - 启动运行 中,我们创建了一个只读 API,它只有一个端点用于列出待办事项。我们可以从很多方向着手让这个 API 更有趣,但一个好的起点是允许一些用户除了读取数据之外还可以修改数据。

步骤 1. 添加受信任用户

之前的教程在数据库中创建了一个名为 web_anon 的角色,用于执行匿名 Web 请求。让我们创建一个名为 todo_user 的角色,用于通过 API 进行身份验证的用户。这个角色将有权对待办事项列表执行任何操作。

-- run this in psql using the database created
-- in the previous tutorial

create role todo_user nologin;
grant todo_user to authenticator;

grant usage on schema api to todo_user;
grant all on api.todos to todo_user;

步骤 2. 创建密钥

客户端使用 JSON Web 令牌对 API 进行身份验证。这些是 JSON 对象,使用只有服务器知道的密钥进行加密签名。由于客户端不知道这个密钥,因此它们无法篡改令牌的内容。PostgREST 会检测到伪造的令牌并拒绝它们。

让我们创建一个密钥并将其提供给 PostgREST。想一个很长的密钥,或者使用工具生成它。您的密钥必须至少 32 个字符长。

注意

Unix 工具可以为您生成一个不错的密钥

# Allow "tr" to process non-utf8 byte sequences
export LC_CTYPE=C

# Read random bytes keeping only alphanumerics and add the secret to the configuration file
echo "jwt-secret = \"$(< /dev/urandom tr -dc A-Za-z0-9 | head -c32)\"" >> tutorial.conf

检查 tutorial.conf(在之前的教程中创建)是否在 jwt-secret 中设置了密钥

# THE SECRET MUST BE AT LEAST 32 CHARS LONG
cat tutorial.conf

如果 PostgREST 服务器仍在从之前的教程中运行,请重新启动它以加载更新的配置文件。

步骤 3. 签署令牌

通常,您在数据库或其他服务器中的代码将创建和签署身份验证令牌,但对于本教程,我们将“手动”创建一个。转到 jwt.io 并填写以下字段

jwt.io interface

如何在 https://jwt.node.org.cn 上创建令牌

请务必填写您生成的密钥,而不是“secret”一词。 填写完密钥和有效负载后,左侧的编码数据将更新。复制编码的令牌。

注意

虽然令牌看起来很模糊,但很容易反向工程有效负载。令牌只是签署了,而不是加密,因此不要将您不想让有决心的客户端看到的东西放在里面。虽然可以读取令牌的有效负载,但无法读取用于签署它的密钥。

步骤 4. 发出请求

回到终端,让我们使用 curl 添加一个待办事项。请求将包含一个包含身份验证令牌的 HTTP 标头。

export TOKEN="<paste token here>"

curl http://localhost:3000/todos -X POST \
     -H "Authorization: Bearer $TOKEN"   \
     -H "Content-Type: application/json" \
     -d '{"task": "learn how to auth"}'

现在我们已经完成了待办事项列表中的所有三个项目,所以让我们使用 PATCH 请求将它们全部设置为 done 为 true。

curl http://localhost:3000/todos -X PATCH \
     -H "Authorization: Bearer $TOKEN"    \
     -H "Content-Type: application/json"  \
     -d '{"done": true}'

对待办事项的请求显示了其中的三个,并且全部已完成。

curl http://localhost:3000/todos
[
  {
    "id": 1,
    "done": true,
    "task": "finish tutorial 0",
    "due": null
  },
  {
    "id": 2,
    "done": true,
    "task": "pat self on back",
    "due": null
  },
  {
    "id": 3,
    "done": true,
    "task": "learn how to auth",
    "due": null
  }
]

步骤 5. 添加过期时间

目前,我们的身份验证令牌永远有效。服务器只要继续使用相同的 JWT 密钥,就会认可该令牌。

使用 exp 声明为令牌包含过期时间戳是一个更好的策略。这是 PostgREST 特殊处理的两个 JWT 声明之一。

声明

解释

角色

用于为 API 请求执行 SQL 的数据库角色

exp

令牌的过期时间戳,以“Unix 纪元时间”表示

注意

纪元时间定义为自 1970 年 1 月 1 日协调世界时 (UTC) 00:00:00 以来经过的秒数,减去自那时以来发生的闰秒数。

为了观察过期时间的实际效果,我们将向之前的令牌添加一个 exp 声明,该声明在未来五分钟内有效。首先找到五分钟后的纪元值。在 psql 中运行以下命令

select extract(epoch from now() + '5 minutes'::interval) :: integer;

返回 jwt.io 并将有效载荷更改为

{
  "role": "todo_user",
  "exp": 123456789
}

注意:不要忘记将上面代码片段中的虚拟纪元值 123456789 更改为 psql 命令返回的纪元值。

像以前一样复制更新后的令牌,并将其保存为新的环境变量。

export NEW_TOKEN="<paste new token>"

尝试在过期时间之前和之后使用 curl 发出此请求

curl http://localhost:3000/todos \
     -H "Authorization: Bearer $NEW_TOKEN"

过期后,API 返回 HTTP 401 未授权

{
  "code": "PGRST301",
  "details": null,
  "hint": null,
  "message": "JWT expired"
}

附加主题:立即撤销

即使有令牌过期,有时您可能希望立即撤销特定令牌的访问权限。例如,假设您得知一名心怀不满的员工正在做坏事,而他的令牌仍然有效。

为了撤销特定令牌,我们需要一种方法来将其与其他令牌区分开来。让我们添加一个自定义的 email 声明,该声明与发出令牌的客户端的电子邮件匹配。

继续创建一个新的令牌,其有效载荷为

{
  "role": "todo_user",
  "email": "disgruntled@mycompany.com"
}

将其保存到环境变量中

export WAYWARD_TOKEN="<paste new token>"

PostgREST 允许我们在尝试身份验证期间指定要运行的函数。该函数可以执行任何操作,包括引发异常以终止请求。

首先创建一个新的模式并添加该函数

create schema auth;
grant usage on schema auth to web_anon, todo_user;

create or replace function auth.check_token() returns void
  language plpgsql
  as $$
begin
  if current_setting('request.jwt.claims', true)::json->>'email' =
     'disgruntled@mycompany.com' then
    raise insufficient_privilege
      using hint = 'Nope, we are on to you';
  end if;
end
$$;

接下来更新 tutorial.conf 并指定新函数

# add this line to tutorial.conf

db-pre-request = "auth.check_token"

重新启动 PostgREST 使更改生效。接下来尝试使用原始令牌和撤销的令牌发出请求。

# this request still works

curl http://localhost:3000/todos -X PATCH \
     -H "Authorization: Bearer $TOKEN"    \
     -H "Content-Type: application/json"  \
     -d '{"done": true}'

# this one is rejected

curl http://localhost:3000/todos -X PATCH      \
     -H "Authorization: Bearer $WAYWARD_TOKEN" \
     -H "Content-Type: application/json"       \
     -d '{"task": "AAAHHHH!", "done": false}'

服务器响应 403 禁止

{
  "code": "42501",
  "details": null,
  "hint": "Nope, we are on to you",
  "message": "insufficient_privilege"
}