GraphQL 安全最佳实践
GraphQL 的灵活性虽然强大,但也带来了独特的安全挑战。以下是保护 GraphQL API 的关键安全措施。
1. 认证与授权
认证机制
javascriptconst { ApolloServer } = require('apollo-server'); const jwt = require('jsonwebtoken'); const server = new ApolloServer({ typeDefs, resolvers, context: ({ req }) => { // 从请求头获取 token const token = req.headers.authorization || ''; try { // 验证 token const decoded = jwt.verify(token, process.env.JWT_SECRET); return { user: decoded }; } catch (error) { return { user: null }; } } });
授权中间件
javascriptconst { AuthenticationError, ForbiddenError } = require('apollo-server-express'); const resolvers = { Query: { me: (parent, args, context) => { if (!context.user) { throw new AuthenticationError('未认证'); } return context.user; }, users: (parent, args, context) => { if (!context.user || context.user.role !== 'ADMIN') { throw new ForbiddenError('需要管理员权限'); } return User.findAll(); } } };
2. 查询深度限制
防止深度嵌套攻击
javascriptconst depthLimit = require('graphql-depth-limit'); const server = new ApolloServer({ typeDefs, resolvers, validationRules: [ depthLimit(7, { ignore: ['ignoredField'] // 忽略特定字段 }) ] });
为什么需要:
- 防止恶意客户端发送深度嵌套查询
- 避免服务器资源耗尽
- 防止 DoS 攻击
3. 查询复杂度限制
限制查询复杂度
javascriptconst { createComplexityLimitRule } = require('graphql-validation-complexity'); const complexityLimitRule = createComplexityLimitRule(1000, { onCost: (cost) => console.log(`查询复杂度: ${cost}`), createError: (max, actual) => { return new Error(`查询复杂度 ${actual} 超过最大限制 ${max}`); } }); const server = new ApolloServer({ typeDefs, resolvers, validationRules: [complexityLimitRule] });
自定义复杂度计算
javascriptconst typeDefs = ` type Query { user(id: ID!): User @cost(complexity: 1) users(limit: Int!): [User!]! @cost(complexity: 5, multipliers: ["limit"]) posts(first: Int!): [Post!]! @cost(complexity: 10, multipliers: ["first"]) } `;
4. 速率限制
使用 express-rate-limit
javascriptconst rateLimit = require('express-rate-limit'); const limiter = rateLimit({ windowMs: 15 * 60 * 1000, // 15 分钟 max: 100, // 限制每个 IP 100 个请求 message: '请求过于频繁,请稍后再试' }); app.use('/graphql', limiter);
GraphQL 特定的速率限制
javascriptconst { GraphQLComplexityLimit } = require('graphql-complexity-limit'); const server = new ApolloServer({ typeDefs, resolvers, validationRules: [ new GraphQLComplexityLimit({ maxComplexity: 1000, onExceed: (complexity) => { throw new Error(`查询复杂度 ${complexity} 超过限制`); } }) ] });
5. 输入验证
使用 Yup 验证
javascriptconst yup = require('yup'); const createUserSchema = yup.object().shape({ name: yup.string().required().min(2).max(100), email: yup.string().email().required(), age: yup.number().min(0).max(150).optional() }); const resolvers = { Mutation: { createUser: async (_, { input }) => { // 验证输入 await createUserSchema.validate(input); // 创建用户 return User.create(input); } } };
GraphQL Schema 验证
graphqlinput CreateUserInput { name: String! @constraint(minLength: 2, maxLength: 100) email: String! @constraint(format: "email") age: Int @constraint(min: 0, max: 150) }
6. 字段级权限控制
使用指令
graphqldirective @auth(requires: Role) on FIELD_DEFINITION enum Role { USER ADMIN } type User { id: ID! name: String! email: String! @auth(requires: ADMIN) salary: Float @auth(requires: ADMIN) }
javascriptconst resolvers = { User: { email: (user, args, context) => { if (context.user?.role !== 'ADMIN') { throw new ForbiddenError('无权访问该字段'); } return user.email; }, salary: (user, args, context) => { if (context.user?.role !== 'ADMIN') { throw new ForbiddenError('无权访问该字段'); } return user.salary; } } };
7. 防止查询注入
参数化查询
javascript// 不好的做法 - 字符串拼接 const query = `SELECT * FROM users WHERE id = '${userId}'`; // 好的做法 - 参数化查询 const query = 'SELECT * FROM users WHERE id = ?'; const result = await db.query(query, [userId]);
使用 ORM
javascript// 使用 Sequelize ORM const user = await User.findOne({ where: { id: userId } });
8. CORS 配置
javascriptconst server = new ApolloServer({ typeDefs, resolvers, cors: { origin: ['https://yourdomain.com'], // 只允许特定域名 credentials: true, methods: ['POST'] } });
9. 错误处理
不暴露敏感信息
javascriptconst server = new ApolloServer({ typeDefs, resolvers, formatError: (error) => { // 生产环境不暴露详细错误信息 if (process.env.NODE_ENV === 'production') { return new Error('服务器内部错误'); } // 开发环境返回完整错误信息 return error; } });
自定义错误类型
graphqltype Error { code: String! message: String! field: String } type UserResult { user: User errors: [Error!]! } type Mutation { createUser(input: CreateUserInput!): UserResult! }
10. 日志与监控
记录查询日志
javascriptconst server = new ApolloServer({ typeDefs, resolvers, plugins: [ { requestDidStart: () => ({ didResolveOperation: (context) => { console.log('查询:', context.request.operationName); console.log('变量:', JSON.stringify(context.request.variables)); } }) } ] });
监控异常查询
javascriptconst server = new ApolloServer({ typeDefs, resolvers, plugins: [ { requestDidStart: () => ({ didEncounterErrors: (context) => { context.errors.forEach(error => { console.error('查询错误:', error.message); // 发送到监控系统 monitoringService.logError(error); }); } }) } ] });
11. 查询白名单
使用 persisted queries
javascriptconst { createPersistedQueryLink } = require('@apollo/client/link/persisted-queries'); const { sha256 } = require('crypto-hash'); const link = createPersistedQueryLink({ sha256, useGETForHashedQueries: true }); // 只允许预定义的查询 const allowedQueries = new Set([ 'hash1', 'hash2' }); const server = new ApolloServer({ typeDefs, resolvers, persistedQueries: { cache: new Map(), ttl: 3600 } });
12. 安全检查清单
- 实施认证机制(JWT、OAuth)
- 实施授权机制(基于角色的访问控制)
- 限制查询深度
- 限制查询复杂度
- 实施速率限制
- 验证所有输入
- 实施字段级权限控制
- 防止查询注入
- 配置 CORS
- 正确处理错误
- 记录查询日志
- 监控异常查询
- 使用查询白名单
- 定期安全审计
- 保持依赖更新
13. 常见安全威胁及防护
| 威胁 | 描述 | 防护措施 |
|---|---|---|
| 深度嵌套攻击 | 恶意客户端发送深度嵌套查询 | 限制查询深度 |
| 复杂度攻击 | 发送高复杂度查询消耗资源 | 限制查询复杂度 |
| DoS 攻击 | 大量请求导致服务不可用 | 速率限制、查询白名单 |
| 注入攻击 | 恶意输入导致 SQL 注入 | 参数化查询、输入验证 |
| 未授权访问 | 访问未授权的数据 | 认证、授权、字段级权限控制 |
| 信息泄露 | 错误信息暴露敏感数据 | 正确的错误处理 |