
PostgREST 本身并没有权限管理的功能,而是将权限验证下放到了数据库层,通过数据库的角色来控制用户访问数据的权限,而 PostgREST 唯一要做的事就是获取每个请求发起者的角色,然后切换到这个角色再去执行 SQL,成败就看这个角色所具备的权限了。
为了安全的获取用户的角色,PostgREST 使用了 JWT 来传递角色信息。但是 PostREST 只能解析 JWT token,用户登录以及如何生成 jwt token 需要我们自己实现。
在 PostgREST 入门篇中,我们创建了 todos
表并完成了一次查询,下面让我们试试创建一条数据,打开 Postman,用 POST 发起请求。
不出意外的话会得到一个未授权的响应,表示我们没有权限修改 todos
表。这是正常的,因为我们不希望未授权的匿名用户执行任何有风险的操作。接下来我们来解决这一问题。
解析JWT
要让 PostgREST 解析 JWT 非常简单,只需要在配置文件中加一行配置即可:
jwt-secret = "01234567890123456789012345678901"
jwt-secret
是用来给 jwt 签名的,PostgREST 会拿着它去验证 jwt 的合法性。注意这个字符串的长度不能小于32,否则 PostgREST 会将它先进行 SHA256 加密,然后用加密后的字符串做为 jwt 密钥,这也是出于安全性的考虑。
还是延续上一篇的例子,上次我们创建了一个匿名角色 web_anon
,它对 todos
表只有查询权限。现在我们需要再创建一个角色 todo_user
并让它可以修改数据库。
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;
首先我们将 todo_user
的权限授予 authenticator
,这样 PostgREST 就能切换到 todo_user
角色了。然后分别授予 todo_user
角色使用 api schema 的权限以及对 todos
表的所有权限。
万事俱备,现在离发起请求只差一个 jwt token 了。如果是用 curl 发起请求,可以去 jwt 官网生成一个 token。
① 处填写我们的角色,② 处输入前面的 jwt 密钥,复制 ③ 处的 token,它会自动更新,然后在请求头中设置 Authorization 字段。
Authorization:Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoidG9kb191c2VyIn0.u5zRwwjJJFkMhYubeemYXq9iT_JTRERmM0V5CX1UiSE
如果使用 Postman 则没有这么麻烦,直接在 Authorization 中填入相关信息就可以了。
① 处选择 JWT Bearer,② 处保持默认,③ 处输入 jwt 密钥,④ 处填入角色,最后点击发送即可。不出意外的话,会看到一个没有内容的 201 Created
响应。
上面我们选择了 HS256,这是一种使用 HMAC-SHA256 算法的对称加密,签发方和验证方使用相同的密钥。有对称就有非对称,PostgREST 也是支持非对称加密的。
非对称加密
在非对称加密中使用的是 RS256 算法,密钥也不叫 JWT,而是叫 JWK,Json Web Key。RS256 使用了 RSA 非对称加密算法来生成公钥和私钥。客户端持有私钥进行加密,PostgREST 持有公钥进行解密。
不管是对称还是非对称,加密算法只能是这两个,不支持客户端指定算法,这也在一定程度上避免了恶意客户端选择不加密的风险。
开始之前,首先我们需要获得一对公钥和私钥。做为演示,我们可以去 mkjwk.org 网站在线生成。官方推荐了一个 latchset/jose 工具,不过貌似只有 Linux 版本。
① 处选择 Signature,② 处选择 RS256 算法,③ 处随便写点什么,左边下拉还能选择其他选项,比如再做一次加密,日期或时间戳等,④ 处我们选择 Yes 才能看到最下面的 X.509 PEM 格式的私钥,最后点击 Generat 生成即可。
接下来我们在 tutorial.conf
文件同级目录下创建一个名为 rsa.jwk.pub
文件,将上图中右上角的 Public Key 下的内容复制,粘贴到 rsa.jwk.pub
文件中。打开 tutorial.conf
文件,将 jwt-secret
配置修改如下:
jwt-secret = "@rsa.jwk.pub"
@
表示从文件中读取内容,文件名可以随意。当然也可以直接将 jwk 做为字符串设置,注意 json 中的双引号需要转义,但是这样不便于修改,所以还是推荐从文件读取。
jwt-secret = "{ \"alg\":\"RS256\", … }"
最后别忘重启 PostgREST 服务。
然后我们回到 Postman,在 Authorization 选项中,使用非对称加密。
① 处选择 RS256,回到生成公私钥的网站,将左下角 Private Key (X.509 PEM Format) 下的内容复制粘贴到 ② 处,注意不要点最下面的复制到剪贴板,因为它会把私钥复制成一个字符串,换行会被替换成 \n
,这样是不对的,直接全选复制。
点击 Send 按钮,不出意外的话,会看到 201 Created
响应。除了 JWK,也同样支持 JWKS,格式为 {"keys": [{jwk2}, {jwk2}]}
。
其他字段
在 jwt 的负载中,除了 role
,PostgREST 还支持以下 jwt 字段:
exp
:指定 token 的过期时间。iat
:Issued At,token 签发时间。nbf
:Not Before,token 生效的起始时间。aud
:Audience,受众。用来指定 token 的接收方,当aud
与当前服务名不匹配时,可以拒绝该 token。它可以是单个字符串或者一个 json 字符串列表,表示有多个受众。
nbf
和 exp
构成了一个时间区间,这个区间就是 token 的生效时间范围。以上都是 jwt 标准保留声明,也就是 jwt 规范的标准字段。但是 role
并不是,它是我们自定义的,在 PostgREST 配置文件中通过 jwt-role-claim-key
配置,默认是 role
。
## Jspath to the role claim key
jwt-role-claim-key = ".role"
此外我们还可以通过 jwt-aud
来配置 PostgREST 的服务名,不过这里只能配置单个字符串,毕竟一个服务只有一个名字是正常的。
jwt-aud = "service_a"
前置验证
按理来说,PostgREST 验证完 jwt token,下一步就是执行 SQL 查询了。但是这里有一个问题,假设我们不小心签发了一个 token 给恶意用户,由于 token 是有有效期的,在 token 的有效期内,恶意用户可以为所欲为,那么阁下又当如何应对呢?
PostgREST 允许通过 db-pre-request
配置一个函数,它会在执行 SQL 之前调用,实现拦截的效果。比如我们可以定义以下 PostgreSQL 函数。
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;
我们定义了一个叫 check_user
的函数,然后从 jwt 中提取 email
字段,为此我们在签发 token 的时候就要先把 email
加上。最后判断如果邮箱是 evil.user@malicious.com
就引发一个异常。
在配置文件中加上下面这行:
db-pre-request = "public.check_user"
这样我们就能立即撤回误发的 token 了,做为示例这里邮箱匹配是写死的字符串,更进一步也可以去查表来获取失效的邮箱地址。
以上就是本期的全部内容,但是我们并没有涉及 jwt token 的签发,我们可以选择使用第三方服务,比如 Auth0 ,也可以通过 PostgreSQL 函数来签发 token,不过这是后面的内容了。
最后只有对外提供服务时才需要用到 jwt 权限验证,如果是一个受信任的内网服务,那么直接给匿名角色足够的权限也是完全合理的。