角色系统概述
PostgREST旨在使数据库始终处于API安全性的中心。所有授权都是通过数据库角色和权限进行的。PostgREST的工作是对请求进行身份验证(即验证客户端是否是他们所说的身份),然后让数据库对客户端操作进行授权。
验证顺序
PostgREST使用三种角色,身份验证者,匿名角色和用户角色。数据库管理员创建这些角色,并将PostgREST配置为使用它们。
应该NOINHERIT
在数据库中创建并配置身份验证器以具有非常有限的访问权限。这是一个变色龙,其工作是“成为”其他用户以服务于经过身份验证的HTTP请求。下图显示了服务器如何处理身份验证。如果auth成功,它将切换到请求指定的用户角色,否则将切换到匿名角色。
这是技术细节。我们使用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-base64
为true
,或仅刷新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