保护你的 GraphQL API
|
这是 GraphQL Library 7 版本的文档。对于长期支持 (LTS) 版本 5,请参考 GraphQL Library 5 LTS 版本。 |
本页面提供了关于如何保护使用 Neo4j GraphQL 库创建的 API 的教程。
先决条件
-
设置一个新的 AuraDB 实例。请参阅 创建 Neo4j Aura 实例。
-
使用 Northwind 数据集填充该实例。
|
如果您已经完成了 GraphQL 和 Aura 控制台入门指南,并且希望清除在那里创建的示例节点,请在用 Northwind 数据集填充数据库之前,在 Query 中运行以下代码
|
本教程建立在 GraphQL 建模教程的基础上。具体来说,它扩展了以下类型定义
type Customer @node {
contactName: String!
customerID: ID! @id
orders: [Order!]! @relationship(type: "PURCHASED", direction: OUT)
}
type Order @limit(max: 10, default: 10) @node {
orderID: ID! @id
customer: [Customer!]! @relationship(type: "PURCHASED", direction: IN)
products: [Product!]! @relationship(type: "ORDERS", direction: OUT, properties: "ordersProperties")
}
type Product @node {
productName: String!
category: [Category!]! @relationship(type: "PART_OF", direction: OUT)
orders: [Product!]! @relationship(type: "ORDERS", direction: IN, properties: "ordersProperties")
supplier: [Supplier!]! @relationship(type: "SUPPLIES", direction: IN)
}
type Category @node {
categoryName: String!
products: [Product!]! @relationship(type: "PART_OF", direction: IN)
}
type Supplier @node {
supplierID: ID! @id
companyName: String!
products: [Product!]! @relationship(type: "SUPPLIES", direction: OUT)
}
type ordersProperties @relationshipProperties {
unitPrice: Float!
quantity: Int!
}
与安全相关的指令
GraphQL 库有几个专用于安全的指令:@authentication 和 @authorization,以及 @jwt 和 @jwtClaim。@selectable 和 @settable 指令可用于通过特定操作控制数据字段的可访问性。
Authentication(身份验证)
您可以全局应用 @authentication 指令,也可以仅将其应用于特定字段或特定类型,并仅针对特定操作应用。
以管理员身份为客户、订单、产品、类别和供应商的操作添加身份验证
-
针对客户的
DELETE操作, -
针对订单的
UPDATE和DELETE操作, -
针对产品、类别和供应商的
CREATE、UPDATE和DELETE操作。
type Customer
@node
@authentication(
operations: [DELETE],
jwt: { roles: { includes: "admin" } }
) {
contactName: String!
customerID: ID! @id
orders: [Order!]! @relationship(type: "PURCHASED", direction: OUT)
}
type Order
@node
@authentication(
operations: [UPDATE, DELETE],
jwt: { roles: { includes: "admin" } }
) {
orderID: ID! @id
customer: [Customer!]! @relationship(type: "PURCHASED", direction: IN)
products: [Product!]! @relationship(type: "ORDERS", direction: OUT, properties: "ordersProperties")
}
type Product
@node
@authentication(
operations: [CREATE, UPDATE, DELETE],
jwt: { roles: { includes: "admin" } }
) {
productName: String!
category: [Category!]! @relationship(type: "PART_OF", direction: OUT)
orders: [Product!]! @relationship(type: "ORDERS", direction: IN, properties: "ordersProperties")
supplier: [Supplier!]! @relationship(type: "SUPPLIES", direction: IN)
}
type Category
@node
@authentication(
operations: [CREATE, UPDATE, DELETE],
jwt: { roles: { includes: "admin" } }
) {
categoryName: String!
products: [Product!]! @relationship(type: "PART_OF", direction: IN)
}
type Supplier
@node
@authentication(
operations: [CREATE, UPDATE, DELETE],
jwt: { roles: { includes: "admin" } }
) {
supplierID: ID! @id
companyName: String!
products: [Product!]! @relationship(type: "SUPPLIES", direction: OUT)
}
JSON Web Token (JWT) 身份验证
JWT 身份验证是一种流行的基于令牌的身份验证方法。它允许客户端获取并使用令牌来验证后续请求。
JWT 由编码后的 JSON 数据表示。这些数据可以包含任意字段——具体应包含哪些字段取决于应用程序的偏好。
例如,如果服务器端尝试解析 身份验证 中引入的 roles 字段,那么 JWT 中就应该包含该字段。使用 @jwt 指定 JWT 数据的类型。然后,您可以使用 @jwtClaim 指定嵌套位置中客户 ID 的路径。
例如:
type JWT @jwt {
roles: [String!]!
customerID: String! @jwtClaim(path: "sub")
}
您可以使用像 https://www.jwt.io/ 这样的网站对 JWT 进行编码和解码。
授权
@authorization 指令既可以用于过滤掉用户不应访问的数据,也可以在针对此类数据执行查询时抛出错误。
两者都有各自的使用场景。
为了使非特定用户或管理员无法访问客户数据和订单数据,请考虑以下使用 @authorization 指令进行过滤的方法
type Customer
@node
@authentication(operations: [DELETE], jwt: { roles: { includes: "admin" } })
@authorization(
filter: [
{ operations: [READ], where: { node: { customerID: { eq: "$jwt.customerID" } } } }
{ where: { jwt: { roles: { includes: "admin" } } } }
]
) {
contactName: String!
customerID: ID! @id
orders: [Order!]! @relationship(type: "PURCHASED", direction: OUT)
}
type Order
@node
@authentication(operations: [UPDATE, DELETE], jwt: { roles: { includes: "admin" } })
@authorization(
filter: [
{ where: { node: { customer: { all: { customerID: { eq: "$jwt.customerID" } } } } } }
{ where: { jwt: { roles: { includes: "admin" } } } }
]
) {
orderID: ID! @id
customer: [Customer!]! @relationship(type: "PURCHASED", direction: IN)
products: [Product!]! @relationship(type: "ORDERS", direction: OUT, properties: "ordersProperties")
}
对于敏感数据,您还可以使用验证授权
type Customer
@node
@authentication(operations: [DELETE], jwt: { roles: { includes: "admin" } })
@authorization(
filter: [
{ operations: [READ], where: { node: { customerID: { eq: "$jwt.customerID" } } } }
{ where: { jwt: { roles: { includes: "admin" } } } }
]
) {
contactName: String!
adminNotes: [String!]! @authorization(
validate: [
{ where: { jwt: { roles: { includes: "admin" } } } }
]
)
customerID: ID! @id
orders: [Order!]! @relationship(type: "PURCHASED", direction: OUT)
}
adminNotes 只能由管理员读取,如果用户不是管理员,尝试访问该字段会导致错误。
需要注意的是,通过验证生成的错误消息可能会引起安全问题,因为它们可能会将数据库内部信息暴露给用户。
另请参阅本页的 [best-practice-internal-errors]。
@selectable 和 @settable
为了通过操作直接限制访问,您可以使用 @selectable 和 @settable 指令,例如
type Customer
@node
@authentication(operations: [DELETE], jwt: { roles: { includes: "admin" } })
@authorization(
filter: [
{ operations: [READ], where: { node: { customerID: { eq: "$jwt.customerID" } } } }
{ where: { jwt: { roles: { includes: "admin" } } } }
]
) {
contactName: String!
sensitiveData: String! @selectable(onRead: false, onAggregate: false)
createdAt: DateTime! @settable(onCreate: true, onUpdate: false)
adminNotes: [String!]! @authorization(validate: [{ where: { jwt: { roles: { includes: "admin" } } } }])
customerID: ID! @id
orders: [Order!]! @relationship(type: "PURCHASED", direction: OUT)
}
sensitiveData 字段既不可用于查询,也不可用于订阅或聚合。createdAt 字段可以在创建新客户时设置,但不能进行更新。
完整示例
以下是扩展了安全相关指令的完整类型定义集
type JWT @jwt {
roles: [String!]!
customerID: String! @jwtClaim(path: "sub")
}
type Customer
@node
@authentication(operations: [DELETE], jwt: { roles: { includes: "admin" } })
@authorization(
filter: [
{ operations: [READ], where: { node: { customerID: { eq: "$jwt.customerID" } } } }
{ where: { jwt: { roles: { includes: "admin" } } } }
]
) {
contactName: String!
sensitiveData: String! @selectable(onRead: false, onAggregate: false)
createdAt: DateTime! @settable(onCreate: true, onUpdate: false)
adminNotes: [String!]! @authorization(validate: [{ where: { jwt: { roles: { includes: "admin" } } } }])
customerID: ID! @id
orders: [Order!]! @relationship(type: "PURCHASED", direction: OUT)
}
type Order
@limit(max: 10, default: 10)
@node
@authentication(operations: [UPDATE, DELETE], jwt: { roles: { includes: "admin" } })
@authorization(
filter: [
{ where: { node: { customer: { all: { customerID: { eq: "$jwt.customerID" } } } } } }
{ where: { jwt: { roles: { includes: "admin" } } } }
]
) {
orderID: ID! @id
customer: [Customer!]! @relationship(type: "PURCHASED", direction: IN)
products: [Product!]! @relationship(type: "ORDERS", direction: OUT, properties: "ordersProperties")
}
type Product @node @authentication(operations: [CREATE, UPDATE, DELETE], jwt: { roles: { includes: "admin" } }) {
productName: String!
category: [Category!]! @relationship(type: "PART_OF", direction: OUT)
orders: [Product!]! @relationship(type: "ORDERS", direction: IN, properties: "ordersProperties")
supplier: [Supplier!]! @relationship(type: "SUPPLIES", direction: IN)
}
type Category @node @authentication(operations: [CREATE, UPDATE, DELETE], jwt: { roles: { includes: "admin" } }) {
categoryName: String!
products: [Product!]! @relationship(type: "PART_OF", direction: IN)
}
type Supplier @node @authentication(operations: [CREATE, UPDATE, DELETE], jwt: { roles: { includes: "admin" } }) {
supplierID: ID! @id
companyName: String!
products: [Product!]! @relationship(type: "SUPPLIES", direction: OUT)
}
type ordersProperties @relationshipProperties {
unitPrice: Float!
quantity: Int!
}
最佳实践检查清单
除了身份验证和授权方面的考虑外,还有一些值得遵循的最佳实践来提高 API 的安全性。
内省和数据字段建议
虽然 GraphQL 和 Aura 控制台入门页面 提倡 启用内省 (Enable introspection) 和 启用字段建议 (Enable field suggestions),但在考虑安全性时,并不建议这样做。
这两者都可能泄露可用于深入了解 GraphQL 架构细节并执行针对性恶意操作的信息。除非有正当理由,否则我们建议您在面向客户的实际场景中禁用这两项功能。
限制查询深度
限制查询深度可以禁止可能有害的查询,例如以下递归查询
query {
order(id: 42) {
products {
order {
products {
order {
products {
order {
# and so on...
}
}
}
}
}
}
}
}
这可以通过 GraphQL Armor 实现
import { ApolloServer } from '@apollo/server';
import { startStandaloneServer } from '@apollo/server/standalone';
import { ApolloArmor } from '@escape.tech/graphql-armor';
import { readFileSync } from 'fs';
// Assume you have your schema definition in a string or file.
const typeDefs = readFileSync('./your-schema.graphql', 'utf-8');
const resolvers = { /* Your resolvers here. */ };
// Instantiate GraphQL Armor and configure the maxDepth plugin.
const armor = new ApolloArmor({
maxDepth: {
enabled: true,
n: 5, // Sets the maximum allowed query depth to 5.
},
});
// Get the security plugins provided by Armor.
const plugins = armor.protect();
const server = new ApolloServer({
typeDefs,
resolvers,
plugins: [...plugins], // Add the armor plugins to Apollo Server.
});
const { url } = await startStandaloneServer(server, {
listen: { port: 4000 },
});
console.log(`🚀 Server ready at ${url}`);
分页列表字段
返回大型查询结果列表可能会对服务器性能产生负面影响。例如,类似下面的查询会返回大量的节点
query {
order(first: 1000) {
orderID
products(last: 100) {
productName
productCategory
}
}
}
您可以通过对查询结果进行分页来防止基于此类查询的拒绝服务攻击。
基于类型定义的服务器端分页解决方案可能如下所示
// Connection types for root-level data (Orders list)
type OrderEdge {
node: Order!
cursor: String!
}
type OrderConnection {
edges: [OrderEdge!]!
pageInfo: PageInfo!
}
type PageInfo {
startCursor: String
endCursor: String
hasNextPage: Boolean!
}
// The root query that is targeted
type Query {
orders(first: Int, after: String): OrderConnection
}
这与定义在 Order 类型定义上的 @limit 指令配合使用。
通过 @limit 指令和服务器端分页,上述潜在的有害查询最多只会产生 10 个 Order 对象;并且根据 first 和 after 的值,查询可以进一步减少结果并设置偏移量。
同样的方法也可以应用于产品。
限制 API 速率
限制 API 速率意味着为客户端在特定时间内可以发送的请求数量或这些请求的成本设置上限。实现方法不止一种。以下章节概述了几种方法。
查询成本点
漏桶算法 (Leaky bucket algorithm) 代表了一种减缓同时处理多个请求的算法解决方案。
查询成本分析
GraphQL Armor 提供了一种限制 GraphQL 查询成本的方法。
使用超时
为了防止 API 无响应或遭受拒绝服务攻击,使用超时是可行的。这样,后续的查询就不会被长时间运行的前序查询所阻塞。
您可以通过 GraphQL 库驱动程序设置超时,请参阅 上下文中的事务配置。
延伸阅读
Neo4j 具有 基于角色的访问控制 机制,可用于进一步提高安全性。
有关 GraphQL 中更多与安全相关的主题,请参考 GraphQL 安全 页面。