身份验证

PostgREST 旨在将数据库置于 API 安全的中心。所有 授权都在数据库中进行 。PostgREST 的工作是 **验证** 请求 - 即验证客户端是否为他们声称的人 - 然后让数据库 **授权** 客户端操作。

角色系统概述

PostgREST 使用三种类型的角色,即 **验证器**、**匿名** 和 **用户** 角色。数据库管理员创建这些角色并配置 PostgREST 使用它们。

../_images/security-roles.png

鉴权角色用于连接数据库,应配置为具有非常有限的访问权限。它就像一只变色龙,其工作是“变成”其他用户来服务经过身份验证的 HTTP 请求。

CREATE ROLE authenticator LOGIN NOINHERIT NOCREATEDB NOCREATEROLE NOSUPERUSER;
CREATE ROLE anonymous NOLOGIN;
CREATE ROLE webuser NOLOGIN;

注意

名称“authenticator”和“anon”名称是可配置的,并非神圣不可侵犯,我们只是为了清晰起见选择它们。请参阅 db-uridb-anon-role

用户模拟

下图显示了服务器如何处理身份验证。如果身份验证成功,它将切换到请求指定的用户的角色,否则它将切换到匿名角色(如果它在 db-anon-role 中设置)。

../_images/security-anon-choice.png

这种角色切换机制称为 **用户模拟**。在 PostgreSQL 中,它是通过 SET ROLE 语句完成的。

注意

模拟的角色将应用其设置。请参阅 模拟角色设置

基于 JWT 的用户模拟

我们使用 JSON Web 令牌 来验证 API 请求,这使我们能够保持无状态,并且不需要进行数据库查找以进行验证。正如您所知,JWT 包含一个加密签名声明列表。所有声明都是允许的,但 PostgREST 特别关心一个名为 role 的声明。

{
  "role": "user123"
}

当请求包含一个具有 role 声明的有效 JWT 时,PostgREST 将在 HTTP 请求期间切换到具有该名称的数据库角色。

SET LOCAL ROLE user123;

请注意,数据库管理员必须允许鉴权角色切换到此用户,方法是事先执行

GRANT user123 TO authenticator;
-- similarly for the anonymous role
-- GRANT anonymous TO authenticator;

如果客户端没有包含 JWT(或没有 role 声明的 JWT),则 PostgREST 将切换到匿名角色。数据库管理员必须正确设置匿名角色权限,以防止匿名用户查看或更改他们不应该查看或更改的内容。

JWT 生成

您可以从数据库内部(参见 SQL 用户管理)或通过外部服务(参见 外部 JWT 生成)创建有效的 JWT。

客户端身份验证

为了进行身份验证请求,客户端必须包含一个 Authorization HTTP 头,其值为 Bearer <jwt>。例如

curl "http://localhost:3000/foo" \
  -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoiamRvZSIsImV4cCI6MTQ3NTUxNjI1MH0.GYDZV3yM0gqvuEtJmfpplLBXSGYnke_Pvnl0tbKAjB4"

Bearer 头值可以大写或小写使用(bearer)。

JWT 缓存

PostgREST 在每次请求时都会验证 JWTs。我们可以缓存 JWTs 以避免这种性能开销。

要启用 JWT 缓存,需要设置配置 jwt-cache-max-lifetime。它是缓存存储 JWT 验证结果的最长时间(以秒为单位)。缓存使用 exp 声明来设置缓存条目生命周期。如果 JWT 没有 exp 声明,它将使用配置值。有关详细信息,请参见 jwt-cache-max-lifetime

注意

您可以使用 服务器计时头 来查看 JWT 缓存的效果。

对称密钥

每个令牌都使用密钥进行加密签名。在对称加密的情况下,签名者和验证者共享相同的密钥短语,可以通过 jwt-secret 进行配置。如果它被设置为一个简单的字符串值,例如“reallyreallyreallyreallyverysafe”,那么 PostgREST 将将其解释为 HMAC-SHA256 密钥短语。

非对称密钥

在非对称加密中,签名者使用私钥,验证者使用公钥。

配置 部分所述,PostgREST 接受 jwt-secret 配置文件参数。但是,您也可以指定一个字面 JSON Web 密钥 (JWK) 或集合。例如,您可以使用以 JWK 编码的 RSA-256 公钥

{
  "alg":"RS256",
  "e":"AQAB",
  "key_ops":["verify"],
  "kty":"RSA",
  "n":"9zKNYTaYGfGm1tBMpRT6FxOYrM720GhXdettc02uyakYSEHU2IJz90G_MLlEl4-WWWYoS_QKFupw3s7aPYlaAjamG22rAnvWu-rRkP5sSSkKvud_IgKL4iE6Y2WJx2Bkl1XUFkdZ8wlEUR6O1ft3TS4uA-qKifSZ43CahzAJyUezOH9shI--tirC028lNg767ldEki3WnVr3zokSujC9YJ_9XXjw2hFBfmJUrNb0-wldvxQbFU8RPXip-GQ_JPTrCTZhrzGFeWPvhA6Rqmc3b1PhM9jY7Dur1sjYWYVyXlFNCK3c-6feo5WlRfe1aCWmwZQh6O18eTmLeT4nWYkDzQ"
}

注意

如果它包含在一个分配给 keys 成员的数组中,例如 { keys: [jwk1, jwk2] },它也可以是 JSON Web 密钥集 (JWKS)。

只需将其作为单行字符串传入,并转义引号即可。

jwt-secret = "{ \"alg\":\"RS256\", … }"

要生成这样的公钥/私钥对,请使用诸如 latchset/jose 之类的工具。

jose jwk gen -i '{"alg": "RS256"}' -o rsa.jwk
jose jwk pub -i rsa.jwk -o rsa.jwk.pub

# now rsa.jwk.pub contains the desired JSON object

您可以像之前看到的那样指定字面值,也可以引用文件名以从文件中加载 JWK。

jwt-secret = "@rsa.jwk.pub"

JWT 声明验证

PostgREST 遵守 exp 声明以进行令牌过期,拒绝已过期的令牌。

JWT 安全

至少有三种常见的批评针对使用 JWT:1) 针对标准本身,2) 针对使用具有已知安全漏洞的库,以及 3) 针对将 JWT 用于 Web 会话。我们将简要解释每种批评,PostgREST 如何处理它,并为适当的用户操作提供建议。

针对 JWT 标准 的批评在 网络上的其他地方 有详细的说明。与 PostgREST 最相关的部分是所谓的 alg=none 问题。一些实现 JWT 的服务器允许客户端选择用于签署 JWT 的算法。在这种情况下,攻击者可以将算法设置为 none,消除对任何签名的需求,并获得未经授权的访问。但是,PostgREST 的当前实现不允许客户端在 HTTP 请求中设置签名算法,这使得这种攻击无关紧要。针对该标准的批评是它根本需要实现 alg=none

另一种批评集中在 JWT 在维护 Web 会话中的误用。基本建议是停止使用 JWT 进行会话,因为大多数(如果不是全部)针对使用 JWT 时出现的问题的解决方案都不起作用。链接的文章深入讨论了这些问题,但问题的本质是 JWT 不是为客户端存储的安全的、有状态的单元而设计的,因此不适合会话管理。

PostgREST 主要使用 JWT 用于身份验证和授权目的,并鼓励用户也这样做。对于 Web 会话,使用 HTTPS 上的 cookie 就足够了,并且标准 Web 框架可以很好地满足这些需求。

自定义验证

PostgREST 除了 JWT 验证之外,不强制执行任何额外的约束。额外约束的一个例子是立即撤销某个用户的访问权限。使用db-pre-request,您可以指定一个函数,该函数将在用户模拟之后立即调用,并在主查询本身运行之前调用。

db-pre-request = "public.check_user"

在函数中,您可以运行任意代码来检查请求并引发异常(参见使用 HTTP 状态码引发错误),如果需要,可以阻止它。在这里,您可以利用请求头、cookie 和 JWT 声明来执行基于 Web 用户信息的自定义逻辑。

CREATE OR REPLACE FUNCTION check_user() RETURNS void AS $$
DECLARE
  email text := current_setting('request.jwt.claims', true)::json->>'email';
BEGIN
  IF email = 'evil.user@malicious.com' THEN
    RAISE EXCEPTION 'No, you are evil'
      USING HINT = 'Stop being so evil and maybe you can log in';
  END IF;
END
$$ LANGUAGE plpgsql;