Как получить имя операции Query/Mutation
Я новичок в Spring boot + GraphQL. Мне нужно получить имя операции Query/Mutation внутри моего класса контроллера.
Цель: необходимо получить разрешение для некоторых пользователей на определенные операции мутации / запроса. Здесь тип пользователя будет передан в качестве заголовка запроса, будет проверен и проверен, разрешен ли пользователю доступ к этой операции.
@PostMapping
public ResponseEntity<Object> callGraphQLService(@RequestBody String query, @RequestHeader("user") String userName) {
ExecutionResult result = graphService.getGraphQL().execute(ExecutionInput.newExecutionInput()
.query(query)
.context(userName)
.build());
return new ResponseEntity<>(result, HttpStatus.OK);
}
Предложите любой эффективный механизм для выполнения авторизации для конкретного запроса / мутации.
1 ответ
Я думаю, что вы думаете об авторизации в терминах REST, и она не очень хорошо отображается на GraphQL. Вместо единственного решения на верхнем уровне, основанного на имени операции (или на основе URL в REST), вам нужен более детальный подход. Вам необходимо знать, кому разрешено видеть / что делать на уровне поля, поскольку клиенту разрешено делать специальные выборки.
Есть несколько способов сделать это, но так как вы упомянули Spring, вы можете просто использовать Spring Security на уровне сервиса. Если каждое защищенное поле поддерживается служебным методом (и так и должно быть), вы можете защитить эти методы, используя Spring Security, как обычно.
Еще лучше, вы также должны предоставить GraphqlFieldVisibility
реализации, так что неавторизованные клиенты не могут даже знать о существовании полей, которые им не разрешено видеть в схеме. Вы можете использовать, например, Spring's SpelExpressionParser
принимать решения о том, какие части схемы будут видны динамически для каждого пользователя на основе правил Spring Security.
Если Spring Security не вариант, вы можете реализовать Instrumentation
(например, путем расширения SimpleInstrumentation
). Там вы можете реализовать обратные вызовы, такие как beginExecuteOperation
, это даст вам доступ к проанализированному запросу (достаточно, если вы на самом деле просто хотите выполнять только аутентификацию верхнего уровня в стиле REST), или begin(Deferred)Field
(который дает вам доступ к FieldDefinition
) или же beginFieldFetch/instrumentDataFetcher
(который дает вам доступ ко всему DataFetchingEnvironment
) выполнить авторизацию для каждого поля.
Если вы пойдете этим путем, вы можете сохранить информацию об аутентификации (например, требуемые роли) в самом определении поля как директивы. И держите текущего авторизованного пользователя в общем контексте. Таким образом, у вас всегда есть все необходимое для аутентификации на каждом уровне.
Во всех случаях желательно предоставить GraphqlFieldVisibility
полностью скрыть существование защищенных полей в контексте.
Вот абстрактный пример, показывающий основные моменты, используя Instrumentation
подход (поскольку для подхода Spring Security ничего особенного не требуется, просто используйте Spring Security как обычно):
//Checks if the current user has the needed roles for each field
public class AuthInstrumentation extends SimpleInstrumentation {
@Override
public DataFetcher<?> instrumentDataFetcher(DataFetcher<?> dataFetcher, InstrumentationFieldFetchParameters parameters) {
GraphQLFieldDefinition fieldDefinition = parameters.getEnvironment().getFieldDefinition();
//Each protected field is expected to have a directive called "auth" with an argument called "rolesRequired" that is a list of strings representing the roles
Optional<GraphQLArgument> rolesRequired = DirectivesUtil.directiveWithArg(fieldDefinition.getDirectives(), "auth", "rolesRequired");
if (rolesRequired.isPresent()) {
List<String> roles = (List<String>) rolesRequired.get().getValue();
User currentUser = parameters.getEnvironment().getContext(); //get the user from context
if (!currentUser.getRoles().containsAll(roles)) {
//Replace the normal resolution logic with the one that always returns null (or throws an exception) when the user doesn't have access
return env -> null;
}
}
return super.instrumentDataFetcher(dataFetcher, parameters);
}
}
Вам не нужно хранить необходимые роли в директивах, это просто удобное место. Вы можете получить ту же информацию из внешнего источника, если это уместно.
Затем зарегистрируйте эту аппаратуру:
GraphQL graphQL = GraphQL.newGraphQL(schema)
.instrumentation(new AuthInstrumentation())
.build();
И при выполнении запроса поместите текущего пользователя в контекст:
//Get the current user's roles however you normally do
User user = loadUser(userName);
ExecutionInput input = ExecutionInput.newExecutionInput()
.query(operation)
.context(user) //put the user into context so the instrumentation can get it
.build()
Таким образом, у вас есть все аккуратно разделенные (не требуется логика аутентификации в распознавателях, не требуется внешний контекст) и контекстные данные для каждого поля, даже без использования Spring Security.
Пойдем дальше и сделаем кастом GraphqlFieldVisibility
:
public class RoleBasedVisibility implements GraphqlFieldVisibility {
private final User currentUser;
public RoleBasedVisibility(User currentUser) {
this.currentUser = currentUser;
}
@Override
public List<GraphQLFieldDefinition> getFieldDefinitions(GraphQLFieldsContainer fieldsContainer) {
return fieldsContainer.getFieldDefinitions().stream()
.filter(field -> isFieldAllowed(field, currentUser))
.collect(Collectors.toList());
}
@Override
public GraphQLFieldDefinition getFieldDefinition(GraphQLFieldsContainer fieldsContainer, String fieldName) {
GraphQLFieldDefinition fieldDefinition = fieldsContainer.getFieldDefinition(fieldName);
return fieldDefinition == null || !isFieldAllowed(fieldDefinition, currentUser) ? null : fieldDefinition;
}
private boolean isFieldAllowed(GraphQLDirectiveContainer field, User user) {
//Same as above, extract this into a common function
Optional<GraphQLArgument> rolesRequired = DirectivesUtil.directiveWithArg(field.getDirectives(), "auth", "rolesRequired");
List<String> roles = (List<String>) rolesRequired.get().getValue();
return currentUser.getRoles().containsAll(roles);
}
}
Как видите, видимость зависит от пользователя, которого на этот раз вы не можете получить из контекста, поэтому вам нужно создавать его экземпляр для каждого запроса. Это означает, что вам необходимо преобразовать схему и создать экземпляр GraphQL для каждого запроса. В остальном то же самое.
GraphQLSchema schema = baseSchema.transform(
schemaBuilder -> schemaBuilder.fieldVisibility(new RoleBasedVisibility(currentUser)));
GraphQL graphQL = GraphQL.newGraphQL(schema)
.instrumentation(new AuthInstrumentation())
.build();
С этим у вас есть полная настройка безопасности. Неавторизованные пользователи даже не узнают, что поле существует, если им не разрешено. Если им разрешено видеть его в целом, но они могут получить его только условно, AuthInstrumentation
покрывает это.