작성일 댓글 남기기

GraphQL 보안

GraphQL에서 **보안(Security)**는 클라이언트와 서버 간의 통신에서 중요한 역할을 합니다. GraphQL API는 많은 자유도를 제공하기 때문에 인증(Authentication), 권한 관리(Authorization), 쿼리 제한(Query Complexity) 등을 통해 서버를 보호하고 데이터의 무결성을 유지하는 것이 매우 중요합니다. 보안을 적절히 설정하지 않으면 서버가 악의적인 요청에 취약해질 수 있습니다.

GraphQL 보안의 주요 요소는 다음과 같습니다:

  1. 인증(Authentication): 클라이언트가 누구인지 확인하는 절차.
  2. 권한 관리(Authorization): 클라이언트가 요청한 데이터를 접근할 권한이 있는지 확인.
  3. 쿼리 제한(Query Complexity): 매우 복잡하거나 대량의 요청으로 서버 자원이 남용되지 않도록 제한.
  4. 입력 검증(Input Validation): 잘못된 데이터나 악의적인 입력을 방지.
  5. 중복된 데이터 요청 방지: 과도한 요청을 처리하지 않도록 설정.

각각의 항목을 자세히 살펴보겠습니다.


1. 인증(Authentication)

인증은 클라이언트가 누구인지 확인하는 과정입니다. GraphQL API는 보통 HTTP 요청을 통해 사용되므로, 기존 REST API에서 사용하는 인증 방식인 JWT (JSON Web Token), OAuth 등을 그대로 사용할 수 있습니다.

JWT 인증 예시:

JWT는 클라이언트가 로그인할 때 서버에서 발급하는 토큰으로, 클라이언트는 이후의 모든 요청에 이 토큰을 HTTP 헤더에 포함하여 인증을 수행합니다.

  1. JWT 발급 과정:
    • 클라이언트가 로그인 정보를 서버에 보내면, 서버는 사용자를 인증한 후 JWT 토큰을 발급하여 클라이언트에 반환합니다.
  2. 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에서는 리졸버 레벨에서 권한을 체크할 수 있습니다. 특정 사용자 그룹이나 권한을 가진 사용자만 특정 필드에 접근할 수 있도록 제한할 수 있습니다.

권한 관리 예시:

  1. 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 역할이 아닌 사용자에게는 접근을 차단합니다.

  1. 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
}

위의 스키마는 nameemail을 필수 값으로 정의하고 있으며, 입력 값이 없거나 잘못된 타입이 들어오면 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의 보안을 강화하기 위해서는 다음과 같은 요소를 고려해야 합니다:

  1. 인증(Authentication): JWT 등을 사용해 클라이언트를 인증.
  2. 권한 관리(Authorization): 각 리졸버에서 사용자의 역할에 따라 접근을 제한.
  3. 쿼리 제한(Query Complexity, Query Depth): 클라이언트가 과도한 데이터를 요청하지 않도록 쿼리의 복잡성이나 깊이를 제한.
  4. 입력 검증(Input Validation): 잘못된 데이터가 서버에 전달되지 않도록 철저히 검증.
  5. Rate Limiting: 동일한 클라이언트가 너무 많은 요청을 보내지 못하도록 제한.
작성일 댓글 남기기

GraphQL의 Federation에 대해서

GraphQL Federation은 여러 개의 마이크로서비스에서 제공하는 여러 개의 GraphQL API를 하나의 통합된 그래프처럼 사용할 수 있게 해주는 아키텍처 패턴입니다. 이를 통해 대규모 애플리케이션에서 서비스 간 분리데이터 통합을 유연하게 처리할 수 있습니다. Apollo Federation이 대표적인 도구이며, 주로 대규모 마이크로서비스 아키텍처에서 GraphQL API를 통합할 때 사용됩니다.

1. Federation이 필요한 이유

전통적인 GraphQL 서버에서는 하나의 서버에서 모든 데이터를 제공하는 방식이 일반적입니다. 하지만 대규모 시스템에서는 여러 서비스가 각각의 데이터 소스를 가지고 있고, 이러한 데이터를 하나의 GraphQL API로 통합해서 사용해야 하는 경우가 많습니다.

Federation은 이러한 문제를 해결하기 위해 설계되었습니다. Federation을 사용하면 각각의 **서브서비스(Subgraph)**가 독립적으로 자신의 데이터를 제공하면서도, Gateway를 통해 모든 데이터를 하나의 통합된 그래프처럼 사용할 수 있습니다.

Federation을 사용하는 이유:

  • 마이크로서비스 아키텍처 지원: 각 서비스는 독립적으로 개발 및 배포되며, GraphQL API도 각 서비스에 맞게 분리되어 있습니다.
  • 데이터 통합: Federation은 여러 서비스에서 제공하는 데이터를 한 곳에서 모아 사용할 수 있게 해줍니다.
  • 확장성: 서비스를 분리하고 독립적으로 확장할 수 있습니다. 서비스 간 의존성이 낮아지고, 각 서비스가 독립적으로 운영됩니다.

2. Federation의 구성 요소

Federation을 구성하는 핵심 요소는 두 가지입니다:

  1. Subgraph (서브그래프): 각 서비스가 자체적으로 제공하는 독립적인 GraphQL API입니다. 각 서비스는 자체적으로 스키마를 정의하고, 해당 데이터를 처리합니다.
  2. Apollo Gateway (게이트웨이): 여러 개의 서브그래프를 하나의 통합된 그래프로 결합하는 역할을 합니다. 클라이언트는 이 게이트웨이를 통해 각각의 서브그래프의 데이터를 요청할 수 있습니다.

그림으로 보면 다음과 같은 구조를 가집니다:

           클라이언트
|
[Apollo Gateway]
/ | \
[Subgraph 1] [Subgraph 2] [Subgraph 3]

게이트웨이가 각 서브그래프에 쿼리를 전달하고, 결과를 클라이언트에 반환하는 방식입니다.


3. Federation의 핵심 기능

1) Key and Extend (키와 확장)

Federation의 핵심 기능 중 하나는 확장성입니다. **@key**와 @extend 디렉티브를 사용해 각 서브그래프의 스키마를 결합할 수 있습니다.

  • @key: 각 서브그래프에서 엔터티를 식별하는 필드를 정의합니다.
  • @extend: 다른 서브그래프에서 정의한 엔터티를 확장할 때 사용합니다.

예시:

User 데이터를 사용자 서비스에서 정의하고, 주문 서비스에서 사용자의 주문 데이터를 확장하려고 할 때, 각 서브그래프는 다음과 같이 정의됩니다.

  1. User 서비스의 Subgraph:
type User @key(fields: "id") {
id: ID!
name: String
email: String
}
  • 여기서 @key(fields: "id")User 타입의 id 필드가 사용자 엔터티를 식별하는 고유 값임을 나타냅니다.
  1. Order 서비스의 Subgraph:
extend type User @key(fields: "id") {
id: ID! @external
orders: [Order]
}

type Order {
id: ID!
product: String
price: Float
}
  • 여기서 **@extend**는 User 타입을 확장하고, @key를 통해 id로 사용자 엔터티를 식별합니다. **@external**은 id 필드가 다른 서브그래프(User 서비스)에서 제공된다는 것을 의미합니다.

2) Entities와 Reference Resolver

게이트웨이가 여러 서브그래프 간에 데이터를 결합할 때, Federation은 entitiesreference resolver를 사용합니다.

  • Entity: 각 서브그래프에서 @key로 정의된 엔터티는 게이트웨이를 통해 통합된 그래프에서 공유됩니다.
  • Reference Resolver: @key로 식별된 엔터티를 다른 서브그래프에서 참조할 수 있도록 리졸버를 제공합니다.

4. Apollo Federation 설정

다음은 Apollo Federation을 실제로 설정하는 과정입니다.

1) 각 서비스의 Subgraph 설정

먼저, 각각의 서브그래프(Subgraph)를 설정해야 합니다. 각 서브그래프는 독립적인 GraphQL 서버로 동작하며, 서로 다른 데이터를 제공합니다.

예시: User 서비스
const { ApolloServer, gql } = require('apollo-server');
const { buildFederatedSchema } = require('@apollo/federation');

const typeDefs = gql`
type User @key(fields: "id") {
id: ID!
name: String
email: String
}

type Query {
users: [User]
}
`;

const resolvers = {
Query: {
users() {
return [
{ id: '1', name: 'John Doe', email: '[email protected]' },
{ id: '2', name: 'Jane Doe', email: '[email protected]' }
];
}
}
};

const server = new ApolloServer({
schema: buildFederatedSchema([{ typeDefs, resolvers }])
});

server.listen({ port: 4001 }).then(({ url }) => {
console.log(`User service running at ${url}`);
});
예시: Order 서비스
const { ApolloServer, gql } = require('apollo-server');
const { buildFederatedSchema } = require('@apollo/federation');

const typeDefs = gql`
extend type User @key(fields: "id") {
id: ID! @external
orders: [Order]
}

type Order {
id: ID!
product: String
price: Float
}

type Query {
orders: [Order]
}
`;

const resolvers = {
User: {
orders(user) {
return [
{ id: '1', product: 'Laptop', price: 1000.0 },
{ id: '2', product: 'Phone', price: 500.0 }
];
}
},
Query: {
orders() {
return [
{ id: '1', product: 'Laptop', price: 1000.0 },
{ id: '2', product: 'Phone', price: 500.0 }
];
}
}
};

const server = new ApolloServer({
schema: buildFederatedSchema([{ typeDefs, resolvers }])
});

server.listen({ port: 4002 }).then(({ url }) => {
console.log(`Order service running at ${url}`);
});

2) 게이트웨이 설정

이제 Apollo Gateway를 설정하여 각각의 서브그래프를 하나의 통합된 GraphQL API로 연결합니다.

Apollo Gateway 설정:
javascript코드 복사const { ApolloServer } = require('apollo-server');
const { ApolloGateway } = require('@apollo/gateway');

const gateway = new ApolloGateway({
  serviceList: [
    { name: 'users', url: 'http://localhost:4001' },
    { name: 'orders', url: 'http://localhost:4002' }
  ]
});

const server = new ApolloServer({
  gateway,
  subscriptions: false
});

server.listen({ port: 4000 }).then(({ url }) => {
  console.log(`Gateway running at ${url}`);
});

게이트웨이는 각 서브그래프(User 서비스와 Order 서비스)를 연결하고, 클라이언트로부터 요청을 받습니다.

3) 클라이언트에서 쿼리 실행

이제 클라이언트는 게이트웨이를 통해 통합된 GraphQL API를 사용할 수 있습니다.

query {
users {
id
name
orders {
product
price
}
}
}

이 쿼리는 users 필드를 통해 사용자 목록을 가져오고, 각 사용자의 주문 데이터를 가져옵니다. 게이트웨이가 각 서비스에 쿼리를 분배하고, 최종적으로 클라이언트에 데이터를 반환합니다.


5. Federation의 장점

  • 마이크로서비스 아키텍처와의 자연스러운 통합: 각 서비스는 독립적으로 관리되며, 이를 통합하여 하나의 그래프로 제공할 수 있습니다.
  • 확장성: 개별 서비스는 독립적으로 개발 및 배포될 수 있어, 시스템 전체의 확장성이 향상됩니다.
  • 일관된 GraphQL API 제공: 클라이언트는 여러 개의 서비스로 나뉘어 있는 데이터를 하나의 통합된 그래프에서 쿼리할 수 있습니다.

6. Federation의 단점 및 고려사항

  • 복잡성 증가: 서비스가 많아질수록 Federation 구조가 복잡해질 수 있습니다. 각 서브그래프 간의 의존성을 관리하고, 데이터를 효율적으로 결합해야 합니다.
  • 추가적인 네트워크 호출: 게이트웨이가 각 서브그래프에 쿼리를 전달하고 데이터를 모으는 과정에서 네트워크 호출이 증가할 수 있습니다. 이는 성능에 영향을 줄 수 있으므로 최적화가 필요합니다.
  • 추가적인 인프라 관리: 게이트웨이와 각 서브그래프를 배포하고 모니터링하는 추가적인 인프라 관리가 필요합니다.

결론

GraphQL Federation은 여러 마이크로서비스를 통합하여 하나의 일관된 GraphQL API를 제공할 수 있는 강력한 아키텍처 패턴입니다. 특히 대규모 시스템에서 서비스 간의 의존성을 줄이고, 독립적으로 개발 및 배포할 수 있는 환경을 제공합니다.

Federation을 효과적으로 사용하려면 각 서브그래프 간의 관계를 명확하게 정의하고, 게이트웨이를 통해 통합된 데이터 흐름을 관리해야 합니다.