<p>apollo-link-ws: как изменить параметры подключения (и токен) после первоначальной настройки?</p>

Созданный на 31 окт. 2017  ·  28Комментарии  ·  Источник: apollographql/apollo-link

Документы описывают аутентификацию в веб-сокетах как предоставление токена в connectionParams при первоначальном создании WsLink.

Но неясно, как изменить токен позже и повторно инициализировать соединение, если пользователь выходит из системы и снова входит в систему (возможно, с другим токеном)

Мне удалось добиться желаемого результата следующим образом:

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

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

но это выглядит немного хакерским, поскольку wsLink.subscriptionClient помечен как частный и не должен быть доступен извне.

enhancement

Самый полезный комментарий

Поскольку новый WebSocketLink по-прежнему создает экземпляр subscriptions-transport-ws , я вручную применяю промежуточное ПО к клиенту Subscription Transport, а не к WebSocketLink.

Я использую тот же процесс, что и в v1, хотя мне кажется, что не использовать его как часть 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])

Любые значения, которые вы добавляете в объект параметров, доступны в onOperation на стороне сервера.

Все 28 Комментарий

Вы можете использовать функцию, которая разрешается в объект в connectionParams.

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

РЕДАКТИРОВАТЬ:

О, подождите, это не вызывается после первого раза ... Может быть, concat с другим промежуточным программным обеспечением, которое помещает объект аутентификации в ответ, который вы можете проанализировать на стороне сервера onOperation ?

Вот что у меня есть для вас, по образцу того, как я делал это с помощью ApolloClient V1.

Добавьте новое промежуточное программное обеспечение ссылки, чтобы прикрепить токен аутентификации к полезной нагрузке.

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

Затем на стороне сервера вы можете проверить это и применить к контексту, используя хук 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}`,
    });

возможно, вы можете попробовать повторно отправить это сообщение: 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);

Есть ли рекомендуемый способ сделать это?

подписка на этот выпуск

Поскольку новый WebSocketLink по-прежнему создает экземпляр subscriptions-transport-ws , я вручную применяю промежуточное ПО к клиенту Subscription Transport, а не к WebSocketLink.

Я использую тот же процесс, что и в v1, хотя мне кажется, что не использовать его как часть 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])

Любые значения, которые вы добавляете в объект параметров, доступны в onOperation на стороне сервера.

тоже есть эта проблема
@jbaxleyiii есть мысли?

Я создал решение (? в моем случае оно работает) несколько недель назад:

Модуль GraphQLModule импортирован в мой 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
    });
  }
}

В моей службе аутентификации я вставляю этот модуль в конструктор через private graphQLModule: GraphQLModule .
После входа или выхода я делаю:

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

Это открывает клиент подписки при запуске приложения и полностью перезапускает его при входе/выходе.

В идеале это также должно поддерживать функцию возврата обещания, поэтому, если нам нужно асинхронно получить токен, мы можем это сделать.

@clayne11 Не уверен, что это так. Я пытаюсь использовать AsyncStorage из React-Native в качестве параметра connectionParams. Но из-за асинхронности, которую я добавил, он продолжает отправлять undefined на сервер.
У меня проблема с этим?

ОБНОВИТЬ:
Есть запрос на слияние, который решает эту проблему, но еще не объединен с основной веткой.

@jonathanheilmann , ваше решение похоже на оригинальный обходной путь @helios1138 . Однако API connect() для клиента подписки помечен как частный и в идеале не должен использоваться.

Я попытался сделать subscriptionClient.close(false, false) , который является общедоступным API и должен был принудительно переподключиться, и это происходит, но подписка по-прежнему не работает. Единственное, что сейчас работает:

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

Любые чистые новые идеи по этому поводу? Люблю иметь решение здесь. Я создаю свою ссылку на аполлон при загрузке, но как только мой пользователь входит в систему, мне действительно нравится обновлять токен, отправляемый в контексте.

Это лучшее решение, которое мы смогли придумать: https://github.com/apollographql/apollo-server/issues/1505.

context создается при каждой операции, и мы просто динамически ищем пользователя.

может ли кто-нибудь подтвердить, что после открытия канала веб-сокета (с заголовком авторизации = токен AAA) каждый последующий запрос с использованием ссылки на веб-сокет всегда будет идентифицироваться как токен AAA.

Или есть способ отправить другой заголовок авторизации для каждого запроса (кроме повторного открытия другого канала ws)?

Я хотел бы понять, что происходит в низкоуровневом протоколе для ws.

Спасибо за ответ!

вот мой код (правильно работает с одним токеном):

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 Для динамического обновления токенов/параметров подключения вы должны установить его с помощью промежуточного программного обеспечения SubscriptionClient. Смотрите мою суть здесь: 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]);

Единственное решение, которое я нашел, это вручную закрыть клиент...

Я опубликовал решение 1,5 года назад: apollographql/apollo-server#1505. AFAIK это все еще работает.

@clayne11 ваше решение подразумевает использование промежуточного программного обеспечения. Я думаю, это здорово, но я работаю с Angular и Yoga-Graphql и по неизвестной причине не могу отправить токен через контекст.

Есть ли способ получить доступ к токену в промежуточном программном обеспечении на сервере?
Я использую йогу, и вот как я получаю доступ к токену в контексте

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

Не закрывая сокет, я получаю кучу сетевых ошибок аполлона (хотя он исправляется сам, мы показываем эти ошибки в куче всплывающих уведомлений, так что это не идеально), поэтому я должен закрыть веб-сокет после обновления токена, затем reconnect: true убедитесь, что он переподключается... за исключением того, что он переподключается в бесконечном цикле, даже если сокет закрывается только один раз 😔

Редактировать: исправлено вызовом SubscriptionClient.client.close() вместо SubscriptionClient.close() .... 🤔

Есть ли один полный пример клиента и сервера, как переподключиться с новым токеном вместо 100 разрозненных полурешений с 5 и 23 лайками?

Я устанавливаю такие подписки apolloServer.installSubscriptionHandlers(server);
и я создаю ApolloServer
const apolloServer = new ApolloServer({ schema, introspection: true, subscriptions: { onConnect: (connectionParams, webSocket, context) => { }, onDisconnect: (webSocket, context) => {}, ...there is no OnOperation: () => {}!!! },

Есть ли другой способ добавить onOperation? Или мне нужно вручную создать SubscriptionServer и не использовать apolloServer.installSubscriptionHandlers(server) ?

В итоге я динамически генерировал клиента. Если пользователь не авторизован, клиент не создает ссылку new WebSocket , в противном случае создает. Приведенная ниже функция generateApolloClient также предоставляет доступ к SubscriptionClient , поэтому я могу добавлять диспетчерские вызовы, когда они подключены (например, сообщать магазину избыточности, что веб-сокет подключен).

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

Вот код в компоненте ApolloProvider , который использует функцию 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>;
};

Это работает хорошо, и я вижу новый токен на сервере каждый раз, когда пользователь выходит из системы и снова входит в нее, но это похоже на дополнительную работу. Мысли?

На основе ответа @dorsett85 . Может быть хорошей идеей использовать API контекста реакции для обновления клиента для приложений React/NextJS.

----------- Контекстный 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>
  );
};

--------- Провайдер Аполлона

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

-------- функция использования, генерирующая нового клиента с авторизацией

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

----------компонент входа

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

----------- при входе
setApolloClient(generateApolloClientWithAuth(token));

Важное примечание: предоставленный код клиента Apollo использует SSR

@kamilkisiela Можем ли мы просто сделать участника wsLink.subscriptionClient общедоступным? Вряд ли его нужно будет менять, не так ли?

используйте get , чтобы получить последнее значение

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

Кто-нибудь поверит мне, если я скажу, что это работает:

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

то есть
Просто измените:

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

К:

connectionParams: () => ({
            token: cookie.get('token'),
}),
Была ли эта страница полезной?
0 / 5 - 0 рейтинги