在上篇文章《spring cloud security 中的 AuthenticationEntryPoint 设置与 AccessDeniedException 捕获过程》中,讲到 spring cloud security
中 AccessDeniedException
异常的捕获和处理,但那只适用于 RESTFul 类型的接口,对于 GraphQL 类型的请求而言,并不适用。这里就来说明如何使用针对 GraphQL 接口进行 AccessDeniedException
异常处理。
GraphQL报错
对于 GraphQL 请求来说,由于请求地址不变(不做配置的话为 /graphql
),对于其作权限控制建议在对应方法上进行控制,所以需要对 spring cloud security
中 httpSecurity
配置多添加一个配置项,让其通过 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, 他如果没有权限,不会在 ApplicationFilterChain
中 internalDoFilter
方法的 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