graphql-security
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseGraphQL Security Skill
GraphQL安全技能
Protect your GraphQL APIs from attacks
保护你的GraphQL API免受攻击
Overview
概述
Learn essential security patterns for GraphQL: JWT authentication, role-based authorization, rate limiting, query complexity limits, and input validation.
学习GraphQL的核心安全模式:JWT身份验证、基于角色的授权、速率限制、查询复杂度限制以及输入验证。
Security Checklist
安全检查清单
| Check | Priority | Implementation |
|---|---|---|
| Authentication | Critical | JWT with refresh tokens |
| Authorization | Critical | Field-level with graphql-shield |
| Rate Limiting | Critical | Per-user/IP with Redis |
| Query Depth | High | graphql-depth-limit |
| Query Complexity | High | graphql-query-complexity |
| Introspection | High | Disable in production |
| Input Validation | High | Validate all inputs |
| Error Masking | Medium | Hide internal errors |
| 检查项 | 优先级 | 实现方案 |
|---|---|---|
| 身份验证 | 关键 | 带刷新令牌的JWT |
| 授权 | 关键 | 使用graphql-shield实现字段级控制 |
| 速率限制 | 关键 | 基于用户/IP结合Redis实现 |
| 查询深度限制 | 高 | 使用graphql-depth-limit |
| 查询复杂度限制 | 高 | 使用graphql-query-complexity |
| 自省查询 | 高 | 生产环境中禁用 |
| 输入验证 | 高 | 验证所有输入内容 |
| 错误掩码 | 中 | 隐藏内部错误信息 |
Core Patterns
核心模式
1. JWT Authentication
1. JWT身份验证
typescript
import jwt from 'jsonwebtoken';
// Token creation
function createTokens(user) {
const accessToken = jwt.sign(
{ userId: user.id, roles: user.roles },
process.env.JWT_SECRET,
{ expiresIn: '15m' }
);
const refreshToken = jwt.sign(
{ userId: user.id },
process.env.JWT_REFRESH_SECRET,
{ expiresIn: '7d' }
);
return { accessToken, refreshToken };
}
// Context setup
const context = async ({ req }) => {
const token = req.headers.authorization?.replace('Bearer ', '');
let user = null;
if (token) {
try {
const payload = jwt.verify(token, process.env.JWT_SECRET);
user = await db.users.findById(payload.userId);
} catch (e) {
// Token invalid or expired
}
}
return { user };
};
// Login resolver
const resolvers = {
Mutation: {
login: async (_, { email, password }) => {
const user = await db.users.findByEmail(email);
if (!user || !await bcrypt.compare(password, user.passwordHash)) {
throw new GraphQLError('Invalid credentials', {
extensions: { code: 'UNAUTHORIZED' }
});
}
return { ...createTokens(user), user };
},
},
};typescript
import jwt from 'jsonwebtoken';
// Token creation
function createTokens(user) {
const accessToken = jwt.sign(
{ userId: user.id, roles: user.roles },
process.env.JWT_SECRET,
{ expiresIn: '15m' }
);
const refreshToken = jwt.sign(
{ userId: user.id },
process.env.JWT_REFRESH_SECRET,
{ expiresIn: '7d' }
);
return { accessToken, refreshToken };
}
// Context setup
const context = async ({ req }) => {
const token = req.headers.authorization?.replace('Bearer ', '');
let user = null;
if (token) {
try {
const payload = jwt.verify(token, process.env.JWT_SECRET);
user = await db.users.findById(payload.userId);
} catch (e) {
// Token invalid or expired
}
}
return { user };
};
// Login resolver
const resolvers = {
Mutation: {
login: async (_, { email, password }) => {
const user = await db.users.findByEmail(email);
if (!user || !await bcrypt.compare(password, user.passwordHash)) {
throw new GraphQLError('Invalid credentials', {
extensions: { code: 'UNAUTHORIZED' }
});
}
return { ...createTokens(user), user };
},
},
};2. Authorization with graphql-shield
2. 使用graphql-shield实现授权
typescript
import { rule, shield, and, or } from 'graphql-shield';
// Rules
const isAuthenticated = rule()((_, __, { user }) => user !== null);
const isAdmin = rule()((_, __, { user }) =>
user?.roles?.includes('ADMIN')
);
const isOwner = rule()(async (_, { id }, { user, dataSources }) => {
const resource = await dataSources.findById(id);
return resource?.userId === user?.id;
});
// Permissions
const permissions = shield({
Query: {
me: isAuthenticated,
users: and(isAuthenticated, isAdmin),
user: and(isAuthenticated, or(isOwner, isAdmin)),
},
Mutation: {
updateUser: and(isAuthenticated, or(isOwner, isAdmin)),
deleteUser: and(isAuthenticated, isAdmin),
},
User: {
email: or(isOwner, isAdmin),
privateField: isOwner,
},
}, {
fallbackError: new GraphQLError('Not authorized'),
});
// Apply
import { applyMiddleware } from 'graphql-middleware';
const protectedSchema = applyMiddleware(schema, permissions);typescript
import { rule, shield, and, or } from 'graphql-shield';
// Rules
const isAuthenticated = rule()((_, __, { user }) => user !== null);
const isAdmin = rule()((_, __, { user }) =>
user?.roles?.includes('ADMIN')
);
const isOwner = rule()(async (_, { id }, { user, dataSources }) => {
const resource = await dataSources.findById(id);
return resource?.userId === user?.id;
});
// Permissions
const permissions = shield({
Query: {
me: isAuthenticated,
users: and(isAuthenticated, isAdmin),
user: and(isAuthenticated, or(isOwner, isAdmin)),
},
Mutation: {
updateUser: and(isAuthenticated, or(isOwner, isAdmin)),
deleteUser: and(isAuthenticated, isAdmin),
},
User: {
email: or(isOwner, isAdmin),
privateField: isOwner,
},
}, {
fallbackError: new GraphQLError('Not authorized'),
});
// Apply
import { applyMiddleware } from 'graphql-middleware';
const protectedSchema = applyMiddleware(schema, permissions);3. Rate Limiting
3. 速率限制
typescript
// Express-level (basic)
import rateLimit from 'express-rate-limit';
app.use('/graphql', rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 100,
keyGenerator: (req) => req.user?.id || req.ip,
}));
// GraphQL-level (granular)
const typeDefs = gql`
directive @rateLimit(max: Int!, window: String!) on FIELD_DEFINITION
type Mutation {
login(email: String!, password: String!): AuthPayload!
@rateLimit(max: 5, window: "15m")
sendEmail(input: SendEmailInput!): Boolean!
@rateLimit(max: 10, window: "1h")
}
`;typescript
// Express-level (basic)
import rateLimit from 'express-rate-limit';
app.use('/graphql', rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 100,
keyGenerator: (req) => req.user?.id || req.ip,
}));
// GraphQL-level (granular)
const typeDefs = gql`
directive @rateLimit(max: Int!, window: String!) on FIELD_DEFINITION
type Mutation {
login(email: String!, password: String!): AuthPayload!
@rateLimit(max: 5, window: "15m")
sendEmail(input: SendEmailInput!): Boolean!
@rateLimit(max: 10, window: "1h")
}
`;4. Query Limits
4. 查询限制
typescript
import depthLimit from 'graphql-depth-limit';
import { createComplexityLimitRule } from 'graphql-validation-complexity';
const server = new ApolloServer({
typeDefs,
resolvers,
validationRules: [
// Max depth of 10
depthLimit(10),
// Max complexity of 1000
createComplexityLimitRule(1000, {
scalarCost: 1,
objectCost: 2,
listFactor: 10,
}),
],
// Disable introspection in production
introspection: process.env.NODE_ENV !== 'production',
});typescript
import depthLimit from 'graphql-depth-limit';
import { createComplexityLimitRule } from 'graphql-validation-complexity';
const server = new ApolloServer({
typeDefs,
resolvers,
validationRules: [
// Max depth of 10
depthLimit(10),
// Max complexity of 1000
createComplexityLimitRule(1000, {
scalarCost: 1,
objectCost: 2,
listFactor: 10,
}),
],
// Disable introspection in production
introspection: process.env.NODE_ENV !== 'production',
});5. Input Validation
5. 输入验证
typescript
import validator from 'validator';
import xss from 'xss';
const validate = {
email: (v) => {
if (!validator.isEmail(v)) throw new Error('Invalid email');
return validator.normalizeEmail(v);
},
password: (v) => {
if (v.length < 8) throw new Error('Password too short');
if (!/[A-Z]/.test(v)) throw new Error('Need uppercase');
if (!/[0-9]/.test(v)) throw new Error('Need number');
return v;
},
html: (v) => xss(v),
};
const resolvers = {
Mutation: {
createUser: async (_, { input }) => {
const clean = {
email: validate.email(input.email),
password: validate.password(input.password),
bio: input.bio ? validate.html(input.bio) : null,
};
return db.users.create(clean);
},
},
};typescript
import validator from 'validator';
import xss from 'xss';
const validate = {
email: (v) => {
if (!validator.isEmail(v)) throw new Error('Invalid email');
return validator.normalizeEmail(v);
},
password: (v) => {
if (v.length < 8) throw new Error('Password too short');
if (!/[A-Z]/.test(v)) throw new Error('Need uppercase');
if (!/[0-9]/.test(v)) throw new Error('Need number');
return v;
},
html: (v) => xss(v),
};
const resolvers = {
Mutation: {
createUser: async (_, { input }) => {
const clean = {
email: validate.email(input.email),
password: validate.password(input.password),
bio: input.bio ? validate.html(input.bio) : null,
};
return db.users.create(clean);
},
},
};6. Error Masking
6. 错误掩码
typescript
const server = new ApolloServer({
formatError: (error) => {
// Log full error
console.error(error);
// In production, hide internal errors
if (process.env.NODE_ENV === 'production') {
if (error.extensions?.code === 'INTERNAL_SERVER_ERROR') {
return { message: 'Internal error', extensions: { code: 'INTERNAL_ERROR' } };
}
}
return error;
},
});typescript
const server = new ApolloServer({
formatError: (error) => {
// Log full error
console.error(error);
// In production, hide internal errors
if (process.env.NODE_ENV === 'production') {
if (error.extensions?.code === 'INTERNAL_SERVER_ERROR') {
return { message: 'Internal error', extensions: { code: 'INTERNAL_ERROR' } };
}
}
return error;
},
});Security Headers
安全头部配置
typescript
import helmet from 'helmet';
import cors from 'cors';
app.use(helmet());
app.use(cors({
origin: process.env.ALLOWED_ORIGINS?.split(','),
credentials: true,
}));
app.use(express.json({ limit: '100kb' }));typescript
import helmet from 'helmet';
import cors from 'cors';
app.use(helmet());
app.use(cors({
origin: process.env.ALLOWED_ORIGINS?.split(','),
credentials: true,
}));
app.use(express.json({ limit: '100kb' }));Troubleshooting
故障排查
| Issue | Cause | Solution |
|---|---|---|
| Token always invalid | Clock skew | Add grace period |
| Rate limit bypass | Wrong key | Use user ID when authenticated |
| Auth not working | Context async | Await context setup |
| Introspection exposed | Wrong env check | Verify NODE_ENV |
| 问题 | 原因 | 解决方案 |
|---|---|---|
| 令牌始终无效 | 时钟偏差 | 添加宽限期 |
| 速率限制被绕过 | 键值错误 | 已认证时使用用户ID作为标识 |
| 身份验证不生效 | 上下文未异步处理 | 等待上下文初始化完成 |
| 自省查询暴露 | 环境变量检查错误 | 验证NODE_ENV配置 |
Security Testing
安全测试
bash
undefinedbash
undefinedTest introspection (should fail in prod)
Test introspection (should fail in prod)
curl -X POST $API
-H "Content-Type: application/json"
-d '{"query":"{ __schema { types { name } } }"}'
-H "Content-Type: application/json"
-d '{"query":"{ __schema { types { name } } }"}'
curl -X POST $API
-H "Content-Type: application/json"
-d '{"query":"{ __schema { types { name } } }"}'
-H "Content-Type: application/json"
-d '{"query":"{ __schema { types { name } } }"}'
Test rate limit
Test rate limit
for i in {1..20}; do
curl -X POST $API
-d '{"query":"mutation { login(email:"x",password:"y") { token } }"}' done
-d '{"query":"mutation { login(email:"x",password:"y") { token } }"}' done
for i in {1..20}; do
curl -X POST $API
-d '{"query":"mutation { login(email:"x",password:"y") { token } }"}' done
-d '{"query":"mutation { login(email:"x",password:"y") { token } }"}' done
Test depth limit (should fail)
Test depth limit (should fail)
curl -X POST $API
-d '{"query":"{ user { posts { author { posts { author { id } } } } } }"}'
-d '{"query":"{ user { posts { author { posts { author { id } } } } } }"}'
---curl -X POST $API
-d '{"query":"{ user { posts { author { posts { author { id } } } } } }"}'
-d '{"query":"{ user { posts { author { posts { author { id } } } } } }"}'
---Usage
使用方法
Skill("graphql-security")Skill("graphql-security")Related Skills
相关技能
- - Server configuration
graphql-apollo-server - - Auth in resolvers
graphql-resolvers - - Auth-aware schema
graphql-schema-design
- - 服务器配置
graphql-apollo-server - - 解析器中的身份验证
graphql-resolvers - - 支持身份验证的Schema设计
graphql-schema-design
Related Agent
相关Agent
- - For detailed guidance
06-graphql-security
- - 提供详细指导
06-graphql-security