<p>apollo-link-ws: Wie ändere ich die Verbindungsparameter (und das Token) nach der Ersteinrichtung?</p>

Erstellt am 31. Okt. 2017  ·  28Kommentare  ·  Quelle: apollographql/apollo-link

Die Dokumentation beschreibt die Authentifizierung auf Websockets als Bereitstellung des Tokens in connectionParams beim anfänglichen Erstellen des WsLink.

Es ist jedoch unklar, wie man das Token später ändert und die Verbindung neu initialisiert, wenn sich der Benutzer abmeldet und wieder anmeldet (evtl. mit einem anderen Token)

Ich habe es geschafft, das gewünschte Ergebnis wie folgt zu erzielen:

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

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

aber es fühlt sich irgendwie hackig an, da wsLink.subscriptionClient als privat gekennzeichnet ist und nicht von außen zugänglich sein soll

enhancement

Hilfreichster Kommentar

Da das neue WebSocketLink immer noch eine Instanz von subscriptions-transport-ws erstellt, wende ich meine Middleware manuell auf den Subscription Transport-Client und nicht auf den WebSocketLink an.

Ich verwende den gleichen Prozess wie in v1, obwohl es sich irgendwie hackig anfühlt, ihn nicht als Teil der WebSocketLink-API zu haben.

// 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])

Alle Werte, die Sie dem Optionsobjekt hinzufügen, sind im onOperation -Callback auf der Serverseite verfügbar.

Alle 28 Kommentare

Sie können in den connectionParams eine Funktion verwenden, die in ein Objekt aufgelöst wird.

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

BEARBEITEN:

Oh warte, das wird nach dem ersten Mal nicht aufgerufen ... Vielleicht mit einer anderen Middleware zusammenfügen, die ein Authentifizierungsobjekt in die Antwort einfügt, das Sie mit onOperation serverseitig analysieren können?

Hier ist, was ich für Sie habe, nach dem Vorbild von ApolloClient V1

Fügen Sie eine neue Link-Middleware hinzu, um das Authentifizierungstoken an die Nutzlast anzuhängen

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

Dann können Sie es serverseitig überprüfen und mit dem onOperation-Hook auf den Kontext anwenden

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

Vielleicht können Sie versuchen, diese Nachricht erneut zu senden: 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);

Gibt es dazu eine empfohlene Methode?

Abonnement dieses Problem

Da das neue WebSocketLink immer noch eine Instanz von subscriptions-transport-ws erstellt, wende ich meine Middleware manuell auf den Subscription Transport-Client und nicht auf den WebSocketLink an.

Ich verwende den gleichen Prozess wie in v1, obwohl es sich irgendwie hackig anfühlt, ihn nicht als Teil der WebSocketLink-API zu haben.

// 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])

Alle Werte, die Sie dem Optionsobjekt hinzufügen, sind im onOperation -Callback auf der Serverseite verfügbar.

habe auch dieses problem
@jbaxleyiii irgendwelche Gedanken?

Ich habe vor ein paar Wochen eine Lösung erstellt (in meinem Fall funktioniert es):

Ein Modul GraphQLModule wird in mein app.module.ts importiert:

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

In meinem Authentifizierungsdienst füge ich dieses Modul über private graphQLModule: GraphQLModule in den Konstruktor ein.
Nach dem Login oder Logout mache ich:

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

Dadurch wird der Abonnement-Client beim App-Start geöffnet und beim Anmelden/Abmelden vollständig neu gestartet.

Idealerweise würde dies auch eine Promise-Return-Funktion unterstützen. Wenn wir also das Token asynchron abrufen müssen, können wir dies tun.

@clayne11 Bin mir nicht sicher, ob das der Fall ist. Ich versuche, AsyncStorage von React-Native als connectionParams zu verwenden. Aber wegen der Asynchronität, die ich hinzugefügt habe, sendet es weiterhin undefiniert an den Server.
Ich habe ein Problem damit?

AKTUALISIEREN:
Es gibt einen Pull Request , der dies anspricht, aber noch nicht in den Master-Branch eingebunden ist

@jonathanheilmann , Ihre Lösung ähnelt der ursprünglichen Problemumgehung von @helios1138 . Die connect() API für den Abonnement-Client ist jedoch als privat gekennzeichnet und sollte idealerweise nicht verwendet werden.

Ich habe versucht, ein subscriptionClient.close(false, false) auszuführen, das eine öffentliche API ist und eine erneute Verbindung hätte erzwingen sollen, und das tut es, aber das Abonnement funktioniert immer noch nicht. Das einzige was gerade funktioniert ist:

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

Irgendwelche sauberen neuen Ideen dazu? Ich liebe es, hier eine Lösung zu haben. Ich erstelle meinen apollo-Link beim Laden, aber sobald sich mein Benutzer anmeldet, möchte ich das im Kontext gesendete Token wirklich aktualisieren.

Dies ist die beste Lösung, die wir finden konnten: https://github.com/apollographql/apollo-server/issues/1505.

Das context wird bei jeder Operation erstellt und wir suchen den Benutzer einfach dynamisch nach.

könnte jemand bestätigen, dass nach dem Öffnen des Websocket-Kanals (mit Autorisierungsheader = Token AAA) jede weitere Anfrage über den Websocket-Link immer als AAA-Token identifiziert wird.

Oder gibt es eine Möglichkeit, bei jeder Anfrage einen anderen Autorisierungsheader zu senden (außer beim erneuten Öffnen eines anderen ws-Kanals)?

Ich würde gerne verstehen, was in einem Low-Level-Protokoll für ws passiert.

Vielen Dank für Ihre Antwort!

hier ist mein Code bisher (funktioniert korrekt mit einem 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 Um Token/Verbindungsparameter dynamisch zu aktualisieren, sollten Sie dies mithilfe der Middleware des SubscriptionClient einstellen. Siehe mein Wesentliches hier: 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]);

Die einzige Lösung, die ich gefunden habe, besteht darin, den Client manuell zu schließen ...

Ich habe vor 1,5 Jahren eine Lösung gepostet: apollographql/apollo-server#1505. AFAIK funktioniert das immer noch.

@clayne11 Ihre Lösung impliziert die Verwendung einer Middleware. Ich finde es großartig, aber ich arbeite mit Angular und Yoga-Graphql und kann aus unbekannten Gründen kein Token durch den Kontext senden.

Gibt es eine Möglichkeit, auf das Token in einer Middleware auf dem Server zuzugreifen?
Ich benutze Yoga und so greife ich im Kontext auf den Token zu

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

Ohne den Socket zu schließen, erhalte ich eine Reihe von apollo-Netzwerkfehlern (obwohl es sich selbst korrigiert, zeigen wir diese Fehler in einer Reihe von Toast-Benachrichtigungen an, also weniger als ideal), also muss ich den Websocket nach dem Aktualisieren des Tokens schließen, dann reconnect: true Stellen Sie sicher, dass es wieder verbunden wird ... außer dass es in einer Endlosschleife wieder verbunden wird, obwohl die Steckdose nur einmal geschlossen ist 😔

Bearbeiten: Behoben durch Aufrufen von SubscriptionClient.client.close() anstelle von SubscriptionClient.close() .... 🤔

Gibt es ein vollständiges Client- und Server-Beispiel, wie man sich mit einem neuen Token wiederherstellt, anstatt 100 verstreute Halblösungen mit 5 und 23 Likes?

Ich installiere Abonnements wie dieses apolloServer.installSubscriptionHandlers(server);
und ich erstelle den ApolloServer
const apolloServer = new ApolloServer({ schema, introspection: true, subscriptions: { onConnect: (connectionParams, webSocket, context) => { }, onDisconnect: (webSocket, context) => {}, ...there is no OnOperation: () => {}!!! },

Gibt es eine andere Möglichkeit, onOperation hinzuzufügen? Oder muss ich den SubscriptionServer manuell erstellen und verwende apolloServer.installSubscriptionHandlers(server) nicht?

Am Ende habe ich den Client dynamisch generiert. Wenn der Benutzer nicht angemeldet ist, erstellt der Client den new WebSocket -Link nicht, andernfalls tut er dies. Die Funktion generateApolloClient unten macht auch die SubscriptionClient verfügbar, damit ich Dispatch-Aufrufe hinzufügen kann, wenn sie verbunden ist (zB Redux Store sagen, dass der Websocket verbunden ist).

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

Hier ist der Code in der ApolloProvider -Komponente, die die generateApolloClient -Funktion verwendet:

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

Dies funktioniert gut und ich sehe jedes Mal ein neues Token auf dem Server, wenn sich ein Benutzer ab- und wieder anmeldet, aber es scheint ein bisschen zusätzliche Arbeit zu sein. Die Gedanken?

Basierend auf der Antwort von @dorsett85 Es kann eine gute Idee sein, die React-Kontext-API zu verwenden, um den Client für React/NextJS-Apps zu aktualisieren.

------------ Kontext-API

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

--------- Apollo-Anbieter

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

-------- util-Funktion generiert neuen Client mit 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({})
  });
};

----------Anmeldekomponente

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

---------- beim Einloggen
setApolloClient(generateApolloClientWithAuth(token));

Wichtiger Hinweis: Der bereitgestellte Apollo-Client-Code verwendet SSR

@kamilkisiela Können wir das wsLink.subscriptionClient Mitglied einfach zu einem öffentlichen Mitglied machen? Es ist nicht wahrscheinlich, dass es sich ändern muss, oder?

Verwenden Sie get , um den letzten Wert zu erhalten

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

Würde mir jemand glauben, wenn ich sage, dass das funktioniert:

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

dh
Nur ändern:

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

ZU:

connectionParams: () => ({
            token: cookie.get('token'),
}),
War diese Seite hilfreich?
0 / 5 - 0 Bewertungen