写点什么

【原理篇】Supabase 权限模型 Part2

作者:张文平
  • 2023-09-08
    湖北
  • 本文字数:3238 字

    阅读完需:约 11 分钟

【原理篇】Supabase 权限模型 Part2

上一篇我们介绍了 Supabase 在用户认证鉴权面临的挑战,以及当前 Supabase 的应对方案。这一篇,我们结合 PostgREST 的工作机制、PostgreSQL RLS 以及 gotrue 组件详细讲解一下 Supabase 在进行用户登录认证及权限管控方面的工作机制。

PostgREST 的认证和鉴权机制

PostgREST 是 Supabase 的核心组件之一。


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


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

这三个 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,敬请关注。

发布于: 刚刚阅读数: 4
用户头像

张文平

关注

Supabase先行者 2020-08-24 加入

还未添加个人简介

评论

发布
暂无评论
【原理篇】Supabase 权限模型 Part2_Serverless_张文平_InfoQ写作社区