在 spring cloud 微服务中使用 GraphQL 时,捕获和处理 AccessDeniedException 异常

在上篇文章《spring cloud security 中的 AuthenticationEntryPoint 设置与 AccessDeniedException 捕获过程》中,讲到 spring cloud securityAccessDeniedException 异常的捕获和处理,但那只适用于 RESTFul 类型的接口,对于 GraphQL 类型的请求而言,并不适用。这里就来说明如何使用针对 GraphQL 接口进行 AccessDeniedException 异常处理。

GraphQL报错

对于 GraphQL 请求来说,由于请求地址不变(不做配置的话为 /graphql),对于其作权限控制建议在对应方法上进行控制,所以需要对 spring cloud securityhttpSecurity 配置多添加一个配置项,让其通过 FilterSecurityInterceptor 过滤器而不报错

httpSecurity
    .authorizeRequests()
    .antMatchers("/graphql").permitAll()
    .anyRequest().authenticated()
复制代码

这样一来只能在每个方法前加 @PreAuthorize("hasAuthority('admin')") 之类的注解,来进行权限控制。但是,对于 GraphQL 请求来说,如果没有权限,并抛出 AccessDeniedException 异常,他并不会进入到 AuthenticationEntryPoint 入口中处理,当不做任何操作时,如果请求的接口没有权限,会打印如下错误

org.springframework.security.access.AccessDeniedException: 不允许访问
    at org.springframework.security.access.vote.AffirmativeBased.decide(AffirmativeBased.java:73) ~[spring-security-core-5.6.0.jar:5.6.0]
    ...
复制代码

并向前端返回

{
    "errors": [
        {
            "message": "Internal Server Error(s) while executing query",
            "locations": []
        }
    ],
    "data": null
}
复制代码

其实,这种情况下,应用也可以正常运行,但反馈给前端的格式却太过笼统,接下来分析报错原因和如何处理。

错误原因

对于 GraphQL 请求来说,一次请求,可以调用多个处理方法,这也是 GraphQL 的设计初衷,由于这个原因,所以和传统 RESTFul 请求有很大不同,他改写了 doGet 方法,不由这个入口请求对应的方法,而是通过各个过滤器后直接返回,所以不同于 RESTFul, 他如果没有权限,不会在 ApplicationFilterChaininternalDoFilter 方法的 servlet.service(request, response); 中抛出异常,所以就不能触发 AuthenticationEntryPoint 入口方法。

graphQL 请求最终会进入到 ExecutionStrategy 抽象类中

@PublicSpi
public abstract class ExecutionStrategy {
    // ...
    protected ExecutionStrategy() {
        this.dataFetcherExceptionHandler = new SimpleDataFetcherExceptionHandler();
    }
    protected ExecutionStrategy(DataFetcherExceptionHandler dataFetcherExceptionHandler) {
        this.dataFetcherExceptionHandler = dataFetcherExceptionHandler;
    }
    // ...
    protected CompletableFuture<FetchedValue> fetchField(ExecutionContext executionContext, ExecutionStrategyParameters parameters) {
        // ...
        try {
            // 异常抛出
            Object fetchedValueRaw = dataFetcher.get(environment);
            fetchedValue = Async.toCompletableFuture(fetchedValueRaw);
        } catch (Exception var21) {
            // 异常捕获
            if (logNotSafe.isDebugEnabled()) {
                logNotSafe.debug(String.format("'%s', field '%s' fetch threw exception", executionId,((ExecutionStepInfo)executionStepInfo.get()).getPath()), var21);
            }
            fetchedValue = new CompletableFuture();
            fetchedValue.completeExceptionally(var21);
        }
        fetchCtx.onDispatched(fetchedValue);
        return fetchedValue.handle((result, exception) -> {
                fetchCtx.onCompleted(result, exception);
                if (exception != null) {
                this.handleFetchingException(executionContext, environment, exception);
                return null;
            } else {
                return result;
            }
        }).thenApply((result) -> {
            return this.unboxPossibleDataFetcherResult(executionContext, parameters, result);
        });
     }
    // ...
    protected void handleFetchingException(ExecutionContext executionContext, DataFetchingEnvironment environment, Throwable e) {
        DataFetcherExceptionHandlerParameters handlerParameters = DataFetcherExceptionHandlerParameters.newExceptionParameters().dataFetchingEnvironment(environment).exception(e).build();
        DataFetcherExceptionHandlerResult handlerResult;
        try {
            // 异常处理
            handlerResult = this.dataFetcherExceptionHandler.onException(handlerParameters);
        } catch (Exception var7) {
            handlerParameters = DataFetcherExceptionHandlerParameters.newExceptionParameters().dataFetchingEnvironment(environment).exception(var7).build();
            handlerResult = (new SimpleDataFetcherExceptionHandler()).onException(handlerParameters);
        }
        handlerResult.getErrors().forEach(executionContext::addError);
    }
}
复制代码
异常处理

处理方式是实现 GraphQLErrorHandler 异常处理接口,来捕获和处理错误返回

@Component
public class CustomGraphQLErrorHandler implements GraphQLErrorHandler {
    @Override
    public List<GraphQLError> processErrors(List<GraphQLError> graphQLErrors) {
        return graphQLErrors.stream().map(this::handleGraphQLError).collect(Collectors.toList());
    }
    private GraphQLError handleGraphQLError(GraphQLError error) {
        if (error instanceof GraphQLException) {
            throw new ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR, "GraphQLException as GraphQLError...", (GraphQLException) error);
        } else if (error instanceof ValidationError){
            throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "ValidationError: " + error.getMessage());
        } else if (error instanceof ExceptionWhileDataFetching) {
            // 处理 AccessDeniedException 错误
            throw new ResponseStatusException(HttpStatus.FORBIDDEN, HttpStatus.FORBIDDEN.getReasonPhrase());
        } else {
            return error;
        }
    }
}
复制代码

这种方法可以使前端返回为

{
    "errors": [
        {
            "message": "403 FORBIDDEN \"Forbidden\"",
            "locations": []
        }
    ],
    "data": null
}
复制代码

就有了清晰的提示,但后端还是会打印错误,因为在 ExecutionStrategy 类中,实例化了 SimpleDataFetcherExceptionHandler 类,这个类其中的 onException 方法中有

@PublicApi
public class SimpleDataFetcherExceptionHandler implements DataFetcherExceptionHandler {
    private static final Logger logNotSafe = LogKit.getNotPrivacySafeLogger(SimpleDataFetcherExceptionHandler.class);
    // ...
    public DataFetcherExceptionHandlerResult onException(DataFetcherExceptionHandlerParameters handlerParameters) {
        // ...
        // 打印信息
        logNotSafe.warn(error.getMessage(), exception);
        return DataFetcherExceptionHandlerResult.newResult().error(error).build();
    }
}
复制代码

所以如果要自定义打印信息,需要替换掉这个类,所以我们可以重写 DataFetcherExceptionHandler 接口,并替换掉 ExecutionStrategy 原来的构造函数

首先实现 DataFetcherExceptionHandler 接口代码

@Slf4j
public class GraphQLExceptionHandler implements DataFetcherExceptionHandler {
    @Override
    public DataFetcherExceptionHandlerResult onException(DataFetcherExceptionHandlerParameters handlerParameters) {
        Throwable exception = handlerParameters.getException();
        SourceLocation sourceLocation = handlerParameters.getSourceLocation();
        ResultPath path = handlerParameters.getPath();
        if (exception instanceof AccessDeniedException) {
            // 自定义打印
            log.warn("unauthorized to access " + path);
        }
        ExceptionWhileDataFetching error = new ExceptionWhileDataFetching(path, exception, sourceLocation);
        return DataFetcherExceptionHandlerResult.newResult().error(error).build();
    }
}
复制代码

对于重新加载自定义的 GraphQLExceptionHandler 处理方法,可以继承 AsyncExecutionStrategy 类,并重写构造函数

@Component
public class CustomExecutionStrategy extends AsyncExecutionStrategy {
    public CustomExecutionStrategy() {
        // 父类中实例化类,
        super(new GraphQLExceptionHandler());
    }
    @Override
    public CompletableFuture<ExecutionResult> execute(ExecutionContext executionContext, ExecutionStrategyParameters parameters) throws NonNullableFieldWasNullException {
        return super.execute(executionContext, parameters);
    }
}
复制代码

这样,后台将会打印自定义信息。

通过以上处理就可以自定义 GraphQL 的 AccessDeniedException 异常处理及提示。

*注:本文 GraphQL 基于 com.graphql-java-kickstart 下的 graphql-spring-boot-starter 包,版本号为 11.1.0

Guess you like

Origin juejin.im/post/7050099954542444581