GraphQL 安全实现
GraphQL 的主要风险不来自语法本身,而来自“单个端点可组合访问很多对象和字段”。只在网关检查 /graphql 是否允许访问,无法保护字段、对象和租户边界。
认证与授权分层
推荐顺序:
- HTTPS 终止与基础 HTTP 限制;
- OAuth 2.0 access token 或服务端会话认证;
- 校验 token 的签发者、受众、有效期、scope 和客户端约束;
- 解析、校验 GraphQL 文档;
- 计算深度、广度、列表倍率和字段成本;
- 在业务授权层检查对象、字段、动作和租户;
- 执行数据访问,并对响应和审计日志脱敏。
字段授权不应散落成不一致的 resolver 条件。更稳妥的做法是让 resolver 调用统一策略层,例如 canRead(user, object, field) 或领域服务,在数据查询时同时施加租户与行级条件。
ID 不是授权
客户端能猜到或通过其他字段取得对象 ID,不代表它有权读取该对象。所有按 ID、游标、关系或批量加载器读取数据的路径都必须执行相同的对象级和租户级授权。
OAuth 与 scope 设计
- access token 的
aud应明确指向该 GraphQL API; - scope 表达粗粒度 API 能力,例如
users:read、users:write; - 组织角色、数据归属、字段敏感级别等细粒度规则仍由服务端授权策略判断;
- 机器调用优先使用独立工作负载身份和最小权限,不共享个人 token;
- 禁止在 URL、GraphQL 变量、日志或
extensions中回显 token。
查询需求控制
至少实施以下控制,而且在执行前拒绝明显超限操作:
| 控制 | 防护目标 |
|---|---|
| 列表分页与最大页大小 | 防止单字段返回无限数据 |
| 最大深度及更严格的列表嵌套深度 | 防止递归关系指数扩张 |
| 顶层字段、alias、fragment 和批量操作上限 | 防止浅层但超宽查询 |
| 字段权重和查询复杂度预算 | 约束昂贵字段与多层列表的组合成本 |
| 身份/客户端/租户级成本限流 | 避免只按 HTTP 请求数限流被绕过 |
| 超时、并发和下游调用上限 | 限制单次执行占用资源 |
一方客户端可在生产环境只允许经过审核的可信文档。公开给第三方自由查询的 API 无法只靠白名单,应提供明确的复杂度规则、配额反馈和稳定错误码。
输入与输出安全
- 使用 variables,避免拼接 GraphQL 文档;
- 类型校验之后仍检查字符串长度、数值范围、URL、HTML 和领域规则;
- 数据库查询必须参数化,不能把参数或字段名直接拼入 SQL/LDAP/命令;
- 自定义 scalar 必须同时定义解析、序列化和边界条件;
- 错误响应隐藏栈、SQL、内部地址和存在性敏感信息;
- 查询日志默认不记录敏感变量,对请求文档使用摘要、操作名或经净化的结构化记录。
Introspection、CSRF 与缓存
禁用生产 introspection 可以降低 schema 的可发现性,但不是安全边界。授权、需求控制和错误脱敏仍是必需项;对于一方客户端,可信文档通常比单独关闭 introspection 更有效。
浏览器环境还应:
- mutation 使用 POST 和
application/json; - 配置严格的 CORS origin、方法与凭据策略;
- cookie 会话采用
SameSite、CSRF token 或自定义请求头等防护; - 不接受可由普通 HTML 表单跨站提交的宽松媒体类型;
- 含个人或租户数据的响应设为私有或禁止缓存,并让缓存键覆盖身份上下文。
上线检查表
- [ ] 全站 HTTPS,认证在 GraphQL 执行前完成
- [ ] token 校验
iss、aud、有效期和 scope - [ ] 每个对象、字段、mutation 与租户边界有服务端授权
- [ ] 列表分页、深度、广度、alias、批量和复杂度均有限额
- [ ] mutation 有幂等/重试策略,敏感操作有审计
- [ ] 输入有业务校验,错误和日志不泄露敏感信息
- [ ] introspection 与可信文档策略符合 API 的开放范围
- [ ] CORS、CSRF、缓存和超时按实际认证方式配置
- [ ] schema 变更经过兼容性检查并有弃用窗口
参考:GraphQL 官方安全指南。