PostgREST 简明教程

2,391 阅读7分钟

介绍

PostgREST 是一个依赖 PostgSQL 数据库的 web 服务器,它会把 PostgSQL 数据库直接转换成一个 Restful API 服务。

本文将介绍如何使用 PostgREST 实现将 PostgSQL 的数据转换成相应的 HTTP 接口,以及如何实现身份认证功能。

运行环境配置

  1. 安装 PostgSQL 数据库:网上安装教程老多了,不在此赘述(本文基于 v14 版本)。

  2. 安装 PostgREST:前往 github release 页面下载对应平台的压缩包,然后解压到本地(本文基于 v9 版本)。

  3. 运行解压后目录内的 postgrest(Windows 系统对应的是 postgrest.exe),如果正常输出类似下述信息,表明环境正常:

    Usage: postgrest [-e|--example] [--dump-config | --dump-schema] FILENAME
      PostgREST 9.0.0 / create a REST API to an existing Postgres database
    
    Available options: ...
    

PostgREST 配置

PostgREST 通过读取配置文件来决定如何连接数据库,以及如何处理客户端请求。我们来创建一个配置文件 postgrest-todos.conf ,配置文件里至少需要包含下述几个字段:

# 数据库连接地址(请替换为自己的userName、password、port、database name)
db-uri = "postgres://username:password@localhost:5432/postgres"

# 需要暴露给客户端的schemas有哪些
db-schemas = "public"

# 因为 PostgREST 支持 JWT 认证,这个配置项配置在没有客户端认证的情况下
# 数据库使用哪个角色来进行操作(该角色的创建见文章后续)
db-anon-role = "anon"

# 非必要字段
# 默认是error不会打印请求日志,改为info可输出,便于调试
# log-level = "info" 

此外也可通过环境变量的形式进行配置,比如上述的配置项可通过下述环境变量进行表示(以 PGRST_ 作为前缀,字母转为大写,以下划线进行分割):

PGRST_DB_URI="postgres://username:password@localhost:5432/postgres"
PGRST_DB_SCHEMAS="public"
PGRST_DB_ANON_ROLE="anon"
# PGRST_LOG_LEVEL="info"

数据库配置

接下来需要在 PostgSQL 里基于上述的配置项进行配置,并填充一些DEMO数据用于测试:

CREATE ROLE anon NOLOGIN;
GRANT SELECT ON todos TO anon;
CREATE TABLE todos (
   id SERIAL PRIMARY KEY,
   content TEXT,
   complete BOOL NOT NULL DEFAULT false
);

INSERT INTO todos(content) 
VALUES('todo1'),('todo2');

运行并获取列表

在命令行内运行 ./postgrest postgrest-todos.conf,若输出类似下述表明运行成功,表示在 3000 端口启动了一个 HTTP 服务:

15/Jan/2022:19:01:59 +0800: Attempting to connect to the database...
15/Jan/2022:19:01:59 +0800: Connection successful
15/Jan/2022:19:01:59 +0800: Listening on port 3000
15/Jan/2022:19:01:59 +0800: Config re-loaded
15/Jan/2022:19:01:59 +0800: Listening for notifications on the pgrst channel
15/Jan/2022:19:01:59 +0800: Schema cache loaded

接着便可发起 GET 请求获取列表了:

curl http://localhost:3000/todos

如何更新数据

上述我们实现了无须认证便可读取数据库数据,由于 anon 没有修改权限,倘若我们通过 PostgREST 所提供的更新请求 (POST / PATH 等)来更新数据,请求会返回下述的响应信息:

{
  "hint": null,
  "details": null,
  "code": "42501",
  "message": "permission denied for table todos"
}

通常对于更新操作我们仅允许已登录的用户才能执行,PostgREST 提供了基于 JWT 的身份认证,下面来看下如何实现已登录用户才能进行数据修改的功能:

首先我们对 JWT 的使用做一个简单的说明/回顾,JWT 全名 JSON Web Token,它的使用流程一般如下图所示:

jwt.png

下面开始配置启用JWT:

  1. 在数据库里新建一个有修改数据权限的角色:

    create role no_anon nologin;
    grant all on todos to no_anon;
    grant usage,select on sequence todos_id_seq to no_anon;
    
  2. 更新 postgrest-todos.conf

    # 新增下述配置
    jwt-secret = "一个至少包含32个字符的密钥串"
    
  3. 重新运行 PostgREST

  4. 生成一个提供给客户端使用的 JWT:这一步需要调用 JWT 生成算法,为了便于测试,可在该网站内在线生成。如下图所示,第一个箭头处填写我们上述创建的角色名(PostgREST 会先切换到这个角色下然后执行 SQL),第二个箭头处填入自己的密钥串,然后左侧会自动得到 JWT:

xx.png

  1. 模拟客户端接口调用:将 JWT 附加在更新请求的请求头内:

    curl -X POST --location "http://localhost:3000/todos" \
        -H "Content-Type: application/json" \
        -H "Authorization: Bearer [YOUR_JWT_HERE]" \
        -d "{
              "content": "created from request"
            }"
    

    响应码为 201 即表示成功,此时可通过 GET 接口进行查询即可发现新增了一条数据:

    curl http://localhost:3000/todos
    

如何给 JWT 添加过期时间

上述生成的 JWT 默认永不过期,在实际场景中肯定是不合理的,那么如何添加过期时间呢?在生成 JWT 时,给 Payload 新增一个 exp 字段,值是一个 unix 时间戳:

{
  "role": "no_anon",
  "exp": 1642269576
}

得到的 JWT 将在该时间后失效,失效后的请求响应会返回 {"message":"JWT expired"}

如何立刻使 JWT 失效

上述添加过期时间仍存在一定的问题:必须要等到指定时间后 JWT 才会失效。那么如何立刻使 JWT 失效呢?首先需要知道,若要能立刻撤销 JWT,我们必须要做一些额外存储,比如新增一个表来存放黑名单用户,然后后端逻辑每次判断当前请求的 JWT 数据是否在黑名单内,在的话则拒绝处理。

PostgREST 提供了一个配置项可以让每次请求前先执行该函数(PostgreSQL Fucntions),若函数抛错便会让请求失败,我们修改配置文件并重新运行 PostgREST

db-pre-request = "auth"

然后在 PostgreSQL 里创建一个名为 auth 的函数:

create or replace function auth() returns void
  language plpgsql
  as $$
declare
  creator text;
begin
  creator := current_setting('request.jwt.claims', true)::json->>'creator';
  if  creator ='babo' 
  then
    raise insufficient_privilege using hint = 'So sorry!';
  end if;
end
$$;

从上述函数定义可看出,当 JWT Payload 里的 creator 值为 babo 时,函数会抛错,即请求会被阻止。然后让我们来分别生成两个 JWT 进行验证:

// 可正常访问的 JWT 的 Payload
{
  "role": "no_anon",
  "creator": "mabo"
}

// 会被阻止访问的 JWT 的 Payload
{
  "role": "no_anon",
  "creator": "babo"
}

分别使用上述两个 JWT 进行 POST 请求,前者会正常响应,后者则会返回 403:

{
  "hint": "So sorry!",
  "details": null,
  "code": "42501",
  "message": "insufficient_privilege"
}

当然,在实际场景中,我们的黑名单是单独维护的,而非硬编码在函数里,这里我们新建一个 blacklist table 来存放黑名单用户:

-- 新建黑名单表
create table blacklist(
    name text PRIMARY key 
);
-- 插入黑名单用户
insert into blacklist(name) values('babo);

-- 给 no_anon 赋予读权限
grant select on blacklist to no_anon;

然后新建一个 auth2 函数来从 blacklist 表里判断:

create or replace function auth2() returns void
  language plpgsql
  as $$
declare
  creator text;
begin
  creator := current_setting('request.jwt.claims', true)::json->>'creator';
  if exists(select 1 from blacklist u where u.name = creator)
  then
    raise insufficient_privilege using hint = 'So sorry from auth2!';
  end if;
end
$$;

最后记得更新下 PostgREST 的配置文件并重新执行:

db-pre-request = "auth2"

此时,通过更新 blacklist 的数据,便能实时对 JWT 进行撤销了。

结尾

倘若将 PostgREST 投入线上使用,还需了解进一步如何“热重载”配置项,如何完善权限配置及隔离,如何预防网络攻击等等,具体都可在官网文档内进行了解。通过这个工具,我们可以几乎不需要编写后端代码,这对 CRUD 类应用来说似乎很有用武之地。

结结尾

网上搜索着突然发现还有一个基于 Go 写的 prest 项目,虽然它俩具体的差别未进一步探索,但单从开发语言来说,还是 Go 听着更亲切一些,仿佛从山里突然走到了城市 🥲。