GraphQL 安全有哪些最佳实践
GraphQL 安全最佳实践GraphQL 的灵活性虽然强大,但也带来了独特的安全挑战。以下是保护 GraphQL API 的关键安全措施。1. 认证与授权认证机制const { 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 }; } }});授权中间件const { 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. 查询深度限制防止深度嵌套攻击const depthLimit = require('graphql-depth-limit');const server = new ApolloServer({ typeDefs, resolvers, validationRules: [ depthLimit(7, { ignore: ['ignoredField'] // 忽略特定字段 }) ]});为什么需要:防止恶意客户端发送深度嵌套查询避免服务器资源耗尽防止 DoS 攻击3. 查询复杂度限制限制查询复杂度const { 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]});自定义复杂度计算const 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-limitconst rateLimit = require('express-rate-limit');const limiter = rateLimit({ windowMs: 15 * 60 * 1000, // 15 分钟 max: 100, // 限制每个 IP 100 个请求 message: '请求过于频繁,请稍后再试'});app.use('/graphql', limiter);GraphQL 特定的速率限制const { GraphQLComplexityLimit } = require('graphql-complexity-limit');const server = new ApolloServer({ typeDefs, resolvers, validationRules: [ new GraphQLComplexityLimit({ maxComplexity: 1000, onExceed: (complexity) => { throw new Error(`查询复杂度 ${complexity} 超过限制`); } }) ]});5. 输入验证使用 Yup 验证const 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 验证input CreateUserInput { name: String! @constraint(minLength: 2, maxLength: 100) email: String! @constraint(format: "email") age: Int @constraint(min: 0, max: 150)}6. 字段级权限控制使用指令directive @auth(requires: Role) on FIELD_DEFINITIONenum Role { USER ADMIN}type User { id: ID! name: String! email: String! @auth(requires: ADMIN) salary: Float @auth(requires: ADMIN)}const 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. 防止查询注入参数化查询// 不好的做法 - 字符串拼接const query = `SELECT * FROM users WHERE id = '${userId}'`;// 好的做法 - 参数化查询const query = 'SELECT * FROM users WHERE id = ?';const result = await db.query(query, [userId]);使用 ORM// 使用 Sequelize ORMconst user = await User.findOne({ where: { id: userId }});8. CORS 配置const server = new ApolloServer({ typeDefs, resolvers, cors: { origin: ['https://yourdomain.com'], // 只允许特定域名 credentials: true, methods: ['POST'] }});9. 错误处理不暴露敏感信息const server = new ApolloServer({ typeDefs, resolvers, formatError: (error) => { // 生产环境不暴露详细错误信息 if (process.env.NODE_ENV === 'production') { return new Error('服务器内部错误'); } // 开发环境返回完整错误信息 return error; }});自定义错误类型type Error { code: String! message: String! field: String}type UserResult { user: User errors: [Error!]!}type Mutation { createUser(input: CreateUserInput!): UserResult!}10. 日志与监控记录查询日志const server = new ApolloServer({ typeDefs, resolvers, plugins: [ { requestDidStart: () => ({ didResolveOperation: (context) => { console.log('查询:', context.request.operationName); console.log('变量:', JSON.stringify(context.request.variables)); } }) } ]});监控异常查询const server = new ApolloServer({ typeDefs, resolvers, plugins: [ { requestDidStart: () => ({ didEncounterErrors: (context) => { context.errors.forEach(error => { console.error('查询错误:', error.message); // 发送到监控系统 monitoringService.logError(error); }); } }) } ]});11. 查询白名单使用 persisted queriesconst { 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 注入 | 参数化查询、输入验证 || 未授权访问 | 访问未授权的数据 | 认证、授权、字段级权限控制 || 信息泄露 | 错误信息暴露敏感数据 | 正确的错误处理 |