likes
comments
collection
share

【原理篇】Supabase 权限模型 Part2

作者站长头像
站长
· 阅读数 36

PostgREST的认证和鉴权机制

PostgREST是Supabase的核心组件之一。

PostgREST是一个将PostgreSQL数据库转换成可以直接使用RESTful API进行访问的Web Server,将原本只能在服务器程序通过数据库驱动访问的数据库转换成了可以通过HTTP接口进行调用的服务,并且保持了SQL语法的灵活性。借助于PostgREST,web或者移动应用程序可以直接访问数据库,不需要再开发服务端。

Supabase的认证鉴权机制紧密结合了PostgREST的认证机制,下图描述了PostgREST的角色系统:

【原理篇】Supabase 权限模型 Part2

这三个role代表了三个概念:

  • anonymous:匿名用户,也就是未通过认证的、不存在的用户,通常会设置最低的访问权限。
  • authenticator:PostgREST的一个特殊角色,是PostgREST服务端程序连接数据库用的账号,通常只赋予该用户LOGIN权限,也就是只能用来连接数据库,不能做其他操作。
  • Users:登录成功的用户全部归到这一类角色。

也就是说PostgREST事实上是以authenticator这个账号连接到PostgreSQL的,那如何以不同的身份执行数据库操作呢,PostgREST将其用到的技术方案称之为“用户模拟”。

PostgREST对外提供的是RESTful API,要求调用者必须传递JWT token进行API认证。JWT token可以包含任意自定义的信息,但必须包含role字段,该字段表明了token对应的角色:

{
  "role": "user123"
}

PostgREST接受到来自客户端的HTTP请求后,会校验该JWT token后,并从JWT token中解析出role字段的值,然后切换到该角色执行后续的SQL语句,切换的方式:

SET LOCAL ROLE user123;

由于当前连接数据库的角色是authenticator,要想切换成功,在创建user23这个角色时需要进行适当的授权:

GRANT user123 TO authenticator;

PostgREST在完成JWT token校验之后,除了切换角色这个操作,还会将JWT token中携带的其他信息保存到当前数据库连接的Session上下文环境中,方便后续业务流程中快速访问相关的信息。保存JWT信息的方法如下:

SELECT
  set_config('request.<setting>''value' ,true);

后续可以通过如下方法访问这些信息:

SELECT
  current_setting('request.<setting>'true);

可以通过这个机制来访问客户端HTTP请求各种信息,比如:

SELECT current_setting('request.headers'true)::json;
SELECT current_setting('request.headers'true)::json->>'user-agent';
SELECT current_setting('request.cookies'true)::json->>'sessionId';
SELECT current_setting('request.jwt.claims'true)::json->>'email';

Supabase的JWT设计

从上面关于PostgREST的认证方案介绍中,我们知道了如下基本机制:

  • PostgREST的API接口需要一个JWT token
  • PostgREST会校验该JWT的合法性
  • JWT token中必须包含role字段,PostgREST会模拟该role来执行接下来的SQL操作

Supabase严格遵守了这个机制来设计其用户认证授权流程:

  • 使用Supabase开发的应用,用户登录后会得到一个JWT token,这部分是有Gotrue实现的,我们会在本篇文章稍后介绍。
  • 客户端程序拿到JWT token后,supabase的SDK会在接下来的请求中自动携带该token
  • PostgREST服务接收到SDK发送的API调用请求后,检查token合法性,这里需要PostgREST服务和Gotrue服务使用相同的secret来操作JWT token。

我们看一下Supabase的JWT token的格式:

{
  "aud": "authenticated",
  "exp": 1615824388,
  "sub": "0334744a-f2a2-4aba-8c8a-6e748f62a172",
  "email": "d.l.solove@gmail.com",
  "app_metadata": {
    "provider": "email"
  },
  "user_metadata": null,
  "role": "authenticated"
}

可以看到,里面除了满足PostgREST的要求,携带了role字段外,还携带了一些其他信息,这些信息在后续的授权管理中都是有用的,我们稍后讨论,先关注一下role字段:

"role": "authenticated"

请注意,Supabase在这个细节处理上跟PostgREST设计上有出入。 PostgREST希望使用这个字段来区分不同的用户,并进行相应的授权管理,这样可以复用pg的一些能力,授权管理可以由dba直接在数据库中完成。而Supabase显然不能使用这种机制。对于应用开发来说,通常是使用邮箱/密码、手机号/验证码、第三方认证接入等方式进行登录验证,这部分工作是交给gotrue这个更专业的组件来完成的,postgres只承担数据库该承担的功能。

因此,Supabase这里的role不会发生变化,永远都是authenticated

那如何控制应用访问数据的权限问题呢,Supabase提供了两种解决方案。

1)如果你的应用是在服务端调用Supabase的接口,那么可以像传统的应用开发模式一样,在你的服务端程序中编写权限校验相关的代码。

2)如果你直接在客户端调用Supabase的接口,比如在web端用js直接调用接口,那么就不能在js中编写权限相关的控制逻辑,因为这部分代码是运行在用户的浏览器中的,用户可以很容易的绕过这部分代码直接访问未经授权的数据。这种场景就需要使用PostgreSQL提供的RLS能力。

使用RLS进行权限管理

PostgreSQL的RLS,全称Row Level Security,允许开发者对每一行数据都有精确的权限设置,利用RLS的能力,我们可以不用编写代码,直接在数据库中对不同用户的数据进行隔离,确保用户只能访问自己的数据,不能访问未经授权的数据。

要使用RLS的能力,需要手动对目标表开启RLS:

ALTER TABLE <name> ENABLE ROW LEVEL SECURITY

然后创建相应的策略(Policy)来规定数据访问规则,即:谁有权限访问哪些数据行。 回到Supabase的场景,我们以一个简单的例子来看一下RLS在Supabase中是如何结合JWT一起发生作用的:

create table my_scores (
    name text,
    score int,
    user_id uuid not null
);

ALTER TABLE my_scores ENABLE ROW LEVEL SECURITY;

insert into my_scores(name, score, user_id)
values
  ('Paul'100'5a4365e7-7c7d-4eaf-a8ee-9ec9432917ca'),
  ('Paul'200'5a4365e7-7c7d-4eaf-a8ee-9ec9432917ca'),
  ('Leto'50,  '9ec94326-2e2d-2ea2-22e3-3a535a4365e7');

create policy "只有管理员能更新评分"
  on my_scores
  for update using (
    auth.jwt() ->> 'email' = 'admin@xxx.com'
  );

上面这条规则使用了JWT token中包含的email信息来进行权限管控。auth.jwt() 是一个函数,可以从当前会话信息中读取到jwt claims中的信息。相当于:

SELECT current_setting('request.jwt.claims'true)::json->>'email';

这一点我们在PostgREST小节中有做介绍。

上面这个规则解读起来就很简单了:只有当前API请求的发送者的email是admin@xxx.com的用户才有权更新my_scores表,其他用户会收到一个包含相关错误信息的HTTP Response。

Gotrue扮演的角色

上面我们提到,Supabase没有使用postgres的账号作为用户登录认证的账号,因为这无法满足真实的应用开发需求。

Gotrue是Supabase的另外一个核心组件,其职责就是负责完成用户注册、登录、第三方接入等功能的实现。

在整个登录认证鉴权的流程中,Gotrue是第一入口,他负责完成用户登录认证,并生成JWT token,供后续的API调用使用。

Gotrue帮助开发者实现了OAuth协议的处理、短信平台的接入、微信小程序、apple账号的接入等功能,用户只需要配置好相应平台的API key就可以完成这些平台的接入。

简单来说,整个流程可以分为如下几个步骤:

  1. Gotrue负责生成一个合法的、用户无法篡改的JWT token
  2. PostgREST拿到这个token后使用与Gotrue相同的secret进行合法性校验,然后将JWT token携带的信息写入会话缓存中
  3. RLS Policy通过读取相关的JWT信息来确定当前用户身份,从而实现数据权限管控

关于Supabase的权限模型,我们就介绍到这,基本上囊括的Supabase在处理用户登录、认证、授权等各个方面的实现原理,下一篇我们会介绍一下anon key以及service role key,敬请关注。