GraphQL에서 **보안(Security)**는 클라이언트와 서버 간의 통신에서 중요한 역할을 합니다. GraphQL API는 많은 자유도를 제공하기 때문에 인증(Authentication), 권한 관리(Authorization), 쿼리 제한(Query Complexity) 등을 통해 서버를 보호하고 데이터의 무결성을 유지하는 것이 매우 중요합니다. 보안을 적절히 설정하지 않으면 서버가 악의적인 요청에 취약해질 수 있습니다.
GraphQL 보안의 주요 요소는 다음과 같습니다:
- 인증(Authentication): 클라이언트가 누구인지 확인하는 절차.
- 권한 관리(Authorization): 클라이언트가 요청한 데이터를 접근할 권한이 있는지 확인.
- 쿼리 제한(Query Complexity): 매우 복잡하거나 대량의 요청으로 서버 자원이 남용되지 않도록 제한.
- 입력 검증(Input Validation): 잘못된 데이터나 악의적인 입력을 방지.
- 중복된 데이터 요청 방지: 과도한 요청을 처리하지 않도록 설정.
각각의 항목을 자세히 살펴보겠습니다.
1. 인증(Authentication)
인증은 클라이언트가 누구인지 확인하는 과정입니다. GraphQL API는 보통 HTTP 요청을 통해 사용되므로, 기존 REST API에서 사용하는 인증 방식인 JWT (JSON Web Token), OAuth 등을 그대로 사용할 수 있습니다.
JWT 인증 예시:
JWT는 클라이언트가 로그인할 때 서버에서 발급하는 토큰으로, 클라이언트는 이후의 모든 요청에 이 토큰을 HTTP 헤더에 포함하여 인증을 수행합니다.
- JWT 발급 과정:
- 클라이언트가 로그인 정보를 서버에 보내면, 서버는 사용자를 인증한 후 JWT 토큰을 발급하여 클라이언트에 반환합니다.
- JWT를 HTTP 헤더에 포함하여 요청:
- 이후 클라이언트는 이 토큰을 Authorization 헤더에 포함해 서버에 요청을 보냅니다.
# HTTP 요청에 Authorization 헤더 추가
{
"Authorization": "Bearer <JWT_TOKEN>"
}
서버 측 인증 처리:
GraphQL 서버에서는 요청이 들어올 때 HTTP 헤더에서 JWT 토큰을 확인하고, 이를 검증한 후 Context에 사용자 정보를 추가하여 리졸버에서 사용할 수 있게 합니다.
const { ApolloServer } = require('apollo-server');
const jwt = require('jsonwebtoken');
const getUserFromToken = (token) => {
try {
if (token) {
return jwt.verify(token, 'your-secret-key'); // 토큰 검증
}
return null;
} catch (err) {
return null;
}
};
const server = new ApolloServer({
typeDefs,
resolvers,
context: ({ req }) => {
const token = req.headers.authorization || '';
const user = getUserFromToken(token.replace('Bearer ', ''));
return { user };
},
});
위 코드에서는 context
함수에서 HTTP 헤더에서 토큰을 추출하고, 이를 검증한 후 사용자 정보를 리졸버에 전달합니다.
2. 권한 관리(Authorization)
권한 관리는 인증된 클라이언트가 요청한 자원에 접근할 권한이 있는지를 확인하는 과정입니다. GraphQL에서는 리졸버 레벨에서 권한을 체크할 수 있습니다. 특정 사용자 그룹이나 권한을 가진 사용자만 특정 필드에 접근할 수 있도록 제한할 수 있습니다.
권한 관리 예시:
- Role-based Authorization: 사용자에게 역할(관리자, 사용자 등)을 부여하고, 각 역할에 따라 접근할 수 있는 리소스를 제어합니다.
const resolvers = {
Query: {
secretData: (parent, args, context) => {
// 인증되지 않은 사용자에게는 에러 반환
if (!context.user) {
throw new Error('Not authenticated');
}
// 관리자만 접근할 수 있도록 제한
if (context.user.role !== 'admin') {
throw new Error('Not authorized');
}
return "This is secret data!";
},
},
};
위 코드에서 context.user.role
을 통해 사용자의 역할을 확인하고, admin
역할이 아닌 사용자에게는 접근을 차단합니다.
- Field-level Authorization: GraphQL의 강력한 점은 각 필드에서 권한 관리를 세밀하게 할 수 있다는 점입니다. 예를 들어, 특정 필드에만 권한을 적용할 수 있습니다.
const resolvers = {
User: {
email: (parent, args, context) => {
// 사용자 자신 또는 관리자가 아닌 경우, 이메일 정보를 숨김
if (context.user.id !== parent.id && context.user.role !== 'admin') {
return null;
}
return parent.email;
},
},
};
이 예시에서는 사용자 자신의 이메일이거나 관리자인 경우에만 이메일을 반환하고, 그렇지 않으면 null
을 반환하여 접근을 차단합니다.
3. 쿼리 제한(Query Complexity)
GraphQL의 유연한 쿼리 특성상 클라이언트가 너무 복잡한 쿼리나 너무 많은 데이터를 한 번에 요청하면 서버에 과부하를 줄 수 있습니다. 이를 방지하기 위해 **쿼리 복잡성 제한(Query Complexity)**과 **쿼리 깊이 제한(Query Depth Limiting)**을 설정할 수 있습니다.
1) 쿼리 깊이 제한(Query Depth Limiting)
클라이언트가 깊게 중첩된 쿼리를 요청할 때, 서버 자원을 많이 사용할 수 있습니다. 이를 방지하기 위해 쿼리의 최대 깊이를 제한할 수 있습니다.
javascript코드 복사const depthLimit = require('graphql-depth-limit');
const server = new ApolloServer({
typeDefs,
resolvers,
validationRules: [depthLimit(5)] // 최대 5단계 중첩까지 허용
});
위 코드에서는 **depthLimit(5)
**를 사용하여 쿼리의 최대 중첩 깊이를 5로 제한합니다.
2) 쿼리 복잡성(Query Complexity)
쿼리 복잡성은 쿼리의 각 필드에 가중치를 부여하여 클라이언트가 너무 많은 리소스를 요구하지 않도록 제한하는 방식입니다. 예를 들어, 필드 하나를 가져오는 데 많은 자원이 소모된다면 그 필드에 높은 가중치를 부여할 수 있습니다.
const { getComplexity, simpleEstimator } = require('graphql-query-complexity');
const server = new ApolloServer({
typeDefs,
resolvers,
plugins: [
{
requestDidStart: () => ({
didResolveOperation({ request, document }) {
const complexity = getComplexity({
schema,
operationName: request.operationName,
query: document,
variables: request.variables,
estimators: [
simpleEstimator({ defaultComplexity: 1 }),
],
});
if (complexity > 100) {
throw new Error(`Query too complex: ${complexity}. Maximum allowed complexity: 100`);
}
},
}),
},
],
});
이 예시는 쿼리 복잡도가 100을 초과하면 에러를 반환하도록 설정하는 방식입니다. 이를 통해 클라이언트가 서버에 과도한 요청을 보내지 않도록 할 수 있습니다.
4. 입력 검증(Input Validation)
입력 검증은 클라이언트에서 잘못된 데이터나 악의적인 데이터를 서버로 보내지 못하도록 방지하는 보안 기법입니다. GraphQL에서는 스키마를 통해 데이터 타입을 엄격하게 정의할 수 있어 기본적인 타입 검증은 자동으로 이루어집니다.
기본적인 입력 검증:
type Mutation {
createUser(name: String!, email: String!): User
}
위의 스키마는 name
과 email
을 필수 값으로 정의하고 있으며, 입력 값이 없거나 잘못된 타입이 들어오면 GraphQL이 자동으로 에러를 반환합니다.
그러나 추가적인 검증이 필요한 경우, 리졸버 내에서 직접 검증 로직을 추가할 수 있습니다.
커스텀 입력 검증 예시:
const resolvers = {
Mutation: {
createUser: (parent, { name, email }) => {
if (!email.includes('@')) {
throw new Error('Invalid email format');
}
return User.create({ name, email });
},
},
};
위 코드는 이메일 형식을 직접 검증하고, 잘못된 형식이면 에러를 반환합니다.
5. 중복된 데이터 요청 방지 (Rate Limiting)
GraphQL API는 자유도가 높기 때문에 클라이언트가 여러 번 동일한 요청을 보내거나, 매우 빠르게 연속된 요청을 보낼 수 있습니다. 이를 방지하기 위해 Rate Limiting을 적용하여 일정 시간 내에 요청할 수 있는 횟수를 제한할 수 있습니다.
Rate Limiting 예시 (express-rate-limit 사용):
const rateLimit = require('express-rate-limit');
const { ApolloServer } = require('apollo-server-express');
const limiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15분 동안
max: 100, // 최대 100개의 요청 허용
message: "Too many requests, please try again later.",
});
const app = require('express')();
app.use(limiter);
const server = new ApolloServer({
typeDefs,
resolvers
});
server.applyMiddleware({ app });
이 코드는 15분 동안 100개의 요청만 허용하는 방식으로, 그 이상 요청이 들어오면 에러 메시지를 반환합니다. 이를 통해 서버 자원 남용을 방지할 수 있습니다.
결론
GraphQL API의 보안을 강화하기 위해서는 다음과 같은 요소를 고려해야 합니다:
- 인증(Authentication): JWT 등을 사용해 클라이언트를 인증.
- 권한 관리(Authorization): 각 리졸버에서 사용자의 역할에 따라 접근을 제한.
- 쿼리 제한(Query Complexity, Query Depth): 클라이언트가 과도한 데이터를 요청하지 않도록 쿼리의 복잡성이나 깊이를 제한.
- 입력 검증(Input Validation): 잘못된 데이터가 서버에 전달되지 않도록 철저히 검증.
- Rate Limiting: 동일한 클라이언트가 너무 많은 요청을 보내지 못하도록 제한.