<p>apollo-link-ws: ¿cómo cambiar los parámetros de conexión (y el token) después de la configuración inicial?</p>

Creado en 31 oct. 2017  ·  28Comentarios  ·  Fuente: apollographql/apollo-link

Los documentos describen la autenticación en websockets proporcionando el token en connectionParams al crear WsLink inicialmente.

Pero no está claro cómo cambiar el token más adelante y reiniciar la conexión si el usuario cierra sesión y vuelve a iniciar sesión (posiblemente con un token diferente)

Me las arreglé para lograr el resultado deseado como este:

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

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

pero se siente un poco extraño, ya que wsLink.subscriptionClient está marcado como privado y no se supone que sea accesible desde el exterior

enhancement

Comentario más útil

Dado que el nuevo WebSocketLink todavía está creando una instancia de subscriptions-transport-ws , estoy aplicando manualmente mi middleware al cliente de transporte de suscripción, en lugar de a WebSocketLink.

Estoy usando el mismo proceso que usé en v1, aunque se siente un poco extraño no tenerlo como parte de la API de 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])

Cualquier valor que agregue al objeto de opciones está disponible en la devolución de llamada onOperation en el lado del servidor.

Todos 28 comentarios

Puede usar una función que se resuelva en un objeto en los parámetros de conexión.

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

EDITAR:

Oh, espera, esto no se llama después de la primera vez... ¿Quizás concatenar con otro middleware que pone un objeto de autenticación en la respuesta que puedes analizar con onOperation del lado del servidor?

Esto es lo que tengo para ti, inspirado en la forma en que lo estaba haciendo con ApolloClient V1

Agregue un nuevo middleware de enlace para adjuntar el token de autenticación a la 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);

Luego, en el lado del servidor, puede verificarlo y aplicarlo al contexto usando el 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}`,
    });

tal vez, puede intentar reenviar este mensaje: 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);

¿Hay una forma recomendada de hacer esto?

suscripción este problema

Dado que el nuevo WebSocketLink todavía está creando una instancia de subscriptions-transport-ws , estoy aplicando manualmente mi middleware al cliente de transporte de suscripción, en lugar de a WebSocketLink.

Estoy usando el mismo proceso que usé en v1, aunque se siente un poco extraño no tenerlo como parte de la API de 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])

Cualquier valor que agregue al objeto de opciones está disponible en la devolución de llamada onOperation en el lado del servidor.

también teniendo este problema
@jbaxleyiii alguna idea?

Creé una solución (? Está funcionando en mi caso) hace unas semanas:

Se importa un módulo GraphQLModule en mi 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
    });
  }
}

En mi servicio de autenticación, estoy inyectando este módulo en el constructor a través private graphQLModule: GraphQLModule .
Después de iniciar sesión o cerrar sesión, estoy haciendo:

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

Esto abre el cliente de suscripción al inicio de la aplicación y lo reinicia por completo al iniciar/cerrar sesión.

Idealmente, esto también admitiría una función de devolución de promesa, por lo que si tenemos que buscar el token de forma asíncrona, podemos hacerlo.

@ Clayne11 No estoy seguro de que lo haga. Estoy tratando de usar AsyncStorage de React-Native como los parámetros de conexión. Pero debido a la sincronización que agregué, sigue enviando indefinidos al servidor.
¿Tengo un problema con esto?

ACTUALIZAR:
Hay una solicitud de extracción que aborda esto pero aún no se fusionó con la rama principal

@ jonathanheilmann , su solución es similar a la solución original de @ helios1138 . Sin embargo, la API de connect() para el cliente de suscripción está marcada como privada e idealmente no debe usarse.

Intenté hacer subscriptionClient.close(false, false) que es una API pública y debería haber forzado una reconexión, y lo hace, pero la suscripción sigue sin funcionar. Lo único que está funcionando en este momento es:

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

¿Alguna idea limpia y nueva sobre esto? Me encantaría tener una solución aquí. Creo mi enlace apolo en la carga, pero una vez que mi usuario inicia sesión, realmente me gusta actualizar el token que se envía en el contexto.

Esta es la mejor solución que hemos podido encontrar: https://github.com/apollographql/apollo-server/issues/1505.

El context se crea en cada operación y simplemente buscamos al usuario dinámicamente.

alguien podría confirmar que una vez que se ha abierto el canal websocket (con encabezado de autorización = token AAA), cada solicitud posterior que utilice el enlace websocket siempre se identificará como token AAA.

¿O hay alguna forma de enviar un encabezado de autorización diferente en cada solicitud (aparte de volver a abrir otro canal ws)?

Me gustaría entender qué sucede en un protocolo de bajo nivel para ws.

¡Gracias por tu respuesta!

aquí está mi código hasta ahora (funciona correctamente con un 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 actualizar dinámicamente tokens/parámetros de conexión, debe configurarlo usando el middleware de SubscriptionClient. Vea mi esencia aquí: 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 única solución que encontré es cerrar manualmente el cliente...

Publiqué una solución hace 1,5 años: apollographql/apollo-server#1505. AFAIK esto todavía funciona.

@clayne11 su solución implica usar un middleware. Creo que es genial, pero trabajo con Angular y Yoga-Graphql y, por razones desconocidas, no puedo enviar el token a través del contexto.

¿Hay alguna forma de acceder al token en un middleware en el servidor?
Estoy usando Yoga y así es como accedo al token en el 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}`);
});

Sin cerrar el socket, recibo un montón de errores de red de apollo (aunque se corrige solo, mostramos esos errores en un montón de notificaciones de brindis, por lo que no es ideal), así que tengo que cerrar el websocket después de actualizar el token, luego reconnect: true asegúrese de que se vuelva a conectar... excepto que se vuelve a conectar en un bucle infinito aunque el enchufe solo se cierre una vez 😔

Editar: se solucionó llamando a SubscriptionClient.client.close() en lugar de SubscriptionClient.close() .... 🤔

¿Hay un ejemplo completo de cliente y servidor sobre cómo volver a conectarse con un token nuevo en lugar de 100 semisoluciones dispersas con 5 y 23 me gusta?

Instalo suscripciones como esta apolloServer.installSubscriptionHandlers(server);
y creo el ApolloServer
const apolloServer = new ApolloServer({ schema, introspection: true, subscriptions: { onConnect: (connectionParams, webSocket, context) => { }, onDisconnect: (webSocket, context) => {}, ...there is no OnOperation: () => {}!!! },

¿Hay alguna otra forma de agregar onOperation? ¿O tengo que crear manualmente SubscriptionServer y no usar apolloServer.installSubscriptionHandlers(server) ?

Terminé generando dinámicamente el cliente. Si el usuario no está logueado, el cliente no crea el enlace new WebSocket , de lo contrario lo hace. La función generateApolloClient a continuación también expone el SubscriptionClient para que pueda agregar llamadas de despacho cuando se haya conectado (por ejemplo, decirle a la tienda redux que el 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>];
};

Aquí está el código en el componente ApolloProvider que usa la función 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>;
};

Esto está funcionando bien y veo un nuevo token en el servidor cada vez que un usuario cierra la sesión y vuelve a iniciarla, pero parece un poco de trabajo adicional. ¿Pensamientos?

Basado en la respuesta de @dorsett85 Puede ser una buena idea usar la API de contexto de reacción para actualizar el cliente para las aplicaciones 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>
  );
};

--------- Proveedor Apolo

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

-------- función util generando nuevo Cliente con auth

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 inicio de sesión

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

----------- al iniciar sesión
setApolloClient(generateApolloClientWithAuth(token));

Nota importante: siempre que el código del cliente Apollo use SSR

@kamilkisiela ¿Podemos hacer que el miembro wsLink.subscriptionClient sea ​​un miembro público? No es probable que tenga que cambiar, ¿verdad?

use get para obtener el ú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);
      }
    },
  },
});

¿Alguien me creería si digo que esto funciona?

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

es decir
Solo cambia:

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

A:

connectionParams: () => ({
            token: cookie.get('token'),
}),
¿Fue útil esta página
0 / 5 - 0 calificaciones