GraphQL은 유연하고 요율적인 API 쿼리 언어이지만, 그만큼 보안적인 측면에서 신중한 설계가 요구된다. 단일 엔드포인트로 모든 요청을 처리한다는 특성상, 인증(Authentication), 권한 부여(Authorization), 쿼리 검증, 로깅 등 여러 계층의 보안 메커니즘이 필요하다.

 

GraphQL 보안 개요


GraphQL 보안은 다음과 같은 요소로 구성된다.

  • 인증 (Authentication)
  • 권한 부여 (Authorization)
  • 쿼리 및 뮤테이션 검증
  • 보안 이벤트 및 로깅
  • 보안 업데이트 및 버전 관리

 


🔑 인증(Authentication)


GraphQL 쿼리를 실행하기 전, 클라이언트가 신뢰할 수 있는 사용자인지 검증하는 단계이다.

 

대표 인증 방식

  • JWT (JSON Web Token)
    👉 클라이언트가 로그인 후 받은 토큰을 요청 시 HTTP 헤더에 포함
  • OAuth 2.0
    👉 외부 인증 제공자를 통해 인증 (Google, Kakao 등)

 

예시: JWT 기반 인증 (Spring Security)

@Override
protected void configure(HttpSecurity http) throws Exception {
    http.csrf().disable()
        .authorizeHttpRequests()
        .requestMatchers("/graphql").authenticated()
        .and()
        .addFilterBefore(jwtAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);
}
  • 이 설정은 /graphql 요청에 대해 JWT 인증을 요구한다.

 


권한 부여(Authorization)


인증 이후, 요청한 사용자가 해당 데이터를 볼 수 있는 권한이 있는지 판단하는 단계이다.

 

리졸버에서 권한 체크 예시

@PreAuthorize("hasRole('ADMIN')")
public User getSensitiveUserInfo(Long id) {
    return userService.getUserById(id);
}
  • Spring Security와 함께 GraphQL을 사용할 경우, 리졸버에 직접 @PreAuthorize, @Secured 등의 애노테이션을 사용하여 권한을 제어할 수 있다.

 


쿼리 및 뮤테이션 검증


입력값이 유효한지, 요청 쿼리가 지나치게 크거나 복잡하지 않은지 검증한다.

 

검증 대상

  • 입력값 유효성 검사
  • 권한 검사
  • 쿼리 구조 검증 (복잡도, 깊이 등)

 


보안 이벤트 및 로깅


GraphQL은 모든 요청이 단일 엔드포인트로 들어오므로, 정교한 로깅보안 이벤트 모니터링이 필요하다.

 

예시: GraphQL Instrumentation을 통한 요청 로깅

public class LoggingInstrumentation extends SimpleInstrumentation {
    @Override
    public InstrumentationContext<ExecutionResult> beginExecution(InstrumentationExecutionParameters parameters) {
        log.info("GraphQL Request: {}", parameters.getQuery());
        return super.beginExecution(parameters);
    }
}

 


보안 업데이트 및 버전 관리


GraphQL 스키마나 리졸버는 종종 변경된다. 보안적으로는 다음 내용을 주의해야 한다.

  • 변경된 필드에 대한 권한 검증 추가
  • 민감한 필드 제거 후 API 버전 관리 적용
  • 의도치 않은 정보 노출 방지

 


인증과 권한 적용 방법


GraphQL은 단일 엔드포인트(/graphql)를 통해 다양한 쿼리와 뮤테이션을 처리한다. 이 특성 때문에 인증과 권한 관리에 신경 써야 할 포인트가 몇 가지 있다.

 

1. Spring Security로 인증 처리

가장 기본적인 방식으로, GraphQL 요청이 들어오는 /graphql 엔드포인트 전체에 인증을 요구한다.

 

📌 예시: Spring Security 설정

@Configuration
@EnableWebSecurity
public class SecurityConfig {

  @Bean
  public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
    http.csrf().disable()
        .authorizeHttpRequests()
        .requestMatchers("/graphql").authenticated()
        .and()
        .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
        .and()
        .oauth2ResourceServer().jwt(); // 또는 JWT 필터 사용 가능
    return http.build();
  }
}
  • 인증이 안 된 요청은 모두 401 Unauthorized 응답
  • 로그인 또는 토큰이 필요한 API 서버에서 사용

 

2. /graphql 전체 허용 + 특정 Query/Muation에만 인증 적용

이 경우, GraphQL 엔드포인트는 열려 있지만, 쿼리/뮤테이션 실행 시점에 인증 여부를 리졸버 단에서 검사한다.

 

📌 예시: 리졸버 메서드에 권한 제어 애노테이션 사용

@Component
public class UserResolver {

  @PreAuthorize("isAuthenticated()")
  public User getMyProfile() {
    // 현재 로그인된 사용자 정보 조회
  }

  public List<Product> getPublicProducts() {
    // 인증 없이 호출 가능
  }
}
  • 인증이 필요한 쿼리와 아닌 쿼리를 분리 가능
  • 공용 쿼리와 개인 쿼리를 모두 제공하는 서비스에 적합

 

3. Directive를 활용한 인증 및 권한 제어

GraphQL의 Directive는 쿼리 또는 스키마 수준에서 조건을 설정할 수 있도록 하는 강력한 도구이다. 이를 인증/권한 시스템과 연동하면 스키마 기반 권한 제어가 가능하다.

 

1️⃣ 인증 디렉티브 정의

directive @auth(role: Role = USER) on FIELD_DEFINITION

enum Role {
  USER
  ADMIN
}

 

2️⃣ 스키마에 디렉티브 적용

type Query {
  myOrders: [Order] @auth(role: USER)
  allUsers: [User] @auth(role: ADMIN)
}

 

3️⃣ Java에서 디렉티브 처리 구현

@Component
public class AuthDirectiveWiring implements SchemaDirectiveWiring {

  @Override
  public GraphQLFieldDefinition onField(SchemaDirectiveWiringEnvironment<GraphQLFieldDefinition> env) {
    DataFetcher<?> originalFetcher = env.getFieldDataFetcher();
    String requiredRole = env.getDirective().getArgument("role").getValue().toString();

    DataFetcher<?> authFetcher = dataEnv -> {
      Authentication auth = SecurityContextHolder.getContext().getAuthentication();
      if (auth == null || !auth.isAuthenticated()) {
        throw new AccessDeniedException("Unauthorized");
      }

      if (!auth.getAuthorities().contains(new SimpleGrantedAuthority("ROLE_" + requiredRole))) {
        throw new AccessDeniedException("Forbidden");
      }

      return originalFetcher.get(dataEnv);
    };

    env.setFieldDataFetcher(authFetcher);
    return env.getElement();
  }
}
  • 인증 및 권한 요구사항이 스키마에 명시됨
  • 보안 정책이 코드가 아닌 스키마 중심으로 관리 가능
  • 쿼리와 뮤테이션 모두에 적용 가능

 


쿼리 보호 전략 (실전 팁)


GraphQL은 매우 유연한 쿼리 언어이지만, 이 유연성이 곧 보안 위협이 될 수 있다. 예를 들어 악의적인 사용자가 과도하게 복잡한 쿼리를 보내 서버 리소스를 고갈시키거나, 깊게 중첩된 요청으로 시스템을 마비시킬 수 있다.

 

세 가지 주요 전략

  1. 쿼리 실행 Timeout 설정
  2. 쿼리 복잡도 / 깊이 / 길이 제한
  3. Whitelist(허용 목록) 기반 쿼리 검증

쿼리 실행 시간 제한 (Timeout 적용)


왜 필요한가?

GraphQL 쿼리는 매우 깊게 중첩되거나, 의도적으로 리졸버를 오래 대기시키는 방식으로 공격할 수 있다. 이러한 요청이 반복되면 서버 리소스를 점유해 서비스 장애(DOS)를 유발할 수 있다.

 

구현 방법 1: Instrumentation을 활용한 Timeout 적용

GraphQL Java에서는 Instrumentation을 활용해 쿼리 실행 시간을 제한할 수 있다.

public class TimeoutInstrumentation extends SimpleInstrumentation {

  private final ExecutorService executor = Executors.newCachedThreadPool();
  private final long timeoutMillis;

  public TimeoutInstrumentation(long timeoutMillis) {
    this.timeoutMillis = timeoutMillis;
  }

  @Override
  public InstrumentationContext<ExecutionResult> beginExecution(InstrumentationExecutionParameters parameters) {
    Future<ExecutionResult> future = executor.submit(() ->
        parameters.getExecutionInput().getGraphQL().execute(parameters.getExecutionInput())
    );

    try {
      return new SimpleInstrumentationContext<>() {
        @Override
        public void onCompleted(ExecutionResult result, Throwable t) {
          future.cancel(true);
        }
      };
    } catch (Exception e) {
      throw new RuntimeException("GraphQL Timeout", e);
    }
  }
}

 

📌 적용 방법

GraphQL.newGraphQL(schema)
    .instrumentation(new TimeoutInstrumentation(2000)) // 2초 제한
    .build();
  • 요청이 2초 이상 걸리면 자동으로 중단되고, 에러 응답이 반환된다.

 


쿼리 제한: 복잡도, 깊이, 길이 제한


왜 필요한가?

GraphQL은 클라이언트가 쿼리 구조를 완전히 정의할 수 있기 때문에, 무분별한 요청을 막을 수단이 필요하다.

 

제한 방식

제한 항목 설명
Complexity 쿼리의 연산량을 기준으로 제한
Depth 중첩 레벨을 기준으로 제한
Length 전체 쿼리 문자열의 길이를 기준으로 제한

 

Query Complexity 계산 예시

query {
  user(id: 1) {
    id
    name
    orders {
      id
      total
      items {
        id
        name
      }
    }
  }
}
  • user: 1
  • orders: 5
  • items: 3
  • 총 Complexity: 1 + 5 + (3 x N) <- N은 아이템 수

 

GraphQL Java 설정 예시

GraphQL graphQL = GraphQL
  .newGraphQL(schema)
  .instrumentation(
    new ChainedInstrumentation(List.of(
      new MaxQueryComplexityInstrumentation(100),
      new MaxQueryDepthInstrumentation(10)
    ))
  )
  .build();
  • MaxQueryDepthInstrumentation: 중첩 깊이를 제한

 


쿼리 Whitelist 적용


왜 필요한가?

GraphQL은 단일 엔드포인트(/graphql)에서 모든 쿼리를 처리하기 때문에, 미리 승인된 쿼리 외에는 실행되지 않도록 제한하는 전략이 필요할 수 있다.

 

주로 금융, 정부, 의료 등 보안이 중요한 API에서 활용된다.

 

구현 방법 예시

1️⃣ 허용할 쿼리를 식별자(해시) 또는 ID로 등록

{
  "queryId": "getUserById",
  "variables": { "id": "123" }
}

 

2️⃣ 서버에서 허용된 쿼리만 매핑

Map<String, String> whitelist = Map.of(
  "getUserById", "query getUser($id: ID!) { user(id: $id) { name } }"
);

 

3️⃣ 요청 시 전달된 ID 기반 쿼리를 대체

String query = whitelist.get(request.getQueryId());
ExecutionInput executionInput = ExecutionInput.newExecutionInput()
    .query(query)
    .variables(request.getVariables())
    .build();