GraphQL 拦截权限验证和异常拦截封装是在 GraphQL 服务中常见的一种需求,用于保护 GraphQL 接口免受未经授权的访问,并提供友好的错误信息处理。
1. 拦截权限验证
GraphQL 拦截权限验证通常涉及以下步骤:
-
定义权限规则: 确定哪些用户或角色具有权限访问 GraphQL 接口,以及哪些接口需要进行权限验证。
-
创建权限验证拦截器: 编写一个拦截器来拦截传入的 GraphQL 请求,在请求到达处理程序之前进行权限验证。拦截器通常会检查请求中的用户身份信息或令牌,并根据权限规则来决定是否允许继续执行请求。
-
实现权限验证逻辑: 在拦截器中实现具体的权限验证逻辑,例如验证用户是否具有访问特定字段或查询的权限,或者是否具有足够的角色权限。
-
拦截未经授权的请求: 如果权限验证失败,则拦截器可以返回适当的错误信息,或者抛出异常以终止请求并返回错误响应。
2. 异常拦截封装
异常拦截封装用于捕获并处理在 GraphQL 接口调用过程中可能发生的异常,以提供更好的错误处理体验。这通常涉及以下步骤:
-
创建全局异常处理器: 编写一个全局异常处理器,用于捕获 GraphQL 请求处理过程中抛出的异常。
-
定义异常处理逻辑: 在异常处理器中定义如何处理不同类型的异常。这可能包括将异常转换为友好的错误消息,记录异常信息以便进行故障排除,或者向客户端返回适当的 HTTP 响应。
-
注册异常处理器: 将异常处理器注册到 GraphQL 服务中,以确保它能够捕获并处理请求处理过程中抛出的异常。
通过实现权限验证和异常拦截封装,可以提高 GraphQL 服务的安全性和可靠性,确保只有经过授权的用户能够访问敏感数据,并提供友好的错误信息以便于开发人员和终端用户理解和处理。
1、引用Maven依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-graphql</artifactId>
<version>2.7.9</version>
</dependency>
<dependency>
<groupId>com.graphql-java-kickstart</groupId>
<artifactId>graphql-java-servlet</artifactId>
<version>15.0.0</version>
</dependency>
2、GraphQl配置类
/**
* @author Lucas
* date 2024/3/22 11:55
* description GraphQl配置类
*/
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass({GraphQlSourceBuilderCustomizer.class, SecurityGraphQlStrategy.class})
public class SecurityGraphQlConfig {
@Resource
private SecurityGraphQlStrategy securityGraphQlStrategy;
/**
* 拦截graphqlSource定义, 将securityGraphQlStrategy作为query/mutation策略进行配置
* @return graphqlSource自定义builder
*/
@Bean
public GraphQlSourceBuilderCustomizer sourceBuilderCustomizer() {
return (resourceBuilder) -> resourceBuilder.configureGraphQl(graphQlBuilder -> {
graphQlBuilder.queryExecutionStrategy(securityGraphQlStrategy)
.mutationExecutionStrategy(securityGraphQlStrategy)
.defaultDataFetcherExceptionHandler(new SecurityDataFetcherExceptionHandler())
.build();
});
}
}
3、权限验证拦截类
/**
* @author Lucas
* date 2024/3/22 11:55
* description 权限验证拦截类
*/
@Slf4j
@Component
public class SecurityGraphQlStrategy extends AsyncExecutionStrategy implements WebGraphQlInterceptor {
private static final String HEADERS = "headers";
/**
* 添加graphql自定义异常处理(将异常封装为R对象, 然后返回/打印R对象)
*/
public SecurityGraphQlStrategy() {
super(new SecurityDataFetcherExceptionHandler());
}
@Override
public CompletableFuture<ExecutionResult> execute(ExecutionContext executionContext, ExecutionStrategyParameters parameters) throws NonNullableFieldWasNullException {
HttpHeaders headers = executionContext.getGraphQLContext().get(HttpHeaders.class);
String token = headers.getFirst(ACCESS_TOKEN);
//权限验证逻辑
if (false) {
ExecutionResult executionResult = ExecutionResultImpl.newExecutionResult()
.data(null)
.errors(List.of(new GenericGraphQLError(JSON.toJSONString(R.fail(SecurityExceptionEnum.TOKEN_HDR_ABSENT)))))
.build();
return Mono.fromFuture(CompletableFuture.completedFuture(executionResult)).toFuture();
}
return super.execute(executionContext, parameters);
}
/**
* 拦截请求并将request里的headers copy到context中
*/
@Override
public Mono<WebGraphQlResponse> intercept(WebGraphQlRequest request, Chain chain) {
request.configureExecutionInput((input, builder) -> {
return builder.graphQLContext(builder1 -> {
builder1.of(HttpHeaders.class, request.getHeaders())
.of(HEADERS, request.getHeaders())
.build();
}).build();
});
return chain.next(request);
}
}
4、异常处理器类
/**
* @author Lucas
* date 2024/3/22 11:56
* description 异常处理器类
*/
public class SecurityDataFetcherExceptionHandler implements DataFetcherExceptionHandler {
private static final Logger logNotSafe = LogKit.getNotPrivacySafeLogger(SimpleDataFetcherExceptionHandler.class);
@Override
public CompletableFuture<DataFetcherExceptionHandlerResult> handleException(DataFetcherExceptionHandlerParameters handlerParameters) {
Throwable exception = unwrap(handlerParameters.getException());
SourceLocation sourceLocation = handlerParameters.getSourceLocation();
ResultPath path = handlerParameters.getPath();
Map<String, Object> arguments = handlerParameters.getDataFetchingEnvironment().getArguments();
BusinessExceptionWhileDataFetching error = new BusinessExceptionWhileDataFetching(path, exception, sourceLocation, arguments);
logException(error, exception);
return CompletableFuture.completedFuture(DataFetcherExceptionHandlerResult.newResult().error(error).build());
}
/**
* Called to log the exception - a subclass could choose to something different in logging terms
* @param error the graphql error
* @param e the exception that happened
*/
protected void logException(BusinessExceptionWhileDataFetching error, Throwable e) {
String errorMsg = "Exception while fetching data (%s) : %s. ".formatted(error.getPath(), error.getMessage());
if (e instanceof BusinessException) {
logNotSafe.warn(errorMsg + ExceptionUtils.filterThrowableMessage(e));
} else {
logNotSafe.warn(errorMsg, e);
}
}
/**
* Called to unwrap an exception to a more suitable cause if required.
*
* @param exception the exception to unwrap
*
* @return the suitable exception
*/
protected Throwable unwrap(Throwable exception) {
if (exception.getCause() != null) {
if (exception instanceof CompletionException) {
return exception.getCause();
}
}
return exception;
}
}
5、异常信息捕获返回固定格式类
/**
* @author Lucas
* date 2024/3/22 11:53
* description 异常信息捕获返回固定格式类
*/
public class BusinessExceptionWhileDataFetching implements GraphQLError {
private final String message;
private final List<Object> path;
private final Throwable exception;
private final List<SourceLocation> locations;
private final Map<String, Object> extensions;
public BusinessExceptionWhileDataFetching(ResultPath path, Throwable exception, SourceLocation sourceLocation, Map<String, Object> arguments) {
this.path = assertNotNull(path).toList();
this.exception = assertNotNull(exception);
this.locations = Collections.singletonList(sourceLocation);
this.extensions = mkExtensions(exception);
this.message = mkMessage(exception, arguments);
}
private String mkMessage(Throwable exception, Map<String, Object> arguments) {
String msg = Optional.ofNullable(exception.getMessage()).orElse("");
var r = R.fail(99, msg.length() > 80 ? "系统暂时繁忙,请稍后重试。" : msg);
r.setErrorData(arguments);
return JSON.toJSONString(r);
}
private Map<String, Object> mkExtensions(Throwable exception) {
Map<String, Object> extensions = null;
if (exception instanceof GraphQLError) {
Map<String, Object> map = ((GraphQLError) exception).getExtensions();
if (map != null) {
extensions = new LinkedHashMap<>(map);
}
}
return extensions;
}
@Override
public String getMessage() {
return message;
}
@Override
public List<SourceLocation> getLocations() {
return locations;
}
@Override
public List<Object> getPath() {
return path;
}
@Override
public Map<String, Object> getExtensions() {
return extensions;
}
@Override
public ErrorType getErrorType() {
return ErrorType.DataFetchingException;
}
@Override
public String toString() {
return "ExceptionWhileDataFetching{" +
"path=" + path +
", exception=" + exception +
", locations=" + locations +
'}';
}
@SuppressWarnings("EqualsWhichDoesntCheckParameterClass")
@Override
public boolean equals(Object o) {
return GraphqlErrorHelper.equals(this, o);
}
@Override
public int hashCode() {
return GraphqlErrorHelper.hashCode(this);
}
}