SQL用户管理#

如上所述 JWT一代 ,外部服务可以提供用户管理,并使用JWT与PostgREST服务器进行协调。也可以完全通过SQL支持登录。这是一个相当大的工作量,所以做好准备。

存储用户和密码#

下面的表、函数和触发器将位于 basic_auth 不应在API中公开的架构。公共视图和函数将位于不同的架构中,该架构在内部引用此内部信息。

首先,我们需要一个表来跟踪我们的用户:

-- We put things inside the basic_auth schema to hide
-- them from public view. Certain public procs/views will
-- refer to helpers and tables inside.

create table
basic_auth.users (
  email    text primary key check ( email ~* '^.+@.+\..+$' ),
  pass     text not null check (length(pass) < 512),
  role     name not null check (length(role) < 512)
);

我们希望该角色是实际数据库角色的外键,但是PostgreSQL不支持对 pg_roles 桌子。我们将使用触发器手动执行它。

create function
basic_auth.check_role_exists() returns trigger as $$
begin
  if not exists (select 1 from pg_roles as r where r.rolname = new.role) then
    raise foreign_key_violation using message =
      'unknown database role: ' || new.role;
    return null;
  end if;
  return new;
end
$$ language plpgsql;

create constraint trigger ensure_user_role_exists
  after insert or update on basic_auth.users
  for each row
  execute procedure basic_auth.check_role_exists();

接下来,我们将使用pgcrypto扩展和触发器来确保密码在 users 桌子。

create extension pgcrypto;

create function
basic_auth.encrypt_pass() returns trigger as $$
begin
  if tg_op = 'INSERT' or new.pass <> old.pass then
    new.pass = crypt(new.pass, gen_salt('bf'));
  end if;
  return new;
end
$$ language plpgsql;

create trigger encrypt_pass
  before insert or update on basic_auth.users
  for each row
  execute procedure basic_auth.encrypt_pass();

有了表,我们就可以创建一个帮助器来根据加密列检查密码。如果电子邮件和密码正确,它将返回用户的数据库角色。

create function
basic_auth.user_role(email text, pass text) returns name
  language plpgsql
  as $$
begin
  return (
  select role from basic_auth.users
   where users.email = user_role.email
     and users.pass = crypt(user_role.pass, users.pass)
  );
end;
$$;

公共用户界面#

在前一节中,我们创建了一个内部表来存储用户信息。在这里,我们创建了一个登录函数,该函数接受电子邮件地址和密码,如果凭据与内部表中的用户匹配,则返回JWT。

权限#

您的数据库角色需要访问架构、表、视图和函数才能服务于HTTP请求。召回从 角色系统概述 该PostgREST使用特殊角色来处理请求,即验证者角色和匿名角色。下面是允许匿名用户创建帐户并尝试登录的权限示例。

create role anon noinherit;
create role authenticator noinherit;
grant anon to authenticator;

然后,添加 db-anon-role 添加到配置文件以允许匿名请求。

db-anon-role = "anon"

来自SQL的JWT#

属性在SQL中创建JWT令牌 pgjwt extension 。它很简单,只需要pgcrypto。如果您所在的环境(如Amazon RDS)不支持安装新的扩展模块,您仍然可以手动运行 SQL inside pgjwt (您需要更换 @extschema@ 使用另一个模式或直接删除它),这将创建您将需要的函数。

接下来,编写一个返回令牌的函数。下面的令牌返回一个具有硬编码角色的令牌,该令牌在颁发后五分钟过期。请注意,该函数也有一个硬编码的秘密。

CREATE FUNCTION jwt_test(OUT token text) AS $$
  SELECT public.sign(
    row_to_json(r), 'reallyreallyreallyreallyverysafe'
  ) AS token
  FROM (
    SELECT
      'my_role'::text as role,
      extract(epoch from now())::integer + 300 AS exp
  ) r;
$$ LANGUAGE sql;

PostgREST通过POST请求向客户端公开此函数 /rpc/jwt_test

备注

为了避免在函数中对秘密进行硬编码,请将其保存为数据库的属性。

-- run this once
ALTER DATABASE mydb SET "app.jwt_secret" TO 'reallyreallyreallyreallyverysafe';

-- then all functions can refer to app.jwt_secret
SELECT sign(
  row_to_json(r), current_setting('app.jwt_secret')
) AS token
FROM ...

登录名#

如中所述 JWT from SQL ,我们将在登录函数中创建一个JWT。请注意,您需要将本例中硬编码的密钥调整为您选择的安全(至少32个字符)密钥。

-- login should be on your exposed schema
create function
login(email text, pass text, out token text) as $$
declare
  _role name;
begin
  -- check email and password
  select basic_auth.user_role(email, pass) into _role;
  if _role is null then
    raise invalid_password using message = 'invalid user or password';
  end if;

  select sign(
      row_to_json(r), 'reallyreallyreallyreallyverysafe'
    ) as token
    from (
      select _role as role, login.email as email,
         extract(epoch from now())::integer + 60*60 as exp
    ) r
    into token;
end;
$$ language plpgsql security definer;

grant execute on function login(text,text) to anon;

由于上述原因, login 函数定义为 security definer ,匿名用户 anon 不需要权限即可读取 basic_auth.users 桌子。它甚至不需要权限就可以访问 basic_auth 架构。 grant execute on function 是为了清楚起见而包括的,但可能不需要,请参见 功能 了解更多详细信息。

调用此函数的API请求将如下所示:

curl "http://localhost:3000/rpc/login" \
  -X POST -H "Content-Type: application/json" \
  -d '{ "email": "foo@bar.com", "pass": "foobar" }'

响应将类似于下面的代码片段。尝试在以下位置解码令牌 jwt.io 。(它是用秘密编码的 reallyreallyreallyreallyverysafe 如上面的SQL代码中所指定的。你会想要在你的应用程序中更改这个秘密!)

{
  "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJlbWFpbCI6ImZvb0BiYXIuY29tIiwicGFzcyI6ImZvb2JhciJ9.37066TTRlh-1hXhnA9oO9Pj6lgL6zFuJU0iCHhuCFno"
}

替代方案#

请参阅操作指南 使用Postgres的用户和密码进行SQL用户管理 以类似的方式完全避免了表 basic_auth.users