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 | # == 角色配置 == |
PostREST 配置
这是一个示例文件:
1 | db-uri = "postgres://authenticator:mysecretpassword@localhost:5432/cm" |
生成 token
如下图所示,可以在 jwt.io 网站上生成 token:
- 第1步的 secret 填写上
jwt-secret
字段配置的字符串 - 第2步的 json 串中至少要有一个
role
字段。在本例中,role
字段填user
- 第3步,复制网站上生成的 TOKEN 到本地,在 curl 命令中验证其有效性
验证方式如下:
1 | export TOKEN="eyJhbGciOiJ..." |
至此,我们就开启了 PostgREST 的 JWT 校验。但是,以上的 token 是永久有效的,这显示不符合实际项目的要求。我们通过 exp
字段来设置 token 的有效时间。添加新字段的 JSON 串类似这样:
1 | { |
注意事项
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 | # add this line to tutorial.conf |
以上面的配置文件为例,这里的 scheme 为 test_user
,函数是 check_token
,而对应的 PostgREST 配置是 db-pre-request
。
PostgREST 官方文档中有详细的”用户管理方案”来立即让一个 token 失效,以及在 pg 数据库内生成 token:
- SQL User Management — PostgREST 12.2 documentation
- SQL User Management using postgres’ users and passwords — PostgREST 12.2 documentation
但正如前面所说,我倾向将数据保存到 pg 数据库,而不是使用 pg 数据库实现过多的业务逻辑。所以没有继续这个方案。
DB 外的身份检验
这个方案的大致流程如下:
1 | 用户请求 -> Nginx 反向代理 -> 网关 -> PostgREST -> PostgreSQL |
这里的网关有两个功能:
- 用户管理
- 提供
/authentate
接口用于生成 token (登录) - 检查业务接口请求中的 token 是否有效
- 通过检查的请求由接口代理模块继续处理
- 未通过检查的请求被重定向到登录流程
- 提供
- 接口代理
- 将业务接口请求(已通过 token 校验)转发到 PostgREST
这个网关是基于 express.js 开发的一个小型 Node 应用,核心功能是用户管理和接口代理。
- 用户管理模块使用
express-jwt
中间件 +jsonwebtoken
库开发 - 接口代理模块使用
http-proxy-middleware
中间件开发
结论
我选择第二种方案,即DB 外的身份检验。原因在于,这种方案虽然比第一种方案多出一个网关层,但构架上仍然足够简单,实现起来也不复杂,并且也满足我只将 PostgreSQL 用作数据存储的诉求。