使用makeRemoteExecutableSchema拼接安全订阅

31
我们已经实现了模式拼接,其中GraphQL服务器从两个远程服务器获取模式并将它们拼接在一起。当我们只处理查询和变异时,一切都很正常,但现在我们有一个用例,甚至需要拼接订阅,而远程模式上实施了身份验证。
我们很难弄清楚如何通过网关将从客户端收到的连接参数中接收的授权令牌传递给远程服务器。
以下是我们内省模式的方式:
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实现这一目标的方法?感谢您的帮助。


我认为你有两个问题, 第一个问题是在没有授权密钥的情况下获取内省模式(据我所知,授权密钥是从客户端在连接上下文中接收的)。第二个问题是在每次订阅操作中以某种方式发送授权密钥。第一个问题可能可以通过正确的架构来解决。但是第二个问题目前在 subscription-transport-wsgraphql-tools 中的模式拼接中不受支持。 解决方案将不得不扩展他们创建的当前协议。 - Daniel Jakobsen Hallel
有进展了吗? - gandalfml
@gandalfml 很遗憾没有进展 :( - Niks
但是我取得了一些进展 :) 问题在于,每个WebSocketLink实例都是一个ws连接。因此,您不能为服务器拥有一个实例,而是为客户端连接拥有一个实例 :) 我将尝试在接下来的一周内在Gist上提供一个示例 - gandalfml
2个回答

1
我有一个可行的解决方案:不要为整个应用程序创建一个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'))

如果您有更多的远程模式,您只需在subscriptionClients映射中创建更多的SubscriptionClient实例即可。
要在远程模式中使用这些客户端,您需要完成两件事:
  1. expose them in the context:

    const server = new GraphQLServer({
      schema,
      context: ({ connection }) => {
        if (connection && connection.context) {
          return connection.context;
        }
      }
    });
    
  2. use custom link implementation instead of WsLink

    (operation, forward) => {
        const context = operation.getContext();
        const { graphqlContext: { subscriptionClients } } = context;
        return subscriptionClients && subscriptionClients[clientName] && subscriptionClients[clientName].request(operation);
    };
    
以这种方式,整个连接参数将被传递到远程服务器。
完整的示例可以在这里找到: https://gist.github.com/josephktcheung/cd1b65b321736a520ae9d822ae5a951b 免责声明:
代码不是我写的,@josephktcheung提供了一个示例。我只是帮了一点忙。这是原始讨论: https://github.com/apollographql/graphql-tools/issues/864

1
这是一个远程模式的工作示例,通过websocket进行订阅,通过http进行查询和变异。它可以通过自定义标头(参数)进行保护,并在此示例中显示。
流程: 客户端请求 -> 通过读取req或connection创建上下文(context)(jwt被解码并在上下文中创建用户对象) -> 执行远程模式 -> 调用链接(link) -> 链接(link)按操作分割(wsLink用于订阅,httpLink用于查询和变异) -> wsLink或httpLink访问上述创建的上下文(context)(=graphqlContext) -> wsLink或httpLink使用上下文(context)为远程模式创建标头(例如,在此示例中使用签名jwt的授权标头) -> “subscription”或“query or mutation”被转发到远程服务器。
注意:
  1. 目前,ContextLink 对 WebsocketLink 没有任何影响。因此,我们应该创建原始的 ApolloLink,而不是使用 concat。
  2. 在创建上下文时,请检查 connection,而不仅仅是 req。如果请求是 websocket,则前者将可用,并且它包含用户发送的元信息,例如身份验证令牌。
  3. HttpLink 期望具有标准规范的全局 fetch。因此,请勿使用 node-fetch,其规范不兼容(特别是与 TypeScript)。相反,请使用 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),
    }
  },
})

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接