目录

  1. 1. 前言
  2. 2. 核心概念
    1. 2.1. 与 REST API 的区别
  3. 3. 基础语法
  4. 4. 服务端实现
  5. 5. 前端实现
  6. 6. 实现认证与授权
  7. 7. 内省查询
    1. 7.1. 结构自省
    2. 7.2. 类型名称自省
    3. 7.3. 导入 voyager
  8. 8. 攻击面
    1. 8.1. 信息收集
    2. 8.2. 敏感信息泄露与越权
    3. 8.3. 框架漏洞
    4. 8.4. GraphQL 注入
    5. 8.5. debug 模式信息泄露

LOADING

第一次加载文章图片可能会花费较长时间

要不挂个梯子试试?(x

加载过慢请开启缓存 浏览器默认开启

GraphQL与安全

2026/5/8 Web GraphQL
  |     |   总文章阅读量:

前言

GraphQL 本质上是一种用于 API 的查询语言,以及一个在服务端执行这些查询的运行时环境

参考:

https://graphql.cn/

https://www.anquanke.com/post/id/147455

https://www.leavesongs.com/content/files/slides/%E6%94%BB%E5%87%BBGraphQL.pdf

https://half90.top/2022/07/13/graphql-gong-ji-mian-zong-jie/

https://graphql.nodejs.cn/learn/introspection/


核心概念

GraphQL 不是一种数据库,而是一种用于 API 的强类型查询语言

与 REST API 的区别

假设正在开发一个博客网站,首页需要展示用户的名字与最近 3 篇文章标题

在传统的 REST API 架构中,前端在这里需要访问获取用户信息的接口(假设是 /users/1)和获取用户文章信息的接口(假设是 /users/1/posts),这就导致两个问题:

  • 获取过度:对于获取用户信息,我们只需要知道用户的名字,但是访问 /users/1 会同时返回用户的其他信息,比如 email,phone 等内容
  • 获取不足导致的多次请求:对于获取用户文章信息,访问 /users/1 之后只返回用户信息而没有文章数据,于是还要发起第二个请求另一个接口 /users/1/posts

二者明显会浪费网络带宽,增加页面加载时间

在 GraphQL 中,通常只有一个 api 端点,假设是 /graphql,前端想要什么数据就自行构造请求体发给服务端,对于上面的需求,前端会构造如下请求:

query {
  user(id: 1) {
    name           # 返回名字
    posts(last: 3) { 
      title        # 返回最近3篇文章的标题
    }
  }
}

服务端在收到后,会严格按照要求的结构返回 json 数据

{
  "data": {
    "user": {
      "name": "Ema",
      "posts": [
        { "title": "hello world" },
        { "title": "React 性能优化" },
        { "title": "2026 前端展望" }
      ]
    }
  }
}

这就是 GraphQL 的核心概念:请求你所要的数据,不多不少;获取多个资源,只用一个请求


基础语法

可用于练习的 lab: https://graphql.org/swapi-graphql/

GraphQL SDL(Schema Definition Language,模式定义语言)

依旧以博客系统为例,首先服务端需要定义 Schema 与类型

# 1. 标量类型(基础类型):String, Int, Float, Boolean, ID

# 2. 对象类型:我们定义一个“用户”对象
type User {
  id: ID!          # 感叹号 ! 代表这个字段是必填的(绝对不会返回 null)
  name: String!
  age: Int
  posts: [Post!]!  # 中括号 [] 代表数组。这意味着它返回一个 Post 数组
}

# 3. 再定义一个“文章”对象
type Post {
  id: ID!
  title: String!
  content: String
  views: Int
}

# 4. 根查询类型(Query):暴露给客户端的“查询入口”
type Query {
  # 查询所有用户
  allUsers: [User!]!
  # 根据 id 查询单个用户,需要传入参数 id
  getUserById(id: ID!): User
}

type Mutation {
  # 创建一篇文章,需要标题和内容,返回创建好的文章对象
  createPost(title: String!, content: String): Post!
}

然后前端就可以进行 Query 了,这里的 Query 相当于 REST 中的 GET 请求

query {
  allUsers {
    name
    age
  }
}

然后后端返回

{
  "data": {
    "allUsers": [
      { "name": "Ema", "age": 16 },
      { "name": "Hiro", "age": 16 }
    ]
  }
}

带参数嵌套查询的例子:

query {
  getUserById(id: "101") {   # 括号里传递参数
    name
    posts {                  # 嵌套查询文章信息
      title
      views
    }
  }
}

修改数据使用 Mutation 类型(Mutation 执行后,可以立刻查询新生成的数据):

mutation {
  createPost(title: "GraphQL", content: "hello world") {
    id        # 创建成功后,后端立刻把新生成的 id 返回给我
    title     # 返回文章标题
  }
}

后端返回

{
  "data": {
    "createPost": {
      "id": "999",
      "title": "GraphQL"
    }
  }
}

服务端实现

服务端需要提供 Resolver,在解析 GraphQL 的 Schema 后进行下一步操作获取数据,如数据库查询、调用第三方 API

不同语言的 Resolver 框架实现:

  • Nodejs:Apollo Server
  • Java:Spring for GraphQL 或 graphql-java
  • Python:Graphene 或 Strawberry
  • Golang:gqlgen

这里使用 Apollo Server 实现

package.json

{
  "name": "graphql-lab",
  "version": "1.0.0",
  "type": "module",
  "scripts": {
    "start": "node index.js"
  },
  "dependencies": {
    "@apollo/server": "^4.10.0",
    "graphql": "^16.8.0"
  }
}

index.js

import { ApolloServer } from '@apollo/server';
import { startStandaloneServer } from '@apollo/server/standalone';

// 模拟数据库
let databaseBooks = [
  { id: '1', title: '三体', price: 99.0, author: '刘慈欣' },
  { id: '2', title: '百年孤独', price: 55.5, author: '马尔克斯' },
];

// 1. 定义 Schema
const typeDefs = `#graphql
  type Book {
    id: ID!
    title: String!
    price: Float!
    author: String!
  }

  type Query {
    getTopBooks(limit: Int!): [Book!]!
  }

  type Mutation {
    addBook(title: String!, price: Float!, author: String!): Book!
  }
`;

// 2. 编写 Resolvers
const resolvers = {
  Query: {
    getTopBooks: (_, args) => {
      return databaseBooks.slice(0, args.limit);
    },
  },
  Mutation: {
    addBook: (_, args) => {
      // 从 args 中拿出前端传来的数据,生成新书
      const newBook = {
        id: String(databaseBooks.length + 1), // 简单生成一个ID
        title: args.title,
        price: args.price,
        author: args.author,
      };
      // 存入数据库
      databaseBooks.push(newBook);
      // 必须返回这本新书,因为 Schema 规定了返回值是 Book
      return newBook;
    },
  },
};

// 3. 启动服务器
const server = new ApolloServer({
  typeDefs,
  resolvers,
});

// 在 Docker 中,推荐显式指定 host 为 0.0.0.0
const { url } = await startStandaloneServer(server, {
  listen: { port: 4000, host: '0.0.0.0' },
});

console.log(`🚀 GraphQL Server 准备就绪: ${url}`);

Dockerfile

FROM node:18-alpine

WORKDIR /app

COPY package*.json ./
RUN npm install

COPY . .

EXPOSE 4000

CMD ["npm", "start"]

docker-compose.yml

version: '3.8'

services:
  graphql-server:
    build: .
    container_name: graphql-lab
    ports:
      - "4000:4000"
    volumes:
      - .:/app
      - /app/node_modules


前端实现

原生的做法,直接 fetch 即可

// 1. 定义你的 GraphQL 查询字符串
const myQuery = `
  query GetBooks($limit: Int!) {
    getTopBooks(limit: $limit) {
      title
      price
    }
  }
`;

// 2. 使用原生 fetch 发送 POST 请求
fetch('http://localhost:4000/', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
  },
  // 核心:把 query 和 variables 塞进 body 里传给后端
  body: JSON.stringify({
    query: myQuery,
    variables: { limit: 2 } 
  }),
})
  .then(res => res.json())
  .then(result => console.log('前端拿到的数据:', result.data));


实现认证与授权

在 GraphQL 中,每次收到前端的请求,都会先生成一个 Context 对象。这个对象会被原封不动地传递给所有的 Resolver。

所以实现认证的最佳位置是在生成 Context 阶段

import { ApolloServer } from '@apollo/server';
import { startStandaloneServer } from '@apollo/server/standalone';
import { GraphQLError } from 'graphql';

// 模拟一个验证 Token 的函数 (实际开发中一般用 jsonwebtoken 库的 jwt.verify)
const getUserFromToken = (token) => {
  if (token === 'super-secret-token-admin') {
    return { id: '1', name: 'Admin', role: 'ADMIN' };
  }
  if (token === 'secret-token-user') {
    return { id: '2', name: 'John Doe', role: 'USER' };
  }
  return null;
};

const server = new ApolloServer({
  typeDefs,
  resolvers,
});

const { url } = await startStandaloneServer(server, {
  listen: { port: 4000 },
  // 核心:context 函数会在每一次前端发来请求时执行
  context: async ({ req }) => {
    // 1. 从 HTTP 请求头中拿到 token
    const authHeader = req.headers.authorization || '';
    const token = authHeader.replace('Bearer ', '');

    // 2. 解析 token 获取当前用户
    const user = getUserFromToken(token);

    // 3. 返回的对象就是 GraphQL 的 Context!所有 Resolver 都可以拿到它。
    return { user };
  },
});

然后是授权部分,拿到 user 之后,接下来就是在 Resolvers 中进行权限判断

如果要保护整个 Query 或 Mutation,那就直接在这个操作中添加一个 Context 校验:

const resolvers = {
  Mutation: {
    updateMyName: (parent, args, context) => {
      if (!context.user) {
        throw new GraphQLError('请先登录!', {
          extensions: { code: 'UNAUTHENTICATED' },
        });
      }

      console.log(`把用户 ${context.user.id} 的名字改成了 ${args.newName}`);
      return true;
    },
  },
};

设置只有 ADMIN 才能删除用户:

const resolvers = {
  Mutation: {
    deleteUser: (parent, args, context) => {
      if (!context.user) {
        throw new GraphQLError('未登录', { extensions: { code: 'UNAUTHENTICATED' } });
      }
      
      if (context.user.role !== 'ADMIN') {
        throw new GraphQLError('权限不足,只有管理员可操作', { 
          extensions: { code: 'FORBIDDEN' } 
        });
      }

      return "删除成功";
    },
  },
};

设置字段级的授权,仅用户自身或管理员可以看到邮箱信息:

# Schema 定义
type User {
  id: ID!
  name: String!
  email: String # 邮箱可能是空的(如果没权限看)
}
const resolvers = {
  Query: {
    getAllUsers: () => [
      { id: '1', name: 'Admin', email: 'admin@abc.com' },
      { id: '2', name: 'John Doe', email: 'john@abc.com' }
    ]
  },
  
  // 单独为 User 类型的 email 字段写一个 Resolver
  User: {
    email: (parent, args, context) => {
      // parent 即当前正在解析的这个 User 对象
      if (context.user && (context.user.role === 'ADMIN' || context.user.id === parent.id)) {
        return parent.email;
      }
      return null; 
    }
  }
};

内省查询

GraphQL 允许我们使用 Introspection query 自省查询来了解 GraphQL API 的结构

结构自省

如果你为 GraphQL API 设计了 type,那么此时你应该已经知道可用的类型。但如果你没有设计它,可以通过查询 __schema 字段来询问 GraphQL,该字段在 query 根操作类型上始终可用。

query {
  __schema {
    types {
      name
    }
  }
}

查询结果如下:

{
  "data": {
    "__schema": {
      "types": [
        {
          "name": "Book"
        },
        {
          "name": "ID"
        },
        {
          "name": "String"
        },
        {
          "name": "Float"
        },
        {
          "name": "Query"
        },
        {
          "name": "Int"
        },
        {
          "name": "Mutation"
        },
        {
          "name": "Boolean"
        },
        {
          "name": "__Schema"
        },
        {
          "name": "__Type"
        },
        {
          "name": "__TypeKind"
        },
        {
          "name": "__Field"
        },
        {
          "name": "__InputValue"
        },
        {
          "name": "__EnumValue"
        },
        {
          "name": "__Directive"
        },
        {
          "name": "__DirectiveLocation"
        }
      ]
    }
  }
}
  • 上面自定义过的 type:BookQueryMutation
  • 内置的 type:IDStringFloatIntBoolean
  • 自省系统的 type:__Schema__Type__TypeKind__Field__InputValue__EnumValue__Directive__DirectiveLocation

接下来尝试查询所有查询的起始类型:

query {
  __schema {
    queryType {
      name
    }
  }
}
{
  "data": {
    "__schema": {
      "queryType": {
        "name": "Query"
      }
    }
  }
}

Query 类型是我们起始的地方。注意,这里的命名只是按照惯例;我们可以将 Query 类型命名为其他任何名称,如果我们指定它是查询的起始类型,它仍然会在这里返回。

接下来查询特定类型及其相关信息,这里选择 Book

query {
  __type(name: "Book") {
    name
    kind
  }
}
{
  "data": {
    "__type": {
      "name": "Book",
      "kind": "OBJECT"
    }
  }
}

再查询其中的字段

query {
  __type(name: "Book") {
    name
    fields {
      name
      type {
        name
        kind
      }
    }
  }
}
{
  "data": {
    "__type": {
      "name": "Book",
      "fields": [
        {
          "name": "id",
          "type": {
            "name": null,
            "kind": "NON_NULL"
          }
        },
        {
          "name": "title",
          "type": {
            "name": null,
            "kind": "NON_NULL"
          }
        },
        {
          "name": "price",
          "type": {
            "name": null,
            "kind": "NON_NULL"
          }
        },
        {
          "name": "author",
          "type": {
            "name": null,
            "kind": "NON_NULL"
          }
        }
      ]
    }
  }
}

类型名称自省

使用 __typename 元字段以查找任何具有对象、接口或联合类型作为基础输出类型的字段

query {
  getTopBooks(limit: 100) {
    __typename
    ... on Book {
      title
    }
  }
}


导入 voyager

使用下面这一串查询,可以将查询的结果导入到 https://apis.guru/graphql-voyager/ 来生成可视化 api 文档

query IntrospectionQuery {
  __schema {

    queryType { name kind }
    mutationType { name kind }
    subscriptionType { name kind }
    types {
      ...FullType
    }
    directives {
      name
      description

      locations
      args {
        ...InputValue
      }
    }
  }
}

fragment FullType on __Type {
  kind
  name
  description


  fields(includeDeprecated: true) {
    name
    description
    args {
      ...InputValue
    }
    type {
      ...TypeRef
    }
    isDeprecated
    deprecationReason
  }
  inputFields {
    ...InputValue
  }
  interfaces {
    ...TypeRef
  }
  enumValues(includeDeprecated: true) {
    name
    description
    isDeprecated
    deprecationReason
  }
  possibleTypes {
    ...TypeRef
  }
}

fragment InputValue on __InputValue {
  name
  description
  type { ...TypeRef }
  defaultValue


}

fragment TypeRef on __Type {
  kind
  name
  ofType {
    kind
    name
    ofType {
      kind
      name
      ofType {
        kind
        name
        ofType {
          kind
          name
          ofType {
            kind
            name
            ofType {
              kind
              name
              ofType {
                kind
                name
                ofType {
                  kind
                  name
                  ofType {
                    kind
                    name
                  }
                }
              }
            }
          }
        }
      }
    }
  }
}

攻击面

DVGA 靶场: https://github.com/dolevf/Damn-Vulnerable-GraphQL-Application

信息收集

使用 graphw00f 实现 GraphQL 的检测和指纹识别: https://github.com/dolevf/graphw00f

找到 GraphQL 后对其服务器进行指纹识别来确定底层实现

主要寻找 /graphql 的接口或者 /graphiql 的交互界面

敏感信息泄露与越权

__schema 自省机制列出 GraphQL 中的 Query、Mutation、ObjectType、Field、Argument

在 objects.types 中寻找敏感信息字段,如 email、password、token 等,以及一些废弃的字段

框架漏洞

Express-GraphQL

Graphene-Django

GraphQL 注入

本质是用户输入能够闭合某些东西来进入到代码中,实现 SQL、XSS、命令注入等

debug 模式信息泄露