于 10 月 8 日至 10 日在纽约市加入我们,了解关于 GraphQL 联邦和 API 平台工程的最新技巧、趋势和新闻。参加 2024 年纽约市 GraphQL 大会
文档
免费开始

GraphOS 路由中的授权

使用集中治理层增强子图安全性


此功能仅在 GraphOS 专用或企业计划中可用。
要比较所有计划类型对 GraphOS 功能的支持,请参阅 定价页面.

API 提供对业务关键数据的访问。未受限制的访问可能导致数据泄露、经济损失或潜在的拒绝服务。即使是内部服务,检查也是必需的,以限制数据仅对有权访问的各方可用。

服务可能有它们自己的访问控制,但在 Apollo 路由器中强制执行授权有以下几个原因:

  • 最佳查询执行:在处理请求之前验证授权允许提前终止未授权请求。在图边缘停止未授权请求可以减少对服务的负载并提高性能。

    ❌ Subquery
    ❌ Subquery
    ⚠️Unauthorized
    request
    GraphOS Router
    Users
    API
    Posts
    API
    Client

    • 如果特定子查询中的每个都需要授权,则查询规划器可以消除整个子图请求,对于未授权请求。例如,一个请求可能有权查看特定用户在社交媒体平台上的帖子,但没有权限查看该用户的任何个人信息(PII)。了解更多信息,请参阅它是如何工作的

    ✅ Authorized
    subquery
    ❌ Unauthorized
    subquery
    ⚠️ Partially authorized
    request
    GraphOS Router
    Users
    API
    Posts
    API
    Client

    • 此外,查询去重根据所需的授权将请求组进行分组。如果没有正确的授权,可以将整个组从中删除。
  • 声明性访问规则:您在字段级别定义访问控制, 组合跨您的服务来实现这些规则。这些规则创建出图原生的治理,而不需要在额外的编排层中。

  • 原则性架构:通过,路由器集中授权逻辑,同时允许在服务级别进行审计。这种集中的授权是其他服务层可以加强的初始检查点。


    🔐 Router layer                                                   
    🔐 Service layer
    Subquery
    Subquery
    Request
    GraphOS Router
    Users
    API
    Posts
    API
    Client

💡 提示

要了解为什么在路由器层进行授权是理想的,请观看Andrew Carlson在2024年奥斯汀API峰会上的演讲: 使用GraphQL集中化数据访问控制

访问控制是如何工作的

GraphOS路由器访问控制通过授权指令,该指令定义了访问您的中特定字段和类型的权限:

例如,想象你正在构建一个包含用户子图的社交媒体平台。你可以使用@requiresScopes指令来声明查看其他用户的信息需要read:user作用域:

type Query {
users: [User!]! @requiresScopes(scopes: [["read:users"]])
}

你可以使用@authenticated指令来声明用户必须登录才能更新自己的信息:

type Mutation {
updateUser(input: UpdateUserInput!): User! @authenticated
}

你可以定义指令——一起或分别——字段级别上微调你的访问控制。当一个字段及其字段类型都声明了时,将尝试所有这些指令,如果其中任何一个未授权,则将移除该字段。GraphOS 组合将这些限制集成到架构中,以便每个子图的限制都得到尊重。然后,路由器将在所有传入请求上强制执行这些指令

先决条件

注意

只有GraphOS Router支持授权指令——@apollo/gateway不支持。如果你想使用它们,请查看迁移指南

在使用您的中的授权指令之前,您必须:

配置请求索赔

索赔是一个请求的身份验证和范围的详细资料。它们可能包括诸如请求用户的 ID 和分配给该用户的任何授权范围之类的详细信息例如, read:profiles。授权指令使用请求的索赔来评估哪些 字段和类型是受授权的。

为了向路由器提供其所需的索赔,您必须配置 JSON Web Token (JWT) 身份验证或添加一个外部协处理器,该协处理器将索赔添加到请求的上下文中。在某些情况下(如下文所述),您可能需要两者都使用。

  • JWT 身份验证配置:如果您配置 JWT 身份验证,GraphOS 路由器 会自动将 JWT 令牌的索赔添加到请求上下文中的 apollo_authentication::JWT::claims 键。
  • 通过协处理器添加索赔:如果您无法使用 JWT 身份验证,您可以使用 通过协处理器添加索赔。协处理器允许您使用自定义代码加入到 GraphOS 路由器的请求处理生命周期中。
  • 通过协处理器增强 JWT 索赔:您的授权策略可能需要超越您的 JSON Web 令牌提供的信息。例如,令牌的索赔可能包括用户 ID,然后您可以使用该 ID 来检索用户角色。对于这种情况,您可以使用 通过协处理器增强 JSON Web 令牌的索赔

授权指令

授权 directives默认开启。要禁用它们,请在您的 router's YAML 配置文件 中包含以下内容:

router.yaml
authorization:
directives:
enabled: false

@requiresScopes
Since1.29.1

The @requiresScopes directive 标记 fields和 types 为基于所需范围的限制性。指令包含一个带有所需作用域数组 scopes 的参数,以声明所需的作用域:

@requiresScopes(scopes: [["scope1", "scope2", "scope3"]])

💡 提示

使用 @requiresScopes 当访问 field或 type 只取决于与索赔对象或访问令牌关联的索赔时。

如果你的授权验证逻辑或数据更加复杂例如检查头中的特定值或从数据库等其他来源查找数据并且不是只基于一个声明对象或访问令牌,请使用 @policy 代替。

根据请求中存在的范围,路由器会筛选掉未授权的 字段 和类型。

您可以使用布尔逻辑来定义所需的范围。有关详细信息,请参阅 组合所需范围

指令通过在一个请求的上下文中加载 apollo_authentication::JWT::claims 键处的声明对象来验证所需的范围。声明对象的 scope 键的值应该是一个空格分隔的字符串,其格式由 OAuth2 RFC中访问令牌范围的 定义。

claims = context["apollo_authentication::JWT::claims"]
claims["scope"] = "scope1 scope2 scope3"

用法

要在子图 使用 @requiresScopes 指令,您可以像这样从 @link 指令导入它

extend schema
@link(
url: "https://specs.apollo.dev/federation/v2.5",
import: [..., "@requiresScopes"])

它定义如下

scalar federation__Scope
directive @requiresScopes(scopes: [[federation__Scope!]!]!) on OBJECT | FIELD_DEFINITION | INTERFACE | SCALAR | ENUM

使用 AND/OR 逻辑组合所需的范围

请求必须包含内层 scopes 数组中的所有元素才能解析相关的 字段 或类型。

@requiresScopes(scopes: [["scope1", "scope2", "scope3"]])

在前面的示例中,请求需要 scope1 AND scope2 AND scope3 以获得授权。

您可以使用嵌套数组引入 OR

@requiresScopes(scopes: [["scope1"], ["scope2"], ["scope3"]])

对于前面的示例,请求需要授权scope1 scope2 scope3

您可以根据需要嵌套数组和元素,以实现所需的逻辑。例如

@requiresScopes(scopes: [["scope1", "scope2"], ["scope3"]])

这种语法要求请求必须包含(scope1并且 scope2 只需 scope3 才能获得授权。

示例 @requiresScopes 应用场景

想象一下,您正在构建一个社交媒体平台,允许用户仅在具备所需的权限的情况下查看其他用户的信息。您的模式可能如下所示

type Query {
user(id: ID!): User @requiresScopes(scopes: [["read:others"]])
users: [User!]! @requiresScopes(scopes: [["read:others"]])
post(id: ID!): Post
}
type User {
id: ID!
username: String
email: String @requiresScopes(scopes: [["read:email"]])
profileImage: String
posts: [Post!]!
}
type Post {
id: ID!
author: User!
title: String!
content: String!
}

根据请求附加的作用域,路由器router会以不同的方式执行以下查询。如果请求仅包含read:others 作用域,那么路由器会执行以下过滤查询:

对路由器的原生查询
query {
users {
username
profileImage
email
}
}
作用域: 'read:others'
query {
users {
username
profileImage
}
}

响应会在 /users/@/email 路径包括一个错误,因为该字段需要 read:emails 作用域。如果请求包含 read:others read:emails 作用域集合,则路由器可以成功执行整个查询。

对于未授权的字段,路由器返回 null,并应用 标准的 GraphQL null 传播规则

未授权请求的响应
{
"data": {
"me": null,
"post": {
"title": "Securing supergraphs",
}
},
"errors": [
{
"message": "Unauthorized field or type",
"path": [
"me"
],
"extensions": {
"code": "UNAUTHORIZED_FIELD_OR_TYPE"
}
},
{
"message": "Unauthorized field or type",
"path": [
"post",
"views"
],
"extensions": {
"code": "UNAUTHORIZED_FIELD_OR_TYPE"
}
}
]
}

@authenticated
Since1.29.1

@authenticated 指令用于标记特定字段和类型需要身份验证。它通过检查请求上下文中的 apollo_authentication::JWT::claims 键来工作,该键可以是 JWT 身份验证插件在请求包含有效 JWT 时添加,也可以是由身份验证协处理器添加。如果该键存在,则表示请求已验证,路由器会完全执行查询。如果请求没有认证,路由器在规划查询之前会移除 @authenticated 字段,并且仅执行不需要身份验证的查询部分。

用法

要在子图(subgraph)中使用 @authenticated 指令,您可以像这样从 @link 指令导入它:

extend schema
@link(
url: "https://specs.apollo.dev/federation/v2.5",
import: [..., "@authenticated"])

它定义如下

directive @authenticated on OBJECT | FIELD_DEFINITION | INTERFACE | SCALAR | ENUM

示例 @authenticated 用例

深入 社交媒体示例:假设未认证用户可以查看帖子的标题、作者和内容。然而,您只想让认证用户看到帖子收到多少浏览次数。您还需要能够查询认证用户的信息。

您的模式中相关的部分可能如下所示

type Query {
me: User @authenticated
post(id: ID!): Post
}
type User {
id: ID!
username: String
email: String @requiresScopes(scopes: [["read:email"]])
posts: [Post!]!
}
type Post {
id: ID!
author: User!
title: String!
content: String!
views: Int @authenticated
}

考虑下面的 查询:

查询示例
query {
me {
username
}
post(id: "1234") {
title
views
}
}

对于认证请求,路由器会执行整个查询。对于未认证请求,路由器会移除 @authenticated 字段并执行筛选后的查询。

认证请求执行的查询
query {
me {
username
}
post(id: "1234") {
title
views
}
}
未认证请求执行的查询
query {
post(id: "1234") {
title
}
}

对于未认证请求,路由器不尝试解析顶级 me 查询,也不解析 id: "1234" 的帖子的浏览次数。响应保留了初始请求的形状,但返回 null 以授权字段,并应用 标准的 GraphQL null 传播规则

未认证请求的响应
{
"data": {
"me": null,
"post": {
"title": "Securing supergraphs",
}
},
"errors": [
{
"message": "Unauthorized field or type",
"path": [
"me"
],
"extensions": {
"code": "UNAUTHORIZED_FIELD_OR_TYPE"
}
},
{
"message": "Unauthorized field or type",
"path": [
"post",
"views"
],
"extensions": {
"code": "UNAUTHORIZED_FIELD_OR_TYPE"
}
}
]
}

如果请求的所有字段都需要身份验证且请求未认证,路由器将生成一个错误,指出查询未授权。

@policy
Since1.35.0

@policy 指令标记字段和类型基于在 Rhai 脚本或协处理器中评估的授权策略作为受限。这可以通过自定义授权验证,超出身份验证和作用域的范围。当我们需要比验证列表中声明值存在更复杂的策略评估时很有用(例如:检查头中的特定值)。

💡 提示

如果仅通过声明对象或访问令牌相关的声明访问字段或类型受限,则请考虑使用 @requiresScopes 而不是。

@policy 指令包括一个 policies 参数,它定义了一个字符串数组,表示所需策略,没有任何格式化约束。通常,您可以根据需要将字符串作为任何格式的参数来使用。以下示例展示了可能需要支持角色的策略:

@policy(policies: [["roles:support"]])

使用 @policy 指令需要 Supergraph 插件 来评估授权策略。这在将路由器授权与现有的授权堆栈相结合或与数据库中的查找关联策略执行时很有用。

以下是 @policy 通过路由器请求生命周期处理的概述:

  • RouterService 的级别,GraphOS 路由器从模式中提取与请求相关的策略列表,然后将其存储在请求的上下文中 apollo_authorization::policies::required 作为映射 策略 -> null|true|false

  • SupergraphService 的级别,您必须提供 Rhai 脚本或协处理器来评估映射。如果策略得到验证,脚本或协处理器应将其值设置为 true 或将其设置为 false。如果值保留为 null,则路由器将 Treat it as false。之后,路由器将过滤请求的类型和字段,只保留那些策略为 true 的。

  • 如果一个子图查询的任何字段都没有通过其授权策略,则路由器将停止进一步处理该查询并拒绝未经授权的子图请求。这是 @policy 和其他授权指令的关键优势之一。

用法

要在子图中使用 @policy 指令,您可以像这样从 @link 指令 导入它:

extend schema
@link(
url: "https://specs.apollo.dev/federation/v2.6",
import: [..., "@policy"])

@policy 指令定义如下:

scalar federation__Policy
directive @policy(policies: [[federation__Policy!]!]!) on OBJECT | FIELD_DEFINITION | INTERFACE | SCALAR | ENUM

使用 @policy 指令需要 Supergraph 插件 来评估授权策略。您可以使用 Rhai 脚本协处理器 来此操作。有关更多信息,请参考以下 示例用例。(尽管原生插件也可以评估授权策略,但我们不建议使用它。)

结合 AND/OR 逻辑策略

授权验证使用 AND 逻辑在内部级别 policies 数组中的元素之间,其中请求必须包含内部级别 policies 数组中的所有元素以解析相关的字段或类型。以下是示例,请求需要 policy1 AND policy2 AND policy3 才能获得授权:

@policy(policies: [["policy1", "policy2", "policy3"]])

另一方面,要引入 OR 逻辑,可以使用嵌套数组。以下是示例,请求需要 policy1 OR policy2 OR policy3 才能获得授权:

@policy(policies: [["policy1"], ["policy2"], ["policy3"]])

您可以根据需要嵌套数组和元素以实现所需的逻辑。以下是示例,其语法要求请求具有 policy1 AND policy2ORpolicy3 以获得授权:

@policy(policies: [["policy1", "policy2"], ["policy3"]])

示例 @policy 用法

使用协处理器

更深入地探讨 社交媒体示例:假设您只想让用户访问自己的个人资料和信用卡信息。在可用的授权 指示中,您使用 @policy 而不是 @requiresScopes,因为在验证逻辑中不仅仅依赖于访问令牌的作用域。

您可以将授权策略添加 read_profileread_credit_card。您的模式的相关部分可能如下所示:

type Query {
me: User @authenticated @policy(policies: [["read_profile"]])
post(id: ID!): Post
}
type User {
id: ID!
username: String
email: String @requiresScopes(scopes: [["read:email"]])
posts: [Post!]!
credit_card: String @policy(policies: [["read_credit_card"]])
}
type Post {
id: ID!
author: User!
title: String!
content: String!
views: Int @authenticated
}

您可以使用在 请求阶段调用的称为 协处理器 的东西来接收和执行策略列表。

如果您这样配置您的 router

router.yaml
coprocessor:
url: http://127.0.0.1:8081
supergraph:
request:
context: true

协处理器随后可以接收此格式的请求

{
"version": 1,
"stage": "SupergraphRequest",
"control": "continue",
"id": "d0a8245df0efe8aa38a80dba1147fb2e",
"context": {
"entries": {
"apollo_authentication::JWT::claims": {
"exp": 10000000000,
"sub": "457f6bb6-789c-4e8b-8560-f3943a09e72a"
},
"apollo_authorization::policies::required": {
"read_profile": null,
"read_credit_card": null
}
}
},
"method": "POST"
}

用户可以阅读自己的个人资料,因此 read_profile 将成功。但只有会计系统应该能够查看信用卡,所以 read_credit_card 将失败。协处理器然后将返回:

{
"version": 1,
"stage": "SupergraphRequest",
"control": "continue",
"id": "d0a8245df0efe8aa38a80dba1147fb2e",
"context": {
"entries": {
"apollo_authentication::JWT::claims": {
"exp": 10000000000,
"sub": "457f6bb6-789c-4e8b-8560-f3943a09e72a"
},
"apollo_authorization::policies::required": {
"read_profile": true,
"read_credit_card": false
}
}
}
}
使用 Rhai 脚本

另一个例子,假设您想限制对支持用户发布的访问。由于 policies 参数是一个字符串,您可以将其设置为 Rhai 脚本可以解析和评估的 "<key>:<value>" 格式。

您的模式中相关的部分可能如下所示

type Query {
me: User @policy(policies: [["kind:user"]])
}
type User {
id: ID!
username: String @policy(policies: [["roles:support"]])
}

然后,您可以使用以下 Rhai 脚本来解析和评估 policies 字符串:

fn supergraph_service(service) {
let request_callback = |request| {
let claims = request.context["apollo_authentication::JWT::claims"];
let policies = request.context["apollo_authorization::policies::required"];
if policies != () {
for key in policies.keys() {
let array = key.split(":");
if array.len == 2 {
switch array[0] {
"kind" => {
policies[key] = claims[`kind`] == array[1];
}
"roles" => {
policies[key] = claims[`roles`].contains(array[1]);
}
_ => {}
}
}
}
}
request.context["apollo_authorization::policies::required"] = policies;
};
service.map_request(request_callback);
}

订阅的特殊情况

当使用 @policy 授权时, 事件将从执行服务重新启动,这意味着如果订阅会话的授权状态发生变化,则它无法再次通过查询计划,会话应该关闭。为此,应该在执行服务级别再次评估策略,如果它们发生变化,则应该返回错误以停止订阅。

组合和联盟

GraphOS's 对授权指令的组合策略是有意累加的。当您在 中的字段和类型上定义授权指令时,GraphOS 将它们组合到超级图模式中。换句话说,如果子图字段或类型包含 @requiresScopes@authenticated@policy 指令,它们也将设置在超级图上。

AND/OR 逻辑的组合

如果共享的 子图 字段 包含多个 指令组合会合并它们。例如,假设 me 查询在一个子图中需要 @authentication

子图 A
type Query {
me: User @authenticated
}
type User {
id: ID!
username: String
email: String
}

并在另一个子图中包含 read:user 权限:

子图 B
type Query {
me: User @requiresScopes(scopes: [["read:user"]])
}
type User {
id: ID!
username: String
email: String
}

请求需要同时被认证 AND 并拥有所需的权限。回想一下,@authenticated 指令只检查请求的上下文中是否存在 apollo_authentication::JWT::claims 键,所以只要请求包含权限,认证就有了保证。

如果多个共享的 子图 字段 包含 @requiresScopes超级图模式将以用于将单个 @requiresScopes 用法的 权限组合起来的相同逻辑 合并它们。例如,如果一个子图需要在 users 查询上 read:others 权限:

子图 A
type Query {
users: [User!]! @requiresScopes(scopes: [["read:others"]])
}

另一个子图为 users 查询需要 read:profiles 权限:

子图 B
type Query {
users: [User!]! @requiresScopes(scopes: [["read:profiles"]])
}

然后,超级图模式需要给它两个作用域。

超级图
type Query {
users: [User!]! @requiresScopes(scopes: [["read:others", "read:profiles"]])
}

结合单个使用 @requiresScopes 的作用域类似,您可以使用嵌套数组来引入或(OR)逻辑:

子图 A
type Query {
users: [User!]! @requiresScopes(scopes: [["read:others", "read:users"]])
}
子图 B
type Query {
users: [User!]! @requiresScopes(scopes: [["read:profiles"]])
}

由于两个作用域数组都是嵌套数组,因此它们将使用或(OR)逻辑组合到超级图模式中:

超级图
type Query {
users: [User!]! @requiresScopes(scopes: [["read:others", "read:users"], ["read:profiles"]])
}

这种语法表示请求需要(读取:其他人 与(AND) 读取:用户)作用域或仅为读取:轮廓作用域进行授权。

授权和 @key 字段

@key 指令类似,它允许您创建一个字段,这些字段可以跨多个子图解析。如果您在@key 指令义的字段上使用授权指令,Apollo 仍然会使用这些字段在子图之间组合实体,但是客户端无法直接查询它们。

考虑以下示例子图模式

Product 子图
type Query {
product: Product
}
type Product @key(fields: "id") {
id: ID! @authenticated
name: String!
price: Int @authenticated
}
库存子图
type Query {
product: Product
}
type Product @key(fields: "id") {
id: ID! @authenticated
inStock: Boolean!
}

未经身份验证的请求将成功执行此查询:

query {
product {
name
inStock
}
}

具体来说,在幕后,路由器将使用id字段来解析Product实体,但它不会将其返回。

对于以下查询,未经身份验证的请求将为id解析null。由于id是一个非空字段,因此product将返回null

query {
product {
id
username
}
}

此行为类似于您可以使用合约@inaccessible指令创建的。

授权和接口

如果一个类型实现了接口需要授权,未经授权的请求可以查询接口,但不能查询任何需要授权的类型部分。

例如,考虑这个模式,其中接口Post不需要身份验证,但实现了接口PrivateBlog的类型需要:

type Query {
posts: [Post!]!
}
type User {
id: ID!
username: String
posts: [Post!]!
}
interface Post {
id: ID!
author: User!
title: String!
content: String!
}
type PrivateBlog implements Post @authenticated {
id: ID!
author: User!
title: String!
content: String!
publishAt: String
allowedViewers: [User!]!
}

如果未通过验证的请求发出此查询:

query {
posts {
id
author
title
... on PrivateBlog {
allowedViewers
}
}
}

路由器将按以下方式过滤查询:

query {
posts {
id
author
title
}
}

响应将包括一个在/posts/@/allowedViewers路径处的"UNAUTHORIZED_FIELD_OR_TYPE"错误。

查询去重

您可以在路由器中启用查询去重以减少对子图查询的重复请求。路由器通过缓冲类似查询并重复使用结果来实现这一点。

查询去重会考虑授权。首先,路由器将未通过验证的查询分组在一起。然后,它根据所需的作用域集将通过验证的查询分组。它在满足请求时使用这些组高效地执行查询。

自省

在路由器中默认关闭,这是最佳的生产实践。如果您选择启用它,请注意授权指令不会影响自省。所有需要授权的字段。

关闭自省后,您可以使用GraphOS的模式注册来探索您的超级图模式并让您的同事也能做到这一点。如果您想要从图形中完全删除字段而不是仅阻止访问(即使在自省开启的情况下),请考虑构建一个合同图

配置选项

授权插件的行为可以用各种选项进行修改。

reject_unauthorized

reject_unauthorized选项配置是否在授权指令失败或查询的任何部分被授权指令过滤时拒绝整个查询。启用时,响应包含受影响的路径列表。

router.yaml
authorization:
directives:
enabled: true
reject_unauthorized: true # default: false

errors

默认情况下,当查询的一部分通过授权进行筛选时,被筛选的路径列表将添加到响应中,并由路由器记录。这种行为可以根据您的需求进行自定义。

记录

通过启用记录选项,您可以选择是否将查询筛选的结果输出为一个日志事件。

router.yaml
authorization:
directives:
errors:
log: false # default: true

注意

如果根据客户端权限对查询的部分进行筛选被视为正常的操作,那么应该禁用记录选项。

响应

您可以通过配置响应来定义应该将哪些被筛选的路径包含在GraphQL响应中:

  • 错误(默认):将筛选的路径放置在GraphQL错误中
  • 扩展:将筛选的路径放置在。在客户端抑制异常的同时,仍然提供查询部分被筛选的信息
  • 禁用:隐藏查询被筛选的所有信息。
router.yaml
authorization:
directives:
errors:
response: "errors" # possible values: "errors" (default), "extensions", "disabled"

dry_run

dry_run选项允许在不修改查询的情况下执行授权指令,并评估授权策略的影响,而不干扰现有的流量。它将生成并作为响应的一部分返回未授权路径的列表。

router.yaml
authorization:
directives:
enabled: true
dry_run: true # default: false
上一页
JWT认证
下一页
子图认证
评分文章评分在GitHub上编辑编辑论坛Discord

©2024Apollo Graph Inc.,商业名称Apollo GraphQL。

隐私政策

公司