65.9K
CodeProject 正在变化。 阅读更多。
Home

解码 PostgreSQL 中的 Django Session

emptyStarIconemptyStarIconemptyStarIconemptyStarIconemptyStarIcon

0/5 (0投票)

2021年3月24日

CPOL

6分钟阅读

viewsIcon

4864

当解决一个需要将用户会话数据与其实际用户对象关联的问题时,Postgres 会派上用场。

Django 中的 Session

Session 是任何基于 HTTP 的 Web 框架的重要组成部分。它们允许 Web 服务器跟踪重复 HTTP 客户端的身份,而无需它们在每次请求时重新进行身份验证。有几种不同的方法可以跟踪会话。有些不需要服务器持久化会话数据(例如 JSON Web Tokens),而有些则需要。

Django,一个流行的基于 Python 的 Web 框架,自带了一个默认的会话后端,该后端确实会存储持久化的会话数据。有几种存储和缓存选项;您可以选择将会话简单地存储在 SQL 数据库中并在每次查找它们,将它们存储在 Redis 或 Memcached 等缓存中,或者两者都使用,并将缓存引擎设置在数据库存储前面。如果您使用最终将会话存储在 SQL 中的选项之一,django_session 表将包含您的用户会话。

Session 模式

当浏览应用程序数据时,您可能会遇到一个问题,需要将用户会话数据与其实际的 User 条目(auth_user 表)相关联。我最近遇到了这种情况,当我查看会话表的模式定义时,我惊讶地发现 user_id 没有作为列存储。这里有一些重要的设计决策,解释了为什么会这样,但这对于像我这样的 SQL 用户来说很不方便。

Decoding Django Sessions in PostgreSQL

 

没有 auth_user id。

session_key 是提供给客户端的密钥。通常,发出请求的客户端会将 session_key 作为 cookie 的一部分包含。当 Web 服务器收到请求时,它会查找 session_key,如果存在,然后查询以查看该密钥是否已知。如果是,它将查看关联的 session_data 并检索有关用户及其会话的元数据。

这就是您能够在 Django 请求中访问 request.user 等内容的方式。user_id 从解码的 session_data 中获取,内置的 User 对象根据存储的 user_id 进行填充,然后 User 对象可供整个项目的视图使用。

快速搜索了一下,我发现默认情况下会话数据是以 JSON 格式存储的。我早就知道 Postgres 卓越的 JSON 功能,所以我怀疑这可以在 Postgres 的范围内进行处理。这对像我这样花费大量时间在 Postgres 中的人来说是个好消息。

构建查询

初次查看

正如您在第一张图片中看到的,session_data 似乎不是 JSON。以 JSON 格式存储的元数据隐藏在 base64 编码 后面。幸运的是,我们可以在 Postgres 中轻松地进行 base64 解码。

从 Base64 解码

这仍然 hardly 可读。我们需要将二进制数据转换为文本。

编码为文本

Postgres 中的 "encode" 函数允许您 "将二进制数据编码为文本表示形式"。

现在,我们终于可以看到一些人类可读的内容了。这是其中一个完整的解码结果(文本格式)

11fcbb0d460fd406e83b60ae082991818a1321a4:{"_auth_user_hash":
"39308b9542b9305fc038d28a51088905e14246a1","_auth_user_backend":"x.alternate_auth.Backend",
"_auth_user_id":"52135"}

提取 JSON

我们这里得到的是一个 JSON 块,前面有一个冒号和某种哈希。我们只对 JSON 块感兴趣。一种快速提取哈希和冒号之后所有文本的方法是找到第一个冒号的位置,并提取其后的所有字符。

为此,我们可以利用 RIGHT 函数(返回字符串末尾的 n 个字符)和 POSITION 函数(返回字符串中字符的位置)。POSITION 只会返回您正在搜索的字符串的第一个实例的位置。

RIGHT 函数接受负数索引。负数索引会从字符串的右侧提取字符,但排除负数索引指示的字符。

为了进一步构建这个查询,我们将使用 CTE 将其分解为 2 部分。当您构建并选择了一个非平凡的列,并且需要多次使用它时,CTE 会很有帮助。如果只使用一个 SELECT,我们将不得不多次键入 encode(decode(session_data, 'base64'), 'escape') 部分。这很混乱,如果您决定更改解析编码数据的方式,则需要在 2 个地方更改函数调用。

这是我们提取 JSON 部分的更新后的查询。

完整结果示例

{"_auth_user_hash":"396db3c0f4ba3d35b350a",
"_auth_user_backend":"x.alternate_auth.Backend","_auth_user_id":"52646"}</code>

JSON 验证

现在该列可以被解析为 JSON 了,我们可以继续。但是,如果您尝试在 Postgres 中将文本强制转换为 JSON,而该文本不是有效的 JSON,Postgres 将会抛出错误并停止您的查询。在我的数据库中,一些会话是无法解析的 JSON。以下是一种快速确保文本看起来像可解析 JSON 的方法。

where
    substring(decoded, position(':' in decoded) + 1, 1) = '{'
    and right(decoded, 1) = '}'`

任何不以花括号开头和结尾的 string 都将被过滤掉。

这不能保证它能够解析,但在我的数百万个会话的数据库中,它确实起作用了。您可以编写自定义的 Postgres 函数来验证 JSON 的可解析性,但这会更慢。

JSON 强制转换

使用 WHERE 子句排除无效会话元数据后,就可以将我们的 string 强制转换为 Postgres 的 JSON 类型,并从 JSON 中提取 _auth_user_id 键。根据您的 Django 配置,此键可能不同。一旦对象被强制转换为 JSON 类型,您就可以使用 object->'key' 语法按键查询 JSON 值。

字符串清理

我们快到了!当从 JSON 强制转换为 text 时,Postgres 会在它周围添加双引号。最终,我们希望 user_id 字段是 int,但 Postgres 不会将包含双引号的 string 解析为 int。即使是 JavaScript 也不允许这样做!

TRIM 函数与 BOTH 一起使用,可以从 string 的两端剥离指定的字符,从而得到一个干净的 string,可以轻松地强制转换为整数。

最终查询

在去除多余的双引号并强制转换为 int 之后,这是我们的最终查询。

现在,正如示例结果所示,我们将 session_key 与 Django auth_user id 关联起来了。

这是可复制格式的完整查询

with step1 as (
  select
    session_key,
    encode(decode(session_data, 'base64'), 'escape') :: text as decoded
  from
    django_session
)
select
  session_key,
  trim(
    both '"'
    from
      (
        right(
          decoded,
          0 - position(':' in decoded)
        ) :: json -> '_auth_user_id'
      ) :: text
  ) :: int as user_id
from
  step1
where
  substring(decoded, position(':' in decoded) + 1, 1) = '{'
  and right(decoded, 1) = '}'

使用物化视图快速查询

如果您的数据库有很多用户,您会发现此查询非常慢。创建物化视图将允许您从持久化视图中重复查询结果,而无需重新运行 SQL。

当您创建物化视图时(以及每次刷新它时),视图的源代码都会运行,并用结果中的行填充。请务必在需要更新数据时刷新视图!

create materialized view mv_django_session_user as
with step1 as (
…

刷新方法

refresh materialized view mv_django_session_user;

摘要

在 Postgres 中进行编码和字符串操作比在 Web 应用程序常用的语言(如 Python、Ruby 或 PHP)中要繁琐一些,但在 Postgres 中构建一个完全提取您想要的确切数据并允许您直接连接到其他表的视图是非常令人满意的。

下次您需要提取由 Web 框架或其他第三方编码的数据时,请在 Postgres 中查找答案!

© . All rights reserved.