教程 1 - 金钥匙
- 作者:
在 教程 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 并填写以下字段

如何在 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 的数据库角色 |
|
令牌的过期时间戳,以“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"
}