PostgREST 身份认证方案调研

通过 PostgREST 我们可以将数据库表映射成 REST API,避免繁琐的增删改查或 ORM 过程。这个方案非常适合原型开发或个人项目开发。

最近我将自己做的一个小系统的数据库从 SQLite 迁移到 PostgreSQL。系统后端接口从原先的 SQLite + Soul 方案调整成 PostgreSQL + PostgREST 方案,所以用户身份校验流程随之也做了一些调整。

以下内容是调整过程中的一些调研。

方案选型

一个大的原则是,使用 JWT(JSON Web Token) 来对请求进行身份校验。

PostgREST 的设计哲学是 “authorization happens in the database “。也就是说,校验是由 PostgREST 以及底层的 PostgreSQL 数据库完成的。当然,PostgREST 并没有限制只能用这种方式,所以实际可供选择的方案有两种:

  • DB 内的身份校验
  • DB 外的身份检验

DB 内校验方案,根据是否使用 PostgreSQL 内置用户表又可细分成两种情况:

  • 使用专门的用户表
  • 使用 PostgreSQL 内置的用户表而不是专门的用户表

需要注意的是,DB 内校验方案需要在 PostgreSQL 中安装 pgjwt 扩展 和 pgcrypto 扩展。

DB 内的身份校验

开启 JWT

我们先来看 DB 内授权方案。(这种方案要对 pg 数据库进行较多的”非数据存储”类的操作,跟我倾向于只将 pg 数据库用于”数据存储”的想法并不一致,所以这一方向的调研只完成了相当基础的一部分)

PostREST 有三种不同的角色类型:

  • authenticator - 这个角色连接 PostgreSQL 数据库
  • anonymous - 这个角色可以简单对应成”不带登录态”的请求,所以它通常只有非常受限数据访问权限
  • user - 这个角色可以对应成”带有效登录态”的请求,所以可以访问授权范围内的数据

要点:

  • 我们要在 pg 数据库中正确地配置这三个角色数据访问权限
  • 我们要在 PostREST 配置文件中正确地配置 db-uri 来连接 pg 数据库,配置参数来自 authenticator 角色

PostgREST 检查 Authorization 请求头(token),

  • 如果没有 token 或者 token 中没有正确配置 role 字段,则认为是 anonymous 角色
  • 如果有 token 且 token 中正确配置 role 字段为 user,则认为是 user 角色,PostREST 以 user 角色来访问 pg 数据库

要点:

  • 我们要在 PostREST 配置文件中正确地配置 db-anon-role 参数,这个参数通常指定为 anonymous
  • 我们要在 PostREST 配置文件中正确地配置 jwt-secret 参数,这个参数用于解码 Authorization 请求头
  • 调用方通常应将 role 指定为 user

接下来是一个具体操作示例:

pg 数据库配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# == 角色配置 ==
# authenticator 用于连接数据库
CREATE ROLE authenticator LOGIN NOINHERIT NOCREATEDB NOCREATEROLE
NOSUPERUSER password ‘mysecretpassword’ ;
# anonymous 用于无登录态用户, 这个角色无任何表权限,或少量表权限
CREATE ROLE anonymous NOLOGIN;
# user 用于有登录态的用户,这个角色可以有数据访问权限
CREATE ROLE user NOLOGIN;

# 允许 authenticator 切换成 anonymous 和 user
GRANT anonymous TO authenticator
GRANT user TO authenticator

# == 数据权限配置 ==
# 向 user 角色授权 test_schema 下各表的读写权限
GRANT usage ON SCHEMA test_schema TO user;
GRANT SELECT, INSERT, UPDATE, DELETE
ON ALL TABLES IN SCHEMA test_schema TO user;

PostREST 配置

这是一个示例文件:

1
2
3
4
db-uri = "postgres://authenticator:mysecretpassword@localhost:5432/cm"
db-schema = "test_schema"
db-anon-role = "anonymous"
jwt-secret = "THIS IS USED TO SIGN AND VERIFY JWT TOKENS"

生成 token

如下图所示,可以在 jwt.io 网站上生成 token:

  • 第1步的 secret 填写上 jwt-secret 字段配置的字符串
  • 第2步的 json 串中至少要有一个 role 字段。在本例中,role 字段填 user
  • 第3步,复制网站上生成的 TOKEN 到本地,在 curl 命令中验证其有效性

验证方式如下:

1
2
3
4
5
6
export TOKEN="eyJhbGciOiJ..."

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

至此,我们就开启了 PostgREST 的 JWT 校验。但是,以上的 token 是永久有效的,这显示不符合实际项目的要求。我们通过 exp 字段来设置 token 的有效时间。添加新字段的 JSON 串类似这样:

1
2
3
4
{
"role": "todo_user",
"exp": 123456789
}

注意事项

PostgREST 默认对每个请求的 token 都进行 JWT 校验,所以可能产生较大的性能开销。可以开启 JWT 缓存来减小性能开销。

To enable JWT caching, the config jwt-cache-max-lifetime is to be set. It is the maximum number of seconds for which the cache stores the JWT validation results. The cache uses the exp claim to set the cache entry lifetime. If the JWT does not have an exp claim, it uses the config value.

配置文件的 jwt-cache-max-lifetime 参数用于开启 JWT 缓存。请求中的 exp 字段决定了缓存有效时间。如果请求中没有 exp 字段,则默认的缓存有效时间为 jwt-cache-max-lifetime

更进一步

前面通过 exp 解决了 token 的有效期问题,另外两个问题是:

  • 如何立即让一个 token 失效
  • 如何利用 PostgREST 生成 token

第一个问题的解决方案是在 pg 数据库对应的 scheme 下新建函数,并为 PostgREST 添加相应的配置。

1
2
3
# add this line to tutorial.conf

db-pre-request = "test_user.check_token"

以上面的配置文件为例,这里的 scheme 为 test_user,函数是 check_token,而对应的 PostgREST 配置是 db-pre-request

PostgREST 官方文档中有详细的”用户管理方案”来立即让一个 token 失效,以及在 pg 数据库内生成 token:

但正如前面所说,我倾向将数据保存到 pg 数据库,而不是使用 pg 数据库实现过多的业务逻辑。所以没有继续这个方案。

DB 外的身份检验

这个方案的大致流程如下:

1
用户请求 -> Nginx 反向代理 -> 网关 -> PostgREST -> PostgreSQL

这里的网关有两个功能:

  • 用户管理
    • 提供 /authentate 接口用于生成 token (登录)
    • 检查业务接口请求中的 token 是否有效
      • 通过检查的请求由接口代理模块继续处理
      • 未通过检查的请求被重定向到登录流程
  • 接口代理
    • 将业务接口请求(已通过 token 校验)转发到 PostgREST

这个网关是基于 express.js 开发的一个小型 Node 应用,核心功能是用户管理和接口代理。

  • 用户管理模块使用 express-jwt 中间件 + jsonwebtoken 库开发
  • 接口代理模块使用 http-proxy-middleware 中间件开发

结论

我选择第二种方案,即DB 外的身份检验。原因在于,这种方案虽然比第一种方案多出一个网关层,但构架上仍然足够简单,实现起来也不复杂,并且也满足我只将 PostgreSQL 用作数据存储的诉求。

参考资料