<p>apollo-link-ws : comment modifier les paramètres de connexion (et le jeton) après la configuration initiale ?</p>

Créé le 31 oct. 2017  ·  28Commentaires  ·  Source: apollographql/apollo-link

La documentation décrit l'authentification sur les Websockets comme fournissant le jeton dans connectionParams lors de la création initiale du WsLink.

Mais on ne sait pas comment changer le token plus tard et réinitialiser la connexion si l'utilisateur se déconnecte et se reconnecte (éventuellement avec un token différent)

J'ai réussi à obtenir le résultat souhaité comme ceci :

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

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

mais cela semble un peu bidon, car wsLink.subscriptionClient est marqué comme privé et n'est pas censé être accessible de l'extérieur

enhancement

Commentaire le plus utile

Étant donné que le nouveau WebSocketLink crée toujours une instance de subscriptions-transport-ws , j'applique manuellement mon middleware au client Subscription Transport, plutôt qu'au WebSocketLink.

J'utilise le même processus que celui que j'ai utilisé dans la v1, bien que cela me semble un peu difficile de ne pas l'avoir dans le cadre de l'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])

Toutes les valeurs que vous ajoutez à l'objet options sont disponibles dans le rappel onOperation côté serveur.

Tous les 28 commentaires

Vous pouvez utiliser une fonction qui se résout en un objet dans connectionParams.

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

ÉDITER:

Oh attendez, ce n'est pas appelé après la première fois... Peut-être concat avec un autre middleware qui met un objet d'authentification sur la réponse que vous pouvez analyser avec onOperation côté serveur ?

Voici ce que j'ai pour vous, inspiré de la façon dont je le faisais avec ApolloClient V1

Ajouter un nouveau middleware de lien pour attacher le jeton d'authentification à la charge utile

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);

Ensuite, côté serveur, vous pouvez le vérifier et l'appliquer au contexte à l'aide du crochet 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}`,
    });

peut-être, vous pouvez essayer de renvoyer ce message : 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-t-il une méthode recommandée pour le faire ?

abonnement ce numéro

Étant donné que le nouveau WebSocketLink crée toujours une instance de subscriptions-transport-ws , j'applique manuellement mon middleware au client Subscription Transport, plutôt qu'au WebSocketLink.

J'utilise le même processus que celui que j'ai utilisé dans la v1, bien que cela me semble un peu difficile de ne pas l'avoir dans le cadre de l'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])

Toutes les valeurs que vous ajoutez à l'objet options sont disponibles dans le rappel onOperation côté serveur.

avoir aussi ce problème
@jbaxleyiii des idées ?

J'ai créé une solution (? ça marche dans mon cas) il y a quelques semaines :

Un module GraphQLModule est importé dans mon 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
    });
  }
}

Dans mon service d'authentification, j'injecte ce module dans le constructeur via private graphQLModule: GraphQLModule .
Après connexion ou déconnexion, je fais :

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

Cela ouvre le client d'abonnement au démarrage de l'application et le réinitialise complètement lors de la connexion/déconnexion.

Idéalement, cela prendrait également en charge une fonction de retour de promesse, donc si nous devons récupérer le jeton de manière asynchrone, nous le pouvons.

@clayne11 Je ne suis pas sûr que ce soit le cas. J'essaie d'utiliser AsyncStorage de React-Native comme connectionParams. Mais à cause de l'async que j'ai ajouté, il continue d'envoyer undefined au serveur.
J'ai un problème avec ça ?

METTRE À JOUR:
Il y a une Pull Request qui répond à ce problème mais pas encore fusionné dans la branche master

@jonathanheilmann , votre solution est similaire à la solution de contournement d'origine @ helios1138 . Cependant, l'API connect() pour le client d'abonnement est marquée comme privée et ne devrait idéalement pas être utilisée.

J'ai essayé de faire un subscriptionClient.close(false, false) qui est une API publique et aurait dû forcer une reconnexion, et c'est le cas, mais l'abonnement ne fonctionne toujours pas. La seule chose qui fonctionne actuellement c'est :

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

De nouvelles idées claires à ce sujet ? J'adore avoir une solution ici. Je crée mon lien apollo au chargement, mais une fois que mon utilisateur se connecte, j'aime vraiment mettre à jour le jeton envoyé dans le contexte.

C'est la meilleure solution que nous ayons trouvée : https://github.com/apollographql/apollo-server/issues/1505.

Le context est créé à chaque opération et nous recherchons simplement l'utilisateur dynamiquement.

Quelqu'un pourrait-il confirmer qu'une fois le canal websocket ouvert (avec en-tête d'autorisation = token AAA), chaque demande ultérieure utilisant le lien websocket sera toujours identifiée comme token AAA.

Ou existe-t-il un moyen d'envoyer un en-tête d'autorisation différent à chaque demande (autre que la réouverture d'un autre canal ws) ?

J'aimerais comprendre ce qui se passe sur un protocole de bas niveau pour ws.

Merci pour votre réponse !

voici mon code jusqu'à présent (fonctionnant correctement avec un jeton):

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 Pour mettre à jour dynamiquement les jetons/paramètres de connexion, vous devez le définir à l'aide du middleware de SubscriptionClient. Voir mon résumé ici : 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]);

La seule solution que j'ai trouvée est de fermer manuellement le client...

J'ai posté une solution il y a 1,5 ans : apollographeql/apollo-server#1505. AFAIK cela fonctionne toujours.

@clayne11 votre solution implique d'utiliser un middleware. Je pense que c'est génial mais je travaille avec Angular et Yoga-Graphql et pour une raison inconnue, je ne peux pas envoyer de jeton par contexte.

Existe-t-il un moyen d'accéder au jeton dans un middleware du serveur ?
J'utilise Yoga et voici comment j'accède au jeton dans le contexte

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}`);
});

Sans fermer le socket, j'obtiens un tas d'erreurs de réseau apollo (même s'il se corrige lui-même, nous affichons ces erreurs dans un tas de notifications toast donc moins qu'idéal) donc je dois fermer le websocket après la mise à jour du jeton, puis reconnect: true assurez-vous qu'il se reconnecte... sauf qu'il se reconnecte dans une boucle infinie même si la socket n'est fermée qu'une seule fois 😔

Edit : Corrigé en appelant SubscriptionClient.client.close() au lieu de SubscriptionClient.close() .... 🤔

Existe-t-il un exemple complet de client et de serveur sur la façon de se reconnecter avec un nouveau jeton au lieu de 100 semi-solutions dispersées avec 5 et 23 likes ?

J'installe des abonnements comme celui-ci apolloServer.installSubscriptionHandlers(server);
et je crée l'ApolloServer
const apolloServer = new ApolloServer({ schema, introspection: true, subscriptions: { onConnect: (connectionParams, webSocket, context) => { }, onDisconnect: (webSocket, context) => {}, ...there is no OnOperation: () => {}!!! },

Existe-t-il un autre moyen d'ajouter onOperation ? Ou je dois créer manuellement SubscriptionServer et n'utilise pas apolloServer.installSubscriptionHandlers(server) ?

J'ai fini par générer dynamiquement le client. Si l'utilisateur n'est pas connecté, le client ne crée pas le lien new WebSocket , sinon il le fait. La fonction generateApolloClient ci-dessous expose également le SubscriptionClient afin que je puisse ajouter des appels de répartition lorsqu'il est connecté (par exemple, dire à redux store que le websocket est connecté).

/**
 * 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>];
};

Voici le code du composant ApolloProvider qui utilise la fonction 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>;
};

Cela fonctionne bien et je vois un nouveau jeton sur le serveur chaque fois qu'un utilisateur se déconnecte et se reconnecte, mais cela semble être un peu de travail supplémentaire. Les pensées?

Basé sur la réponse @dorsett85 Il peut être judicieux d'utiliser l'API de contexte de réaction pour actualiser le client pour les applications React/NextJS.

----------- API contextuelle

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>
  );
};

--------- Fournisseur Apollo

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

-------- fonction util générant un nouveau client avec authentification

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({})
  });
};

----------composant de connexion

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

----------- à la connexion
setApolloClient(generateApolloClientWithAuth(token));

Remarque importante : à condition que le code du client Apollo utilise SSR

@kamilkisiela Pouvons-nous simplement faire du membre wsLink.subscriptionClient un membre public ? Il est peu probable que cela doive changer, n'est-ce pas ?

utilisez get pour obtenir la dernière valeur

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);
      }
    },
  },
});

Est-ce que quelqu'un me croirait si je dis que ça marche :

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

c'est à dire
Changez simplement :

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

À:

connectionParams: () => ({
            token: cookie.get('token'),
}),
Cette page vous a été utile?
0 / 5 - 0 notes