认证(Authentication,你是谁)与授权(Authorization,你能做什么)是每个 GraphQL 服务的第一道安全门。REST 时代我们在路由层按 URL 配权限,但 GraphQL 只有一条路由——权限判断下沉到了 resolver 层和字段层。这个差异是 GraphQL 安全设计的地基:没有「按字段授权」的 GraphQL 服务,就像在 REST 里只保护了 /users 却没保护返回的用户列表字段。
本文从认证凭证在 GraphQL 中的传递讲起,深入授权模型(RBAC/ABAC)、resolver 层授权、字段级权限、Federation 跨子图授权,最后覆盖 GraphQL 特有的攻击面与授权测试。
一、认证凭证的传递与 context
1.1 凭证从哪来
GraphQL 请求是单个 POST 端点,凭证通过 HTTP 头传递:
# 三种主流凭证
# 1) Authorization: Bearer <JWT>(最常用,无状态)
# 2) Cookie(浏览器场景,Session 服务端存储)
# 3) 自定义头/API Key(机器间,网关层校验)
# 关键: 凭证绝不进 query 字符串/body(会进日志与缓存)
1.2 解析进 context
认证应在进入 resolver 之前完成,把解析好的用户对象注入 context,resolver 只消费:
// 服务器启动时解析 token → 注入 context
server = new ApolloServer({
context: async ({ req }) => {
const token = extractToken(req.headers.authorization);
const user = token ? await authService.verify(token) : null;
return { user, token }; // 所有 resolver 从这里拿当前用户
},
});
工程铁律:认证只做一次(在 context),授权在 resolver 反复用。别让每个 resolver 各自去解 token。
1.3 匿名与可选认证
# 三种模式
# 强制认证: 所有操作都需登录(后台类 API)
# 可选认证: context.user 可能为 null,resolver 按需分支
# (公开字段可读,私有字段需登录)
# 混合: 公开端点匿名,写/敏感操作强制
# 注意: 可选认证最容易漏防——"判断 user 为空就返回公共数据",别把私有数据当公共返回
二、JWT 与 Session 的工程选择
2.1 JWT 的用与防
| 维度 | JWT | Session |
|---|---|---|
| 状态存储 | 无状态(token 自包含) | 服务端存储 |
| 扩展性 | 横向扩展友好 | 需共享 session 存储 |
| 吊销 | 难(需黑名单/版本号) | 简单(删 session) |
| 适用 | 分布式 API、跨服务 | 单域 Web 应用 |
JWT 落地细节:
# 1) 签名算法: 用 RS256(非对称),别用 HS256 泄漏密钥
# 2) 过期: access token 短时效(15 分钟)+ refresh token 轮换
# 3) 声明: sub(用户 id)、role、scope、jti(唯一 id)、exp
# 4) 校验: 验签名 + 验过期 + 验 iss/aud + 验 jti 是否已吊销
# 5) 安全: token 不进 URL、日志打码、客户端存安全存储
2.2 refresh token 轮换
# 轮换协议(防重放)
# 1) refresh token 每次使用即作废,换发新 refresh + access
# 2) 服务端存 refresh token 的 hash + 轮换记录
# 3) 检测到"旧 refresh 再次使用"(重放)→ 吊销整条 token 链
# 收益: 盗取 refresh token 的攻击窗口被压缩到一次使用
三、授权模型:RBAC 与 ABAC
3.1 RBAC:角色分配
RBAC(Role-Based Access Control)按角色授权,最简单也最常见:
# 用户 → 角色 → 权限
# 角色: admin / editor / viewer
# 权限: user:read / user:write / user:delete
# resolver 里: 校验当前用户角色是否含所需权限
# 优点: 模型简单、可管理
# 局限: 细粒度("仅本部门"、"仅自己的数据")不够用
3.2 ABAC:属性授权
ABAC(Attribute-Based Access Control)用属性规则表达细粒度授权:
# 规则: 用户属性 + 资源属性 + 上下文 → 允许/拒绝
# 例: 允许 staff 读取 本部门 且 未归档 的工单
# 例: 允许 owner 更新自己的资料
# 实现: 策略引擎(OPA/Cedar)或 resolver 内规则函数
# 适用: 多租户、数据级权限、动态策略
# 代价: 规则复杂、调试难、性能开销
3.3 混合实践
# 生产推荐: RBAC 定"能力边界",ABAC 定"数据边界"
# 角色管: 能调用哪些字段/操作(静态)
# 属性管: 能看哪些行/哪些对象(动态,数据级)
# 分层校验:
# 字段级 RBAC(能不能查该字段)
# + 行级 ABAC(能查到哪些记录)
四、resolver 层授权:三层防护
4.1 入口层授权
# 三层防护
# 1) 传输/网关层: 认证 + 基础校验(401/403,拦截非法 token)
# 2) 字段入口层: resolver 开头校验权限(能否操作该字段)
# 3) 数据层: 行级过滤(能读到哪些数据)
# 例: 查询订单
# - 网关验 token → 通过
# - resolver 查角色是否有 order:read → 通过
# - 数据层 WHERE user_id = 当前用户 OR is_public → 只返回该用户订单
// resolver 层的授权模板
const order = async (_parent, args, ctx) => {
assertPermission(ctx, "order:read"); // 字段级 RBAC
const rows = await db.orders.findByOwner( // 行级 ABAC
{ ownerId: ctx.user.id, scope: ctx.user.scope }
);
return rows;
};
4.2 授权复用与中间件
授权逻辑要复用,别在每个 resolver 里复制粘贴:
# 模式
# 1) 高阶函数包装: requirePermission('order:read')(resolver)
# 2) schema 指令: @auth(role: "admin") 声明式(graphql-shield/graphql-directive)
# 3) 统一守卫: 在 resolver 执行前后统一钩子
# 选型: schema 指令把授权写进 schema(直观);函数包装更灵活
# 指令式授权示例
type Query {
users: [User!]! @auth(requires: ADMIN)
me: User @auth
}
4.3 授权失败的表现
# 授权失败响应规范
# 1) 401(未认证)vs 403(已认证但无权限)——别混用
# 2) errors[] 里 code: 'UNAUTHENTICATED' / 'FORBIDDEN'
# 3) 敏感字段: 无权限时返回 null 还是抛错?
# 列表字段建议过滤(返回能看到的部分)
# 单条敏感数据建议抛 FORBIDDEN(避免"猜是否存在")
# 4) 别泄露"为什么没权限"(枚举探测风险)
五、字段级权限:GraphQL 的核心差异
5.1 为什么必须字段级
REST 里字段跟随 URL 一起授权;GraphQL 里查询任意组合字段,必须按字段拦截:
# 典型泄露场景
# 查询 { user(id) { name, email, phone, ssn } }
# REST: /users/{id} 返回字段受控
# GraphQL: 若 resolver 直接返回整个 user 对象,
# 客户端可自由选择要 email/ssn → 泄露
# 解法: 敏感字段在 schema 层/field resolver 层单独授权
5.2 字段级授权实现
# 方案一: schema 指令
type User {
id: ID!
name: String!
email: String! @auth(requires: SELF_OR_ADMIN)
ssn: String @auth(requires: ADMIN)
}
// 方案二: 字段 resolver 内分支
const User = {
email: (user, _args, ctx) => {
if (!canRead(ctx, user, "email")) return null;
return user.email;
},
};
工程决策:敏感度高的字段用指令(声明式、schema 可见);复杂规则用字段 resolver(灵活)。
5.3 列表级行过滤
字段级 + 行级组合,处理"列表只返回有权看的部分":
# 例: 项目列表 —— 用户只看得到自己参与的
# resolver 查数据时带上授权过滤(WHERE ...)
# 避免: 先查出全部再内存过滤(性能 + 泄露窗口)
# 数据层授权(RLS 等价物)是最强的防线
六、Federation 与跨子图授权
6.1 子图间的信任边界
Federation 把 schema 拆成多个子图,授权必须考虑"哪个子图负责鉴权":
# 联邦授权模式
# 1) 网关统一认证(解 token)→ 注入到子图
# 2) 字段级授权在各子图内做(数据归属的子图负责行级过滤)
# 3) 跨子图敏感数据: 不通过 @external 暴露,或用 @shareable 时重新授权
# 关键风险: 子图 B 信任了子图 A 传入的实体参数,
# 若 A 未授权就"代查"了 B 的私有数据
6.2 实体解析的授权
Federation 的 _entities 解析是按 key 组装跨子图实体,容易被绕过授权:
# 防护
# 1) 实体解析器也做授权(不只是业务 resolver)
# 2) @requires 引用的跨子图字段,引用方授权后还要引用方重验
# 3) 网关层面禁止未授权方调用实体的私有字段
七、GraphQL 特有的安全攻击面
7.1 复杂查询与深度攻击
GraphQL 允许客户端"点播"任意字段组合,恶意客户端可构造超大查询:
| 攻击 | 危害 | 防护 |
|---|---|---|
| 深度攻击 | 嵌套层数深,递归打爆 | maxDepth 限制 |
| 复杂度攻击 | 字段数×类型复杂度过高 | maxCost 成本计算 |
| 别名滥用 | 同字段起多个别名放大 | 别名计数限制 |
| 批量攻击 | 一次查询大量 ID | 列表大小限制 |
# 成本计算示例(估算每个字段权重)
# Query.getUsers(limit) 成本 = 10 + limit * 20
# 超过 maxCost → 拒绝(409/429)
# 结合持久化查询(persisted queries)白名单最稳
7.2 信息泄露面
# GraphQL 特有泄露点
# 1) introspection: 生产环境必须关闭(schema 即攻击地图)
# 2) 错误信息: 内部堆栈/表名/字段名别回给客户端
# 3) 批量取数: 未授权列表字段被逐条探测(时间侧信道)
# 4) 缓存侧信道: 公共缓存里混入未授权数据
# 统一: 生产关闭 introspection + formatError 脱敏 + 缓存键含授权维度
7.3 速率限制与配额
# 速率限制(网关层)
# 1) 按客户端(token/API key)配额
# 2) 按操作复杂度加权(成本预算,不是"请求数")
# 3) 登录/敏感操作额外限制
# 为什么按成本: 一个复杂查询抵百个简单查询,只按次数限制拦不住
八、授权测试与审计
8.1 授权测试矩阵
# 授权测试要点
# 1) 每个字段: 无 token / 游客 / 普通用户 / 管理员 / 数据归属者 五种身份
# 2) 角色矩阵: 每个角色对每个字段/操作的结果断言
# 3) 边界: 越权参数(他人 id)、批量探测、深度/复杂度超限
# 4) 负向测试: 明确"不该访问的必须被拒"(403),别只测正向
# 工具: 契约测试 + 授权断言夹具,CI 里全量回归
8.2 授权审计与日志
# 审计要求
# 1) 敏感操作(删除、改权限、读 PII)记录: 谁、何时、访问什么
# 2) 授权失败打日志(区分正常 403 与批量扫描)
# 3) 权限变更留痕(角色分配历史)
# 4) 定期权限对账: 僵尸账号、超范围授权清理
# 监控信号: 异常"越权尝试"频率、403 批量触发 → 告警
8.3 常见陷阱
- 只做认证不做授权:能登录就行,字段级全放行——等于裸奔。
- 授权写在客户端:前端隐藏按钮不是授权,API 必须自己校验。
- 把整个 user 对象塞进 data:字段 resolver 不细控,客户端随意点选。
- Federation 信任子图:@external/@requires 字段绕过授权。
- 生产开 introspection:schema 结构直接暴露给攻击者。
Q1: 认证和授权必须分开实现吗?
是的,在架构上分开。认证(解 token 定身份)在 context/网关统一做一次;授权(字段/行级判断)在 resolver 各层反复用。混在一起会让每个 resolver 重复解 token,且职责不清难审计。
Q2: 字段级授权会不会让 resolver 代码变啰嗦?
会,但值得。用 schema 指令(@auth(requires:))或高阶函数包装把规则集中声明,resolver 只写业务逻辑。啰嗦的代码在规则集中后反而更少、更可测。
Q3: 列表字段无权限时应该过滤还是报错?
列表默认过滤(返回有权看的部分),因为"是否存在未授权数据"本身也是敏感信息。单条敏感查询建议抛 FORBIDDEN,避免枚举探测。两者要成约定写进规范。
Q4: Federation 下授权放网关还是子图?
网关统一认证、子图各自授权(数据归属子图负责行级)。关键原则:数据在哪,行级授权就在哪;跨子图字段(@requires/@external)要重新授权,不能因为"网关验过就信任子图参数"。
Q5: 生产环境真的必须关 introspection 吗?
对互联网暴露的 API 强烈建议关闭(schema 即攻击地图)。内部服务可按需保留,但配合网关控制访问。关闭 introspection 不影响正常客户端(他们用已知 schema)。
一句话总结
GraphQL 的认证与授权,核心是从 REST 的"按 URL 授权"转向按字段授权:认证在 context 统一解 token,授权在 resolver 按字段与行级逐层校验,Federation 里"数据在哪行级授权就在哪",再叠加 GraphQL 特有的成本控制与 introspection 收敛——把 schema 从攻击地图变成受控的权限边界。
相关阅读
- GraphQL 安全与防护 — 攻击面与防护清单
- GraphQL 错误处理 — 授权失败的错误契约
- GraphQL Federation — 跨子图授权边界
- GraphQL Schema 设计进阶 — 字段级授权的 schema 设计
- GraphQL 契约测试 — 授权测试的自动化
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。