<p>apollo-link-ws: como alterar connectionParams (e token) após a configuração inicial?</p>

Criado em 31 out. 2017  ·  28Comentários  ·  Fonte: apollographql/apollo-link

Os documentos descrevem a autenticação em websockets como fornecendo o token em connectionParams ao criar o WsLink inicialmente.

Mas não está claro como alterar o token posteriormente e reinicializar a conexão se o usuário efetuar logout e efetuar login novamente (possivelmente com um token diferente)

Consegui alcançar o resultado desejado assim:

export const changeSubscriptionToken = token => {
  if (wsLink.subscriptionClient.connectionParams.authToken === token) {
    return
  }

  wsLink.subscriptionClient.connectionParams.authToken = token
  wsLink.subscriptionClient.close()
  wsLink.subscriptionClient.connect()
}

mas parece meio hacky, já que wsLink.subscriptionClient está marcado como privado e não deve ser acessível de fora

enhancement

Comentários muito úteis

Como o novo WebSocketLink ainda está criando uma instância de subscriptions-transport-ws , estou aplicando manualmente meu middleware ao cliente de Transporte de Assinatura, em vez de ao WebSocketLink.

Estou usando o mesmo processo que fiz na v1, embora pareça meio complicado não tê-lo como parte da API WebSocketLink.

// create the web socket link
const wsLink = new WebSocketLink({
  uri: 'ws://example.com',
  options: {
    reconnect: true
  }
})
// create my middleware using the applyMiddleware method from subscriptions-transport-ws
const subscriptionMiddleware = {
  applyMiddleware (options, next) {
    options.auth = { ... }
    next()
  }
}
// add the middleware to the web socket link via the Subscription Transport client
wsLink.subscriptionClient.use([subscriptionMiddleware])

Quaisquer valores que você adicionar ao objeto de opções estarão disponíveis no retorno de chamada onOperation no lado do servidor.

Todos 28 comentários

Você pode usar uma função que resolve para um objeto no connectionParams.

    const wsLink = new WebSocketLink({
      uri: config.subscriptionURL,
      options: {
        reconnect: true,
        connectionParams: () => ({
          authToken: reduxStore.getState().authentication.token,
        }),
      },
    });

EDITAR:

Oh espere, isso não é chamado depois da primeira vez... Talvez concat com outro middleware que coloque um objeto de autenticação na resposta que você pode analisar com onOperation do lado do servidor?

Aqui está o que eu tenho para você, modelado da maneira que eu estava fazendo com o ApolloClient V1

Adicione um novo middleware de link para anexar o token de autenticação à carga útil

const authMiddleware = new ApolloLink((operation, forward) => {
    // Add the authorization to the headers for HTTP authentication
    operation.setContext({
      headers: {
        authorization: `Bearer ${authToken()}`,
      },
    });

    // Add onto payload for WebSocket authentication
    (operation as Operation & { authToken: string | undefined }).authToken = authToken();


    return (forward as any)(operation);
  });

const myLink = concat(myLink, wsLink);

Em seguida, no lado do servidor, você pode verificar e aplicar ao contexto usando o gancho onOperation

function configureDecodeTokenSocketMiddleware(authURL: string) {
  return async function decodeTokenSocketMiddleware<ConnectionParams extends { authToken: string }>(connectionParams: ConnectionParams, operationParams: object) {
    let authPayload;
    try {
      if (typeof connectionParams.authToken === 'string') {
        authPayload = await verifyJWT(authURL, connectionParams.authToken);
      } else {
        throw new Error('Auth Token not available');
      }
    } catch(e) {
      authPayload = {};
    }
    return {
      ...operationParams,
      context: {
        authentication: authPayload,
      },
    };
  };
}

new SubscriptionServer({
      execute,
      subscribe,
      schema,
      onOperation: configureDecodeTokenSocketMiddleware(appConfig.authURL),
    }, {
      server: appConfig.server,
      path: `/${appConfig.subscriptionsEndpoint}`,
    });

talvez você possa tentar reenviar esta mensagem: https://github.com/apollographql/subscriptions-transport-ws/blob/master/src/client.ts#L507

      const payload: ConnectionParams = typeof this.connectionParams === 'function' ? this.connectionParams() : this.connectionParams;
      this.sendMessage(undefined, MessageTypes.GQL_CONNECTION_INIT, payload);

Existe uma maneira recomendada de fazer isso?

assinatura este problema

Como o novo WebSocketLink ainda está criando uma instância de subscriptions-transport-ws , estou aplicando manualmente meu middleware ao cliente de Transporte de Assinatura, em vez de ao WebSocketLink.

Estou usando o mesmo processo que fiz na v1, embora pareça meio complicado não tê-lo como parte da API WebSocketLink.

// create the web socket link
const wsLink = new WebSocketLink({
  uri: 'ws://example.com',
  options: {
    reconnect: true
  }
})
// create my middleware using the applyMiddleware method from subscriptions-transport-ws
const subscriptionMiddleware = {
  applyMiddleware (options, next) {
    options.auth = { ... }
    next()
  }
}
// add the middleware to the web socket link via the Subscription Transport client
wsLink.subscriptionClient.use([subscriptionMiddleware])

Quaisquer valores que você adicionar ao objeto de opções estarão disponíveis no retorno de chamada onOperation no lado do servidor.

tambem to com esse problema
@jbaxleyiii alguma ideia?

Eu criei uma solução (? está funcionando no meu caso) algumas semanas atrás:

Um módulo GraphQLModule é importado no meu app.module.ts :

import { HttpClientModule } from '@angular/common/http';
import { NgModule } from '@angular/core';
import { Apollo, ApolloModule } from 'apollo-angular';
import { HttpLinkModule } from 'apollo-angular-link-http';
import { InMemoryCache } from 'apollo-cache-inmemory';
import { WebSocketLink } from 'apollo-link-ws';
import { SubscriptionClient } from 'subscriptions-transport-ws';

@NgModule({
  exports: [
    ApolloModule,
    HttpClientModule,
    HttpLinkModule
  ]
})
export class GraphQLModule {
  public subscriptionClient: SubscriptionClient = null;

  constructor(apollo: Apollo) {
    const wssEndpoint = 'wss://your-domain.com/subscriptions';
    // It's required to define connectionParams as function, as otherwise the updated token is not transferred
    const connectionParams = () => {
      const token = sessionStorage.getItem('token');
      return token ? { 'Authorization': 'token ' + token } : {};
    };

    const wsLink = new WebSocketLink({
      uri: wssEndpoint,
      options: {
        connectionParams: connectionParams,
        reconnect: true
      }
    });
    this.subscriptionClient = (<any>wsLink).subscriptionClient;

    const cache = new InMemoryCache({});

    apollo.create({
      link: wsLink,
      cache: cache.restore(window[ '__APOLLO_CLIENT__' ]),
      connectToDevTools: true,
      queryDeduplication: true
    });
  }
}

No meu serviço de autenticação, estou injetando este módulo no construtor via private graphQLModule: GraphQLModule .
Após o login ou logout estou fazendo:

const client = this.graphQLModule.subscriptionClient;
// Close actual connection
client.close(true, true);
// todo: set/unset session token in sessionStorage
// Connect again
(<any>client).connect();

Isso abre o cliente de assinatura no início do aplicativo e o reinicia completamente no login/logout.

Idealmente, isso também suportaria uma função de retorno de promessa, portanto, se tivermos que buscar o token de forma assíncrona, podemos.

@clayne11 Não tenho certeza se isso acontece. Estou tentando usar o AsyncStorage do React-Native como connectionParams. Mas por causa do assíncrono que adicionei, ele continua enviando indefinido para o servidor.
Eu tenho um problema com isso?

ATUALIZAR:
Existe um Pull Request que aborda isso, mas ainda não foi mesclado no branch master

@jonathanheilmann , sua solução é semelhante à solução alternativa original @helios1138 . No entanto, a API connect() para o cliente de assinatura é marcada como privada e, idealmente, não deve ser usada.

Tentei fazer um subscriptionClient.close(false, false) que é uma API pública e deveria ter forçado uma reconexão, e funciona, mas a assinatura ainda não funciona. A única coisa que está funcionando agora é:

subscriptionClient.close();
subscriptionClient.connect(); // this is a private API :-(

Alguma ideia nova e limpa sobre isso? Adoraria ter uma solução aqui. Eu crio meu link apollo no carregamento, mas uma vez que meu usuário faz login, o id realmente gostaria de atualizar o token que está sendo enviado no contexto.

Esta é a melhor solução que conseguimos: https://github.com/apollographql/apollo-server/issues/1505.

O context é criado em cada operação e simplesmente procuramos o usuário dinamicamente.

alguém poderia confirmar que uma vez que o canal websocket foi aberto (com Authorization header = token AAA), cada solicitação subsequente usando o link websocket sempre será identificada como token AAA.

Ou existe uma maneira de enviar um cabeçalho de autorização diferente em cada solicitação (além de reabrir outro canal ws)?

Eu gostaria de entender o que está acontecendo em um protocolo de baixo nível para ws.

Obrigado por sua resposta!

aqui está meu código até agora (funcionando corretamente com um token):

const wsClient = new SubscriptionClient(
  graphqlEndpoint,
  {
    reconnect: true,
    connectionParams: () => ({
      headers: {
        'Authorization': 'mytokenAAA',
      },
    }),
  },
  ws,
);
const link = new WebSocketLink(wsClient);

makePromise(execute(link, options)); // that's using token AAA
// how to make another query (execute) using token BBB without creating another link ?

@sulliwane @RyannGalea @jonathanheilmann @helios1138 Para atualizar dinamicamente os tokens/parâmetros de conexão, você deve configurá-lo usando o middleware do SubscriptionClient. Veja minha essência aqui: https://gist.github.com/cowlicks/71e766164647f224bf15f086ea34fa52

const subscriptionMiddleware = {
  applyMiddleware: function(options, next) {
    // Get the current context
    const context = options.getContext();
    // set it on the `options` which will be passed to the websocket with Apollo 
    // Server it becomes: `ApolloServer({contetx: ({payload}) => (returns options)
    options.authorization = context.authorization;
    next()
  },
};

const server = new ApolloServer({
  context: ({connection, payload, req}) => {
    // whatever you return here can be accessed from the middleware of the SubscriptionMiddleware with
    // applyMiddleware: (options, next) => options.getContext()
    return {authorization: payload.authorization};
  },
});

const link = new WebSocketLink({
    uri: WS_URL,
    webSocketImpl: WebSocket,
    options: {
        reconnect: true,
    }
});

link.subscriptionClient.use([subscriptionMiddleware]);

A única solução que encontrei é fechar manualmente o cliente...

Eu postei uma solução há 1,5 anos: apollographql/apollo-server#1505. AFAIK isso ainda funciona.

@clayne11 sua solução implica usar um middleware. Acho ótimo, mas trabalho com Angular e Yoga-Graphql e por motivo desconhecido não consigo enviar token pelo contexto.

Existe uma maneira de acessar o token em um middleware no servidor?
Estou usando o Yoga e é assim que acesso o token no contexto

const server = new GraphQLServer({
  typeDefs: './src/schema.graphql',
  middlewares: [permissions],
  resolvers,
  context: ({  connection, ...request }) => {
    if(connection) {
          wsToken: connection.context.authToken
      }
    }
    return ({...request, prisma });
  }
});


const options = {
  port: process.env.PORT || 4000,
  tracing: true,
  subscriptions: {
    path: "/",
    onConnect: async (connectionParams, webSocket) => {
     if (connectionParams.authToken) {
     // I can access this through context
       return {
         authToken: connectionParams.authToken
       };
     }
    }
  }
};


server.express.use(async (req, res, next) => {
  /*
     I want to access authToken here
  */
  next();
});


server.start(options, ({port}) => {
  console.log(`Server is runnning on http://localhost:${port}`);
});

Sem fechar o soquete, recebo vários erros de rede do apollo (mesmo que ele se corrija, mostramos esses erros em um monte de notificações de brinde tão menos do que o ideal), então tenho que fechar o websocket depois de atualizar o token, então reconnect: true certifique-se de que ele se reconecta... exceto que ele se reconecta em um loop infinito, mesmo que o soquete seja fechado apenas uma vez 😔

Edit: Corrigido chamando SubscriptionClient.client.close() em vez de SubscriptionClient.close() .... 🤔

Existe um exemplo completo de cliente e servidor de como se reconectar com um novo token em vez de 100 semi-soluções espalhadas com 5 e 23 curtidas?

Eu instalo assinaturas assim apolloServer.installSubscriptionHandlers(server);
e eu crio o ApolloServer
const apolloServer = new ApolloServer({ schema, introspection: true, subscriptions: { onConnect: (connectionParams, webSocket, context) => { }, onDisconnect: (webSocket, context) => {}, ...there is no OnOperation: () => {}!!! },

Existe alguma outra maneira de adicionar onOperation? Ou tenho que criar manualmente o SubscriptionServer e não usar apolloServer.installSubscriptionHandlers(server) ?

Acabei gerando o cliente dinamicamente. Se o usuário não estiver logado, o cliente não cria o link new WebSocket , caso contrário, cria. A função generateApolloClient abaixo expõe o SubscriptionClient também para que eu possa adicionar chamadas de despacho quando ele estiver conectado (por exemplo, diga ao redux store que o websocket está conectado).

/**
 * Http link
 */
const httpLink = new HttpLink({
  uri: '/graphql'
});

/**
 * Perform actions before each http request
 */
const middlewareLink = new ApolloLink((operation, forward) => {
  operation.setContext({
    headers: {
      authorization: localStorage.getItem('token')
    }
  });
  return forward(operation);
});

const cache = new InMemoryCache();

type ApolloSubscriptionClient<TLoggedIn extends boolean> = TLoggedIn extends true ? SubscriptionClient : undefined;

/**
 * Function to create our apollo client. After login we want to add subscriptions
 * with websocket, so we'll need to recreate the entire client depending on the
 * user being logged in or logged out.
 */
export const generateApolloClient = <TLoggedIn extends boolean>(
  loggedIn: TLoggedIn
): [ApolloClient<NormalizedCacheObject>, ApolloSubscriptionClient<TLoggedIn>] => {
  let link = middlewareLink.concat(httpLink);

  // Only apply our subscription client when the user is logged in
  let subscriptionClient: SubscriptionClient | undefined;
  if (loggedIn) {
    subscriptionClient = new SubscriptionClient('ws://localhost:4001/graphql/ws', {
      reconnect: true,
      connectionParams: () => ({ authorization: localStorage.getItem('token') })
    });

    const wsLink = new WebSocketLink(subscriptionClient);

    link = split(
      ({ query }) => {
        const definition = getMainDefinition(query);
        return (
          definition.kind === 'OperationDefinition' &&
          definition.operation === 'subscription'
        );
      },
      wsLink,
      link
    );
  }

  const apolloClient = new ApolloClient({ link, cache });

  return [apolloClient, subscriptionClient as ApolloSubscriptionClient<TLoggedIn>];
};

Aqui está o código no componente ApolloProvider que usa a função generateApolloClient :

const GraphqlProvider: React.FC = ({ children }) => {
  const dispatch = useAppDispatch();
  const loggedIn = useUserLoggedIn();
  const [client, setClient] = useState(generateApolloClient(false)[0]);

  useEffect(() => {
    if (loggedIn) {
      const [apolloClient, subscriptionClient] = generateApolloClient(loggedIn);
      subscriptionClient.onConnected(() => {
        dispatch(setSubscriptionConnected(true));
      })
      setClient(apolloClient);
    }
  }, [dispatch, loggedIn]);

  return <ApolloProvider client={client}>{children}</ApolloProvider>;
};

Isso está funcionando bem e vejo um novo token no servidor toda vez que um usuário faz logout e volta novamente, mas parece um pouco de trabalho extra. Pensamentos?

Com base na resposta @dorsett85, pode ser uma boa ideia usar a API de contexto de reação para atualizar o cliente para aplicativos React/NextJS.

----------- API de contexto

export const ApolloClientContext = createContext(undefined);
export const ApolloClientContextProvider: React.FC<{}> = ({ children }) => {
  const [apolloClient, setApolloClient] = useState(client);

  return (
    <ApolloClientContext.Provider value={[apolloClient, setApolloClient]}>{children}</ApolloClientContext.Provider>
  );
};

--------- Provedor Apollo

   <ApolloClientContextProvider>
      <ApolloClientContext.Consumer>
        {([apolloClient, setApolloClient]) => {
          return (
            <ApolloProvider client={apolloClient}>
              <Component {...pageProps} />
            </ApolloProvider>
          );
        }}
      </ApolloClientContext.Consumer>
    </ApolloClientContextProvider>

-------- função util gerando novo cliente com autenticação

export const generateApolloClientWithAuth = (token): ApolloClient<any> => {
 const httpLink = createHttpLink({
  uri: 'http://localhost:5000/graphql',
  fetch,
  credentials: 'include'
});
  const wsLink = process.browser
    ? new WebSocketLink({
        uri: `ws://localhost:5000/graphql`,
        options: {
          reconnect: true,
          connectionParams() {
            console.log('-token', token);
            return {
              authorization: token ? `Bearer ${token}` : ''
            };
          }
        }
      })
    : null;
  const splitLink = process.browser
    ? split(
        ({ query }) => {
          const definition = getMainDefinition(query);
          return definition.kind === 'OperationDefinition' && definition.operation === 'subscription';
        },
        wsLink,
        httpLink
      )
    : httpLink;
  return new ApolloClient({
    ssrMode: true,
    link: splitLink,
    connectToDevTools: process.env.NODE_ENV === 'development',
    cache: new InMemoryCache().restore({})
  });
};

----------componente de login

const [apolloClient, setApolloClient] = useContext(ApolloClientContext);

----------- no login
setApolloClient(generateApolloClientWithAuth(token));

Observação importante: o código do cliente Apollo fornecido usa SSR

@kamilkisiela Podemos apenas tornar o membro wsLink.subscriptionClient um membro público? Não é provável que precise mudar, não é?

use get para obter o último valor

const wsLink = new WebSocketLink({
  uri: CLIENT_CONFIG.websocketUrl,
  options: {
    lazy: true,
    reconnect: true,
    connectionParams: {
      get authorization() {
        return user.getAuthorization();
      },
    },
    connectionCallback: (error) => {
      //@ts-ignore
      if (error?.message === "Authentication Failure!") {
        //@ts-ignore
        //wsLink.subscriptionClient.close(false, false);
      }
    },
  },
});

Alguém acreditaria em mim se eu dissesse que isso funciona:

const wsLink = process.browser
    ? new WebSocketLink({
        uri: webSocketUri,
        options: {
          reconnect: true,
          connectionParams: () => ({
            token: cookie.get('token'),
          }),
        },
      })
    : null;

ou seja
Apenas mude:

connectionParams: {
            token: cookie.get('token'),
},

PARA:

connectionParams: () => ({
            token: cookie.get('token'),
}),
Esta página foi útil?
0 / 5 - 0 avaliações

Questões relacionadas

vjpr picture vjpr  ·  3Comentários

j3ddesign picture j3ddesign  ·  3Comentários

NicholasLYang picture NicholasLYang  ·  4Comentários

ChenRoth picture ChenRoth  ·  4Comentários

lobosan picture lobosan  ·  3Comentários