使用PostgREST的RestAPI操作之角色系统教程

角色系统概述

PostgREST旨在使数据库始终处于API安全性的中心。所有授权都是通过数据库角色和权限进行的。PostgREST的工作是对请求进行身份验证(即验证客户端是否是他们所说的身份),然后让数据库对客户端操作进行授权

验证顺序

PostgREST使用三种角色,身份验证者匿名角色和用户角色。数据库管理员创建这些角色,并将PostgREST配置为使用它们。

_images / security-roles.png

应该NOINHERIT在数据库中创建并配置身份验证器以具有非常有限的访问权限。这是一个变色龙,其工作是“成为”其他用户以服务于经过身份验证的HTTP请求。下图显示了服务器如何处理身份验证。如果auth成功,它将切换到请求指定的用户角色,否则将切换到匿名角色。

_images / security-anon-choice.png

这是技术细节。我们使用JSON Web令牌来认证API请求。您会记得,JWT包含一个经过密码签名的声明的列表。允许所有声明,但PostgREST特别关心称为角色的声明。

{
  "role": "user123"
}

当请求包含具有角色声明的有效JWT时,PostgREST将在HTTP请求期间切换到具有该名称的数据库角色。

SET LOCAL ROLE user123;

请注意,数据库管理员必须通过预先执行身份验证程序角色才能切换到该用户

GRANT user123 TO authenticator;

如果客户端不包括JWT(或没有角色声明的JWT),则PostgREST切换到匿名角色,该匿名角色的实际特定于数据库的名称(如具有身份验证者角色的名称)在PostgREST服务器配置文件中指定。数据库管理员必须正确设置匿名角色权限,以防止匿名用户看到或更改他们不应该看到的内容。

用户和组

PostgreSQL使用角色的概念来管理数据库访问权限。可以将角色视为数据库用户或一组数据库用户,这取决于角色的设置方式。

每个Web用户的角色

PostgREST可以容纳任何一个观点。如果您将角色视为单个用户,那么上述基于JWT的角色切换将满足您的大部分需求。当通过身份验证的用户发出请求时,PostgREST将切换到该用户的角色,除限制查询外,SQL还可通过该current_user变量使用该角色。

您可以使用行级安全性来灵活限制当前用户的可见性和访问权限。这是Tomas Vondra 的示例,它是一个聊天表,用于存储用户之间发送的消息。用户可以在其中插入行以将消息发送给其他用户,并对其进行查询以查看其他用户发送给他们的消息。

CREATE TABLE chat (
  message_uuid    UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
  message_time    TIMESTAMP NOT NULL DEFAULT now(),
  message_from    NAME      NOT NULL DEFAULT current_user,
  message_to      NAME      NOT NULL,
  message_subject VARCHAR(64) NOT NULL,
  message_body    TEXT
);

我们要实施一项政策,以确保用户只能看到他发送或发给他的那些消息。另外,我们还希望防止用户使用其他人的名字伪造message_from列。

PostgreSQL(9.5及更高版本)允许我们使用行级安全性设置此策略:

CREATE POLICY chat_policy ON chat
  USING ((message_to = current_user) OR (message_from = current_user))
  WITH CHECK (message_from = current_user)

任何访问聊天表的API终结点的人都将确切地看到他们应该的行,而无需我们自定义命令性服务器端编码。

警告

角色是按群集而不是按数据库命名的,因此它们可能容易发生冲突。

Web用户共享角色

或者,数据库角色可以代表组,而不是(或除了)单个用户。您可以选择Web应用程序的所有登录用户共享角色webuser。您可以通过在JWT中包含其他声明(例如电子邮件)来区分单个用户。

{
  "role": "webuser",
  "email": "john@doe.com"
}

SQL代码可以通过PostgREST为每个请求设置的GUC变量来访问声明。例如,要获取电子邮件声明,请调用此函数:

current_setting('request.jwt.claim.email', true)

这使JWT生成服务可以包含额外的信息,并且您的数据库代码可以对此做出反应。例如,可以将RLS示例修改为使用此current_setting而不是current_user。第二个“ true”参数告诉current_setting如果当前配置中缺少该设置,则返回NULL。

混合用户组角色

您可以混合使用组角色策略和个人角色策略。例如,我们仍然可以拥有一个webuser角色以及从中继承的单个用户:

CREATE ROLE webuser NOLOGIN;
-- grant this role access to certain tables etc

CREATE ROLE user000 NOLOGIN;
GRANT webuser TO user000;
-- now user000 can do whatever webuser can

GRANT user000 TO authenticator;
-- allow authenticator to switch into user000 role
-- (the role itself has nologin)

自定义验证

PostgREST兑现exp令牌到期的索赔,拒绝过期的令牌。但是,它不执行任何额外的约束。额外约束的一个示例是立即撤消某个用户的访问权限。配置文件参数pre-request指定一个存储过程,该存储过程在身份验证器切换到新角色之后并且在主查询本身运行之前立即调用。

这是一个例子。在配置文件中,指定一个存储过程:

pre-request = "public.check_user"

在函数中,您可以运行任意代码以检查请求,并根据需要引发异常以将其阻止。

CREATE OR REPLACE FUNCTION check_user() RETURNS void AS $$
BEGIN
  IF current_user = 'evil_user' THEN
    RAISE EXCEPTION 'No, you are evil'
      USING HINT = 'Stop being so evil and maybe you can log in';
  END IF;
END
$$ LANGUAGE plpgsql;

客户端验证

要发出经过身份验证的请求,客户端必须包含Authorization带有值的HTTP标头。例如:Bearer <jwt>

GET /foo HTTP/1.1
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoiamRvZSIsImV4cCI6MTQ3NTUxNjI1MH0.GYDZV3yM0gqvuEtJmfpplLBXSGYnke_Pvnl0tbKAjB4

JWT生成

您可以从数据库内部或通过外部服务创建有效的JWT。每个令牌都用一个秘密密钥加密签名。在对称密码学的情况下,签名者和验证者共享相同的秘密密码。在非对称密码术中,签名者使用私钥,而验证者使用公钥。PostgREST支持对称和非对称加密。

来自SQL的

您可以使用pgjwt扩展名在SQL中创建JWT令牌。这很简单,只需要pgcrypto。如果您在不支持安装新扩展的Amazon RDS之类的环境中,仍可以在pgjwt内部手动运行SQL(您需要@extschema@用其他架构替换或删除它),从而创建所需的功能。

接下来,编写一个返回令牌的存储过程。下面的那个返回一个具有硬编码角色的令牌,该令牌在发行后五分钟到期。请注意,此功能也具有硬编码的秘密。

CREATE TYPE jwt_token AS (
  token text
);

CREATE FUNCTION jwt_test() RETURNS public.jwt_token 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通过对/ rpc / jwt_test的POST请求向客户端公开此功能。

注意

为避免对存储过程中的秘密进行硬编码,请将其另存为数据库的属性。

-- 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 ...

智威汤逊从Auth0 

Auth0这样的外部服务可以完成将Github,Twitter,Google等的OAuth转换为适合PostgREST的JWT的艰苦工作。Auth0还可以处理电子邮件注册和密码重置流程。

要使用Auth0,请将其客户端密码作为拷贝到您的PostgREST配置文件中jwt-secret。(旧式Auth0密码是Base64编码的。将这些密码设置secret-is-base64true,或仅刷新Auth0密码。)您可以在Auth0管理控制台的客户端设置中找到该密码。

注意

确保已关闭符合OIDC的规范。

最近的Auth0更改默认情况下将其设置为打开。在这里关闭它:

客户端> 您的应用 >设置>显示高级设置> OAuth>符合OIDC

还请确保您的客户端应用程序不会传递任何受众群体配置。

我们的代码要求在JWT中具有数据库角色。要添加它,您需要将数据库角色保存在Auth0 应用程序元数据中。然后,您将需要编写一条规则,该规则将从用户元数据中提取角色,并role在我们的用户对象的有效负载中包含声明。然后,在您的Auth0Lock代码中,将role声明包括在您的范围参数中

// Example Auth0 rule
function (user, context, callback) {
  user.app_metadata = user.app_metadata || {};
  user.role = user.app_metadata.role;
  callback(null, user, context);
}
// Example using Auth0Lock with role claim in scope
new Auth0Lock ( AUTH0_CLIENTID, AUTH0_DOMAIN, {
  container: 'lock-container',
  auth: {
    params: { scope: 'openid role' },
    redirectUrl: FQDN + '/login', // Replace with your redirect url
    responseType: 'token'
  }
})

非对称密钥

配置部分所述,PostgREST接受jwt-secret配置文件参数。如果将其设置为简单的字符串值,例如“ reallyreallyreallyreallyverysafe”,则PostgREST会将其解释为HMAC-SHA256密码短语。但是,您也可以指定文字JSON Web密钥(JWK)或设置。例如,您可以使用编码为JWK的RSA-256公钥:

{
  "alg":"RS256",
  "e":"AQAB",
  "key_ops":["verify"],
  "kty":"RSA",
  "n":"9zKNYTaYGfGm1tBMpRT6FxOYrM720GhXdettc02uyakYSEHU2IJz90G_MLlEl4-WWWYoS_QKFupw3s7aPYlaAjamG22rAnvWu-rRkP5sSSkKvud_IgKL4iE6Y2WJx2Bkl1XUFkdZ8wlEUR6O1ft3TS4uA-qKifSZ43CahzAJyUezOH9shI--tirC028lNg767ldEki3WnVr3zokSujC9YJ_9XXjw2hFBfmJUrNb0-wldvxQbFU8RPXip-GQ_JPTrCTZhrzGFeWPvhA6Rqmc3b1PhM9jY7Dur1sjYWYVyXlFNCK3c-6feo5WlRfe1aCWmwZQh6O18eTmLeT4nWYkDzQ"
}

注意

如果它包含在分配给key成员的数组中,例如,它也可以是JSON Web密钥集(JWKS)。{ keys: [jwk1, jwk2] }

只需将其作为单行字符串传递,即可将引号转义:

jwt-secret = "{ \"alg\":\"RS256\", … }"

要生成这样的公用/专用密钥对,请使用闩锁集/ jose之类的实用程序。

jose jwk gen -i '{"alg": "RS256"}' -o rsa.jwk
jose jwk pub -i rsa.jwk -o rsa.jwk.pub

# now rsa.jwk.pub contains the desired JSON object

您可以指定如我们先前所见的文字值,或引用文件名从文件中加载JWK:

jwt-secret = "@rsa.jwk.pub"

JWT安全性

至少有三种类型的反对使用JWT的常见批评:1)反对标准本身,2)反对使用具有已知安全漏洞的库,以及3)反对使用JWT进行Web会话。我们将简要说明每个评论,PostgREST如何处理它,并提供适当的用户操作建议。

JWT标准的批评已在网络上的其他地方详细表达。与PostgREST最相关的部分是所谓的alg=none问题。一些实现JWT的服务器允许客户端选择用于签名JWT的算法。在这种情况下,攻击者可以将算法设置为none,根本不需要任何签名,并获得未经授权的访问。但是,PostgREST的当前实现不允许客户端在HTTP请求中设置签名算法,从而使此攻击无关紧要。对该标准的批评是,它完全需要实施alg=none

对JWT库的批评仅通过它使用的库与PostgREST有关。如上所述,不允许客户端在HTTP请求中选择签名算法会消除最大的风险。如果服务器使用非对称算法(例如RSA)进行签名,则可能会发生另一种更细微的攻击。同样,这与PostgREST不相关,因为它不受支持。好奇的读者可以在本文中找到更多信息。可以在jwt.io上找到有关在API客户端中使用的高质量库的建议。

最后一种批评方式集中在滥用JWT维护Web会话上。基本建议是停止使用JWT进行会话,因为大多数(即使不是全部)解决您在工作时出现的问题的解决方案也不起作用。链接的文章深入讨论了问题,但问题的实质是JWT并非设计为用于客户端存储的安全且有状态的单元,因此不适合会话管理。

PostgREST主要将JWT用于身份验证和授权目的,并鼓励用户这样做。对于Web会话,通过HTTPS使用cookie足够好,并且可以很好地满足标准Web框架的要求。

HTTPS 

PostgREST的目标是做好一件事:向PostgreSQL数据库添加HTTP接口。为了使代码小而集中,我们不实现HTTPS。使用反向代理(例如NGINX)添加它,方法如下。请注意,某些平台即服务(例如Heroku)也会在其负载均衡器中自动添加SSL。

架构隔离

PostgREST实例配置为公开服务器配置文件中指定的单个架构的所有表,视图和存储过程。这意味着私有数据或实现细节可以进入私有模式内部,并且对HTTP客户端不可见。然后,您可以公开视图和存储过程,以将内部细节与外界隔离。它使您的代码更易于重构,并提供了一种自然的API版本控制方法。有关使用公共视图包装私有表的示例,请参见下面的“ 公共用户界面”部分。

SQL用户管理

存储用户和密码

如前所述,外部服务可以使用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 schema if not exists basic_auth;

create table if not exists
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 or replace 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;

drop trigger if exists ensure_user_role_exists on basic_auth.users;
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 if not exists pgcrypto;

create or replace 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;

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

有了该表之后,我们可以帮助您对照加密列检查密码。如果电子邮件和密码正确,它将返回用户的数据库角色。

create or replace 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。

登录

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

-- login should be on your exposed schema
create or replace function
login(email text, pass text) returns basic_auth.jwt_token as $$
declare
  _role name;
  result basic_auth.jwt_token;
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 result;
  return result;
end;
$$ language plpgsql security definer;

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

POST /rpc/login HTTP/1.1

{ "email": "foo@bar.com", "pass": "foobar" }

响应看起来像下面的代码片段。尝试在jwt.io上解码令牌。(它使用reallyreallyreallyreallyverysafe上面的SQL代码中指定的秘密进行编码。您将要在您的应用中更改此秘密!)

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

权限

您的数据库角色需要访问架构,表,视图和函数,以便为HTTP请求提供服务。回顾角色系统概述,PostgREST使用特殊角色来处理请求,即身份验证者角色和匿名角色。以下是允许匿名用户创建帐户并尝试登录的权限示例。

-- the names "anon" and "authenticator" are configurable and not
-- sacred, we simply choose them for clarity
create role anon noinherit;
create role authenticator noinherit;
grant anon to authenticator;

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

由于上述login功能被定义为安全性定义程序,因此匿名用户anon不需要权限即可读取basic_auth.users表。它甚至不需要访问basic_auth架构的权限。 为清楚起见,其中包含,但可能不需要,请参阅功能特权以获取更多详细信息。grant execute on function

 

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值