在 SurrealDB 中使用外部 JWT 令牌时身份验证失败

Ash*_*mnx 4 surrealdb

任何人都可以帮助我使用外部 jwt 令牌设置身份验证

到目前为止,我已经尝试了以下多种变体。

首先我使用定义令牌

DEFINE TOKEN my_token ON DATABASE TYPE HS512 VALUE '1234567890';
Run Code Online (Sandbox Code Playgroud)

然后我使用上面的“1234567890”和以下标头字段生成一个令牌。

{
  "alg": "HS512",
  "typ": "JWT",
  "NS": "help",
  "DB": "help",
  "TK": "my_token"
}
Run Code Online (Sandbox Code Playgroud)

注意:我还尝试在令牌的有效负载部分定义“NS”、“DB”、“TK”字段。

然后我尝试使用 JS 客户端中的令牌和带有 Bearer 授权标头的 http 请求进行身份验证。

db.authenticate("eyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCIsIk5TIjoiaGVscCIsIkRCIjoiaGVscCIsIlRLIjoibXlfdG9rZW4ifQ.e30.uoJypJ-Y9OrZjQW6WtuZWmFYBEOCHlkutbR6mlEYPCHvb49h9nFiWshKDc464MD3jaBh69T1OLwZ2aUWNujiuw")
Run Code Online (Sandbox Code Playgroud)

Js客户端和Http请求均出错

name: "AuthenticationError"
message: "There was a problem with authentication"
stack: "AuthenticationError: There was a problem with authentication\n    at Surreal.
Run Code Online (Sandbox Code Playgroud)

Jer*_*emy 10

这个答案最终比最初的预期更加全面。因此,这里的内容列表可以帮助您找到所需的内容。不幸的是,似乎不可能将它们转换为链接。(对不起)

\n

目录

\n
    \n
  • 编写 JWT 令牌\n
      \n
    • 令牌的组成部分\n
        \n
      • 令牌头
      • \n
      • 令牌有效负载
      • \n
      • 令牌签名
      • \n
      \n
    • \n
    • 对令牌进行编码\n
        \n
      • 示例:一步一步
      • \n
      • 使用 NodeJ
      • \n
      \n
    • \n
    \n
  • \n
  • SurrealDB 令牌身份验证\n
      \n
    • 定义令牌处理程序
    • \n
    • 使用我们制作的代币
    • \n
    • 使用公钥加密技术
    • \n
    \n
  • \n
  • SurrealDB 权限\n
      \n
    • 令牌类型\n
        \n
      • 命名空间令牌
      • \n
      • 数据库令牌
      • \n
      • 范围代币
      • \n
      \n
    • \n
    • 表权限\n
        \n
      • FULL:无需任何身份验证即可查询
      • \n
      • NONE:受限表(隐式默认值)
      • \n
      • 细化表权限
      • \n
      • 精细的字段权限
      • \n
      \n
    • \n
    • 从查询访问令牌和身份验证数据
    • \n
    \n
  • \n
  • 进一步阅读
  • \n
\n

编写 JWT 令牌

\n

现在我们需要生成一个令牌来测试它。您可能知道,Json Web Token (JWT) 由三部分组成:标头、负载和签名。它采用 base64url 编码(一种使用可安全在网址或超链接中使用的字符的 base64 编码形式)。

\n

令牌的组成部分

\n

令牌头

\n

标头向验证方(在本例中为 SurrealDB)描述它是什么类型的令牌以及它使用什么算法。让我们创建它:

\n
{\n    "alg": "HS512",\n    "typ": "JWT",\n}\n
Run Code Online (Sandbox Code Playgroud)\n

令牌有效负载

\n

现在,有效负载是有趣的部分。

\n

为了与 SurrealDB 一起使用,有许多字段决定数据库如何处理令牌。

\n

SurrealDB 自版本以来允许的令牌类型surreal-1.0.0-beta.8如下:

\n
    \n
  • 范围令牌身份验证:( ns, db, sc, tk [, id])
  • \n
  • 数据库令牌验证:( ns, db, tk)
  • \n
  • 命名空间令牌身份验证:( ns, tk)
  • \n
\n

有关详细信息,请参阅:
\n令牌验证逻辑 - SurrealDB - GitHub

\n

列出的字段名称为:

\n
    \n
  • ns :string命名空间
  • \n
  • db :string数据库
  • \n
  • sc :string范围
  • \n
  • tk :string代币
  • \n
  • id ?:string代表用户的事物(表行)(可选)
  • \n
\n

还有许多具有不同含义的公开注册的字段名称 - 如果您需要互操作性或标准化,则相关;对于仅使用 SurrealDB 来说,情况就不那么严重了。您可以将任何想要的可序列化数据放入有效负载中。但请记住,该数据将通过网络发送多次,因此值得保持简短。

\n

如果您好奇:\n公开注册的 JWT 字段列表 - 由 IANA 维护

\n

让我们创建一个数据库令牌。当我们注册它时,我们调用它,my_token所以让我们将其添加为我们的tk字段,添加我们db的和nsas 。这些字段并不像 SurrealDB 所看到的那样区分大小写,但是如果您稍后尝试作为权限或选择查询的一部分直接访问有效负载数据,它们就会区分大小写。

\n
{\n    "ns": "help",\n    "db": "help",\n    "tk": "my_token",\n    "someOtherValue": "justToShowThatWeCan"\n}\n
Run Code Online (Sandbox Code Playgroud)\n

令牌签名

\n

一旦我们组合了标头和有效负载,创建令牌的最后一步就是对其进行签名。

\n

签名由以下部分组成:

\n
    \n
  • 删除空白;和
  • \n
  • 对标头和负载进行 base64url 编码;然后
  • \n
  • 用点(句号/句号)将它们连接起来并将它们分开。
  • \n
\n

整个字符串与密钥一起通过(在本例中为 HMAC_SHA512)哈希算法,然后将结果进行 base64url 编码以形成签名。

\n

如果您对更深入的内容感兴趣:\n HMAC 如何将密钥与数据结合起来 - 维基百科

\n

让我们看看它的实际效果:

\n

对令牌进行编码

\n

示例:一步一步

\n
    \n
  1. 编码后的标头
    \neyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9

    \n
  2. \n
  3. 编码后的有效负载
    \neyJucyI6ImhlbHAiLCJkYiI6ImhlbHAiLCJ0ayI6Im15X3Rva2VuIiwic29tZU90aGVyVmFsdWUiOiJqdXN0VG9TaG93VGhhdFdlQ2FuIn0

    \n
  4. \n
  5. 连接并用点分隔
    \neyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9.eyJucyI6ImhlbHAiLCJkYiI6ImhlbHAiLCJ0ayI6Im15X3Rva2VuIiwic29tZU90aGVyVmFsdWUiOiJqdXN0VG9TaG93VGhhdFdlQ2FuIn0

    \n
  6. \n
  7. 将结果与秘钥进行哈希运算得到:
    \n8nBoXQQ_Up3HGKBB64cKekw906zES8GXa6QZYygYWD5GbFoLlcPe2RtMMSAzRrHHfGRsHz9F5hJ1CMfaDDy5AA

    \n
  8. \n
  9. 将密钥追加到输入中,再次使用点
    \neyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9.eyJucyI6ImhlbHAiLCJkYiI6ImhlbHAiLCJ0ayI6Im15X3Rva2VuIiwic29tZU90aGVyVmFsdWUiOiJqdXN0VG9TaG93VGhhdFdlQ2FuIn0.8nBoXQQ_Up3HGKBB64cKekw906zES8GXa6QZYygYWD5GbFoLlcPe2RtMMSAzRrHHfGRsHz9F5hJ1CMfaDDy5AA

    \n
  10. \n
\n

这就是我们完整的令牌!

\n

您可以使用jwt.io来处理有效负载、标头和签名算法。

\n

使用 NodeJ

\n

如果你只想在 Node.js 上编码令牌,我会推荐这个包: npm - jsonwebtoken

\n
npm i jsonwebtoken\n
Run Code Online (Sandbox Code Playgroud)\n
import jwt, { JwtPayload } from \'jsonwebtoken\';\n\n// Typescript Types\ntype signTokenFn = (payload:object, secretOrPrivateKey:string, options?:object) => Promise<string>;\ntype verifyTokenFn = (token:string, secretOrPublicKey:string, options?:object) => Promise<string | JwtPayload>;\n\n// Let\'s make them await-able\nconst promisifyCallback = (resolve, reject) => (failure, success) => failure ? reject(failure) : resolve(success);\n\nconst signToken:signTokenFn = async (payload, secretOrPrivateKey, options = {}) => new Promise((resolve, reject) => {\n    jwt.sign(payload, secretOrPrivateKey, options, promisifyCallback(resolve, reject));\n});\n\nconst verifyToken:verifyTokenFn = async (token, secretOrPublicKey, options = {}) => new Promise((resolve, reject) => {\n    jwt.verify(token, secretOrPublicKey, options, (err, decoded) => err ? reject(err) : resolve(decoded));\n});\n\n// The actual encoding/verifying\nconst secret = \'0123456789\';\n\nconst tokenPayload = {\n    ns: "help",\n    db: "help",\n    tk: "my_token",\n    someOtherValue: "justToShowThatWeCan"\n};\n\nconst signedToken = await signToken(tokenPayload, secret, { \n    expiresIn: \'10m\' // Set any duration here ex: \'24h\'\n});\n\nconst accessDecoded = await verifyToken(signedToken, secret)\n\n
Run Code Online (Sandbox Code Playgroud)\n
\n

SurrealDB 令牌身份验证

\n

定义令牌处理程序

\n

您关于如何定义令牌处理程序的问题是正确的,所以让我们这样做:

\n
DEFINE TOKEN my_token ON DATABASE TYPE HS512 VALUE \'1234567890\';\n
Run Code Online (Sandbox Code Playgroud)\n

可以在命名空间 (ns)、数据库 (db) 或范围上定义令牌。后者尚未记录,因为它是最近对代码库的提交之一。请参阅:
\n提交 ( 75d1e86)"Add DEFINE TOKEN \xe2\x80\xa6 ON SCOPE \xe2\x80\xa6 functionality" - GitHub 上的 SurrealDB

\n

使用我们制作的代币

\n

使用 vs-code REST 客户端,我们可以这样测试我们的令牌:

\n
POST /sql HTTP/1.1\nHost: localhost:8000\nContent-Type: text/plain\nAccept: application/json\nToken: eyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9.eyJucyI6ImhlbHAiLCJkYiI6ImhlbHAiLCJ0ayI6Im15X3Rva2VuIiwic29tZU90aGVyVmFsdWUiOiJqdXN0VG9TaG93VGhhdFdlQ2FuIn0.8nBoXQQ_Up3HGKBB64cKekw906zES8GXa6QZYygYWD5GbFoLlcPe2RtMMSAzRrHHfGRsHz9F5hJ1CMfaDDy5AA\nNS: help\nDB: help\n\nSELECT * FROM myHelpTable\n
Run Code Online (Sandbox Code Playgroud)\n

我们应该得到这样的响应:

\n
HTTP/1.1 200 OK\ncontent-type: application/json\nversion: surreal-1.0.0-beta.8+20220930.c246533\nserver: SurrealDB\ncontent-length: 91\ndate: Tue, 03 Jan 2023 00:09:49 GMT\n\n[\n  {\n    "time": "831.535\xc2\xb5s",\n    "status": "OK",\n    "result": [\n      {\n        "id": "test:record"\n      },\n      {\n        "id": "test:record2"\n      }\n    ]\n  }\n]\n
Run Code Online (Sandbox Code Playgroud)\n

现在我们知道它可以工作了,让我们用 javascript 客户端库来尝试一下。(这对于 Node.JS 来说是一样的)

\n
import Surreal from \'surrealdb.js\';\n\nconst db = new Surreal(\'http://127.0.0.1:8000/rpc\');\nconst NS = \'help\';\nconst DB = \'help\';\n\nasync function main() {\n    await db.authenticate(\'eyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9.eyJucyI6ImhlbHAiLCJkYiI6ImhlbHAiLCJ0ayI6Im15X3Rva2VuIiwic29tZU90aGVyVmFsdWUiOiJqdXN0VG9TaG93VGhhdFdlQ2FuIn0.8nBoXQQ_Up3HGKBB64cKekw906zES8GXa6QZYygYWD5GbFoLlcPe2RtMMSAzRrHHfGRsHz9F5hJ1CMfaDDy5AA\');\n\n    await db.use(NS, DB);\n    const result = await db.select(\'test\');\n\n    console.log(result);\n    // [\n    //   { id: \'test:record\' },\n    //   { id: \'test:record2\' }\n    // ]\n}\nmain();\n\n
Run Code Online (Sandbox Code Playgroud)\n

使用公钥加密技术

\n

如果需要,您还可以使用公钥/私钥对来验证令牌,而无需共享生成真实令牌所需的秘密。

\n
import crypto from \'node:crypto\';\n\n// Generate Fresh RSA Keys for Access Tokens on Startup\nconst { publicKey, privateKey } = crypto.generateKeyPairSync(\'rsa\', {\n    modulusLength: 4096,\n    publicKeyEncoding: { type: \'spki\', format: \'pem\' },\n    privateKeyEncoding: { type: \'pkcs8\', format: \'pem\' },\n});\n\nasync function main() {\n    // Add our public key to SurrealDB as the verifier\n    await db.query(`DEFINE TOKEN my_token ON DATABASE TYPE RS256 VALUE "${publicKey}";`).then(() => \n\n    console.log(\'yay!\');\n}\nmain();\n
Run Code Online (Sandbox Code Playgroud)\n

SurrealDB 权限

\n

如上所述,可以定义并使用三种类型的令牌来验证查询。

\n

代币类型

\n

命名空间令牌

\n
-- Will apply to the current namespace\nDEFINE TOKEN @name ON NAMESPACE TYPE @algorithm VALUE @secretOrPublicKey;\n\n-- Can also be abbreviated:\nDEFINE TOKEN @name ON NS TYPE @algorithm VALUE @secretOrPublicKey;\n
Run Code Online (Sandbox Code Playgroud)\n

警告:执行命名空间令牌持有者查询时,不会处理表和字段权限。

\n

这种类型的令牌使经过身份验证的用户或系统能够访问定义该令牌的整个命名空间。

\n

这包括对所有数据库中的所有表的选择、创建、更新和删除 (SCUD) 访问权限,以及定义和删除数据库和表的能力。

\n

数据库令牌

\n
-- Will apply to the current database\nDEFINE TOKEN @name ON DATABASE TYPE @algorithm VALUE @secretOrPublicKey;\n\n-- Can also be abbreviated:\nDEFINE TOKEN @name ON DB TYPE @algorithm VALUE @secretOrPublicKey;\n
Run Code Online (Sandbox Code Playgroud)\n

警告:执行数据库令牌持有者查询时,不会处理表和字段权限。

\n

这种类型的令牌使经过身份验证的用户或系统能够访问定义该令牌的整个数据库。

\n

这包括对特定数据库中所有表的选择、创建、更新和删除 (SCUD) 访问,以及定义和删除表的能力。

\n

范围代币

\n
-- Requires a defined scope on which to define the token; scope is defined as a property on the current database.\nDEFINE SCOPE @name;\n\n-- Define the token after we define the scope:\nDEFINE TOKEN @name ON SCOPE @name TYPE @algorithm VALUE @secretOrPublicKey;\n
Run Code Online (Sandbox Code Playgroud)\n

在执行范围令牌持有者的查询时,表和字段权限将照常处理。

\n

这种类型的令牌使经过身份验证的用户或系统能够访问定义了范围的数据库,但仅限于为表和字段定义的权限允许的范围。

\n

这包括对特定数据库中所有表(允许的权限)的选择、创建、更新和删除 (SCUD) 访问权限,但是作用域令牌不能创建、修改、查看表的信息,也不能删除表。

\n

有效负载中的可选id参数允许将范围令牌链接到表行。这可以用于用户帐户、批处理或自动化系统的客户端 ID 等。语义由您决定。在表权限中,可以通过以下方式访问id,$token.id可以通过以下方式访问指向的行$auth

\n

表权限

\n

FULL:无需任何身份验证即可查询

\n
DEFINE TABLE this_table_is_publicly_accessible;\n
Run Code Online (Sandbox Code Playgroud)\n

当您定义表时,请注意,如果您没有为其定义任何权限,则默认情况下可供公众访问 - 即无需任何类型的身份验证。

\n

请记住,使用strict模式时,您需要先显式定义表,然后才能使用它们。为了避免它们被无意中公开,请始终设置某种许可。

\n

NONE:受限表(隐式默认值)

\n
CREATE restricted:hello;\n\n-- The above implicitly creates a table with this definition:\nDEFINE TABLE restricted SCHEMALESS PERMISSIONS NONE;\n
Run Code Online (Sandbox Code Playgroud)\n

如果您未定义表,但开始创建条目,从而隐式创建该表,则会为其提供一组默认权限,不允许公共访问和范围访问。只有数据库令牌持有者和命名空间令牌持有者才能访问数据。

\n

细化表权限

\n
DEFINE TABLE granular_access SCHEMALESS PERMISSIONS\nFOR select FULL\nFOR create,update WHERE $token.someOtherValue = "justToShowThatWeCan"\nFOR delete NONE;\n
Run Code Online (Sandbox Code Playgroud)\n

在这里,我们允许公共访问从表中进行选择,同时只允许令牌中“someOtherValue”设置为“justToShowThatWeCan”的范围用户进行创建和更新。同时,任何拥有范围令牌的人都不能删除。现在只有数据库和命名空间类型令牌持有者可以从表中删除。

\n

精细的字段权限

\n
DEFINE field more_granular ON TABLE granular_access PERMISSIONS\nFOR select FULL\nFOR create,update WHERE $token.someOtherValue = "justToShowThatWeCan"\nFOR delete NONE;\n
Run Code Online (Sandbox Code Playgroud)\n

与全表类似,也可以对单个字段设置权限。

\n

从查询访问令牌和身份验证数据

\n

受保护的 params $session$scope$token、 和$auth包含与客户端相关的额外信息。

\n

要查看可以访问哪些数据,请尝试运行查询:

\n
SELECT * FROM $session;\nSELECT * FROM $token;\nSELECT * FROM $scope;\nSELECT * FROM $auth;\n
Run Code Online (Sandbox Code Playgroud)\n

使用命名空间或数据库令牌时,只有$session$token参数具有值。简要地:

\n
    \n
  • $session一个包含会话数据的对象,看起来最有用的是$session.ip它显示与 SurrealDB 连接的客户端 IP 和传出端口。例子:127.0.0.1:60497
  • \n
  • $token使用于验证会话的 JWT 令牌有效负载中存在的所有字段作为对象可用。
  • \n
  • $scope似乎只包含用户/客户端有权访问的范围的名称。
  • \n
  • $auth当作用域 JWT 还包含字段id并且包含 指定的表行中的数据时,就会出现id。例如,如果id包含users:some_row_id,则将$auth包含指向的行,如果存在,并且范围是否有权访问该行。也可以使用权限对此对象隐藏字段。
  • \n
\n

进一步阅读

\n\n

  • 谢谢@Jeramy 非常详细的回答。我通过在源代码中添加一些日志并在本地编译来找到问题。问题是我没有在令牌中包含令牌过期(“exp”)和“iat”字段,但 Surreal 实际上也会检查这些字段以验证令牌。而不是说令牌是否已过期或这些字段何时丢失。超现实只是抛出一般的“AuthenticationError”。目前的文档并未提及 SurrealDB 所做的额外检查。 (3认同)