Создание защищенных подписок с помощью makeRemoteExecutableSchema

Мы реализовали сшивание схем, при котором сервер GraphQL извлекает схему с двух удаленных серверов и сшивает их вместе. Все работало нормально, когда мы работали только с запросами и мутациями, но теперь у нас есть вариант использования, когда нам даже нужно сшивать подписки, а удаленная схема имеет аутентификацию, реализованную поверх нее.

Нам сложно понять, как передать токен авторизации, полученный в connectionParams, от клиента к удаленному серверу через шлюз.

Вот как мы исследуем схему:

Код шлюза API:

const getLink = async(): Promise<ApolloLink> => {
const http = new HttpLink({uri: process.env.GRAPHQL_ENDPOINT, fetch:fetch})

const link = setContext((request, previousContext) => {
    if (previousContext
        && previousContext.graphqlContext
        && previousContext.graphqlContext.request
        && previousContext.graphqlContext.request.headers
        && previousContext.graphqlContext.request.headers.authorization) {
        const authorization = previousContext.graphqlContext.request.headers.authorization;
        return {
            headers: {
                authorization
            }
        }
    }
    else {
        return {};
    }
}).concat(http);

const wsLink: any = new WebSocketLink(new SubscriptionClient(process.env.REMOTE_GRAPHQL_WS_ENDPOINT, {
    reconnect: true,
    // There is no way to update connectionParams dynamically without resetting connection
    // connectionParams: () => { 
    //     return { Authorization: wsAuthorization }
    // }
}, ws));


// Following does not work
const wsLinkContext = setContext((request, previousContext) => {
    let authToken = previousContext.graphqlContext.connection && previousContext.graphqlContext.connection.context ? previousContext.graphqlContext.connection.context.Authorization : null
    return {
        context: {
            Authorization: authToken
        }
    }
}).concat(<any>wsLink);

const url = split(({query}) => {
    const {kind, operation} = <any>getMainDefinition(<any>query);
    return kind === 'OperationDefinition' && operation === 'subscription'
},
wsLinkContext,
link)

return url;
}

const getSchema = async (): Promise < GraphQLSchema > => {
  const link = await getLink();
  return makeRemoteExecutableSchema({
    schema: await introspectSchema(link),
    link,
  });
}
const linkSchema = `
  extend type UserPayload {
    user: User
  }
`;
const schema: any = mergeSchemas({
  schemas: [linkSchema, getSchema],
});
const server = new GraphQLServer({
  schema: schema,
  context: req => ({
    ...req,
  })
});

Есть ли способ добиться этого с помощью graphql-tools? Любая помощь приветствуется.


person Niks    schedule 25.06.2018    source источник
comment
Я думаю, у вас есть две проблемы: первая - получить схему интроспекции без какого-либо ключа авторизации (из того, что я понял, ключ авторизации получен от клиента в контексте соединения). а второй - каким-то образом при каждой операции подписки отправлять ключ аутентификации. первая проблема, вероятно, решима при правильной архитектуре. но вторая проблема в настоящее время не поддерживается в subscription-transport-ws или graphl-tools сшиванием схемы. решение для этого должно будет расширить текущий протокол, который они создали.   -  person Daniel Jakobsen Hallel    schedule 05.07.2018
comment
Есть ли в этом прогресс?   -  person gandalfml    schedule 08.09.2018
comment
@gandalfml к сожалению нет прогресса :(   -  person Niks    schedule 13.09.2018
comment
Немного продвинулся :) Дело в том, что каждый экземпляр WebSocketLink - это одно соединение ws. Итак, у вас не может быть один экземпляр для сервера, а скорее один экземпляр для подключения к клиенту :) Я постараюсь предоставить пример по сути на следующей неделе.   -  person gandalfml    schedule 14.09.2018


Ответы (2)


У меня есть одно рабочее решение: идея состоит в том, чтобы не создавать один экземпляр SubscriptionClient для всего приложения. Вместо этого я создаю клиентов для каждого подключения к прокси-серверу:

server.start({
    port: 4000,
    subscriptions: {
      onConnect: (connectionParams, websocket, context) => {
        return {
          subscriptionClients: {
            messageService: new SubscriptionClient(process.env.MESSAGE_SERVICE_SUBSCRIPTION_URL, {
              connectionParams,
              reconnect: true,
            }, ws)
          }
        };
      },
      onDisconnect: async (websocket, context) => {
        const params = await context.initPromise;
        const { subscriptionClients } = params;
        for (const key in subscriptionClients) {
          subscriptionClients[key].close();
        }
      }
    }
  }, (options) => console.log('Server is running on http://localhost:4000'))

если бы у вас было больше удаленных схем, вы бы просто создали больше экземпляров SubscriptionClient на subscriptionClients карте.

Чтобы использовать этих клиентов в удаленной схеме, вам нужно сделать две вещи:

  1. выставить их в контексте:

    const server = new GraphQLServer({
      schema,
      context: ({ connection }) => {
        if (connection && connection.context) {
          return connection.context;
        }
      }
    });
    
  2. использовать реализацию настраиваемой ссылки вместо WsLink

    (operation, forward) => {
        const context = operation.getContext();
        const { graphqlContext: { subscriptionClients } } = context;
        return subscriptionClients && subscriptionClients[clientName] && subscriptionClients[clientName].request(operation);
    };
    

Таким образом, все параметры подключения будут переданы на удаленный сервер.

Полный пример можно найти здесь: https://gist.github.com/josephktcheung/cd1b65b20a321736a >

Заявление об ограничении ответственности:

Код не мой, поскольку @josephktcheung опередил меня, предоставив пример. Я просто немного помог с этим. Вот исходное обсуждение: https://github.com/apollographql/graphql-tools/issues/864

person gandalfml    schedule 17.09.2018

Это рабочий пример удаленной схемы с подпиской через webscoket и запросом и изменением через http. Его можно защитить с помощью настраиваемых заголовков (параметров) и показать в этом примере.

Поток

Клиентский запрос -> context создается путем чтения req или connection (jwt декодируется и создает объект пользователя в контексте)
-> выполняется удаленная схема -> вызывается link -> link разделяется операцией (wsLink для подписки, httpLink для запросов и изменений) -> wsLink или httpLink доступ к context созданному выше (= graphqlContext) -> wsLink или httpLink используют context для созданных заголовков (заголовок авторизации с подписанным jwt в этом примере) для удаленной схемы. -> «подписка» или «запрос или изменение» пересылаются на удаленный сервер.

Примечание

  1. В настоящее время ContextLink не влияет на WebsocketLink. Итак, вместо concat мы должны создать необработанный ApolloLink.
  2. При создании контекста проверяйте connection, а не только req. Первый будет доступен, если запрос является веб-сокетом и содержит метаинформацию, отправляемую пользователем, например токен аутентификации.
  3. HttpLink ожидает глобальную выборку со стандартной спецификацией. Таким образом, не используйте node-fetch, спецификация которого несовместима (особенно с машинописным текстом). Вместо этого используйте cross-fetch.
const wsLink = new ApolloLink(operation => {
    // This is your context!
    const context = operation.getContext().graphqlContext

    // Create a new websocket link per request
    return new WebSocketLink({
      uri: "<YOUR_URI>",
      options: {
        reconnect: true,
        connectionParams: { // give custom params to your websocket backend (e.g. to handle auth) 
          headers: {
            authorization: jwt.sign(context.user, process.env.SUPER_SECRET),
            foo: 'bar'
          }
        },
      },
      webSocketImpl: ws,
    }).request(operation)
    // Instead of using `forward()` of Apollo link, we directly use websocketLink's request method
  })

const httpLink = setContext((_graphqlRequest, { graphqlContext }) => {
  return {
    headers: {
      authorization: jwt.sign(graphqlContext.user, process.env.SUPER_SECRET),
    },
  }
}).concat(new HttpLink({
  uri,
  fetch,
}))

const link = split(
  operation => {
    const definition = getMainDefinition(operation.query)
    return (
      definition.kind === 'OperationDefinition' &&
      definition.operation === 'subscription'
    )
  },
  wsLink, // <-- Executed if above function returns true
  httpLink, // <-- Executed if above function returns false
)

const schema = await introspectSchema(link)

const executableSchema = makeRemoteExecutableSchema({
    schema,
    link,
  })

const server = new ApolloServer({
  schema: mergeSchemas([ executableSchema, /* ...anotherschemas */]),
  context: ({ req, connection }) => {
    let authorization;
    if (req) { // when query or mutation is requested by http
      authorization = req.headers.authorization
    } else if (connection) { // when subscription is requested by websocket
      authorization = connection.context.authorization
    }
    const token = authorization.replace('Bearer ', '')
    return {
      user: getUserFromToken(token),
    }
  },
})
person jjangga    schedule 31.01.2020