<p>apollo-link-ws:初期設定後にconnectionParams(およびトークン)を変更する方法は?</p>

作成日 2017年10月31日  ·  28コメント  ·  ソース: apollographql/apollo-link

ドキュメントでは、WsLinkを最初に作成するときに、 connectionParamsでトークンを提供するものとしてWebSocketでの認証について説明しています。

ただし、ユーザーがログアウトして再度ログインした場合(おそらく別のトークンを使用して)、後でトークンを変更して接続を再初期化する方法は不明です。

私はこのような望ましい結果を達成することができました:

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のインスタンスを作成しているので、ミドルウェアをWebSocketLinkではなくSubscriptionTransportクライアントに手動で適用しています。

私はv1で行ったのと同じプロセスを使用していますが、WebSocketLinkAPIの一部としてそれを持たないのはちょっとハックな感じがします。

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

編集:

ちょっと待ってください、これは最初から呼び出されません...多分onOperationサーバー側で解析できる応答に認証オブジェクトを置く別のミドルウェアと連結しますか?

これが私があなたのために持っているもので、私がApolloClientV1でそれをやっていた方法をモデルにしています

新しいリンクミドルウェアを追加して、認証トークンをペイロードにアタッチします

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のインスタンスを作成しているので、ミドルウェアをWebSocketLinkではなくSubscriptionTransportクライアントに手動で適用しています。

私はv1で行ったのと同じプロセスを使用していますが、WebSocketLinkAPIの一部としてそれを持たないのはちょっとハックな感じがします。

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

これにより、アプリの起動時にサブスクリプションクライアントが開き、ログイン/ログアウト時に完全に再初期化されます。

理想的には、これはpromiseリターン関数もサポートするので、トークンを非同期フェッチする必要がある場合は可能です。

@ clayne11そうかどうかはわかりません。 React-NativeのAsyncStorageをconnectionParamsとして使用しようとしています。 しかし、私が追加した非同期のために、サーバーに未定義を送信し続けます。
これに問題がありますか?

アップデート:
これに対処するプルリクエストがありますが、まだマスターブランチにマージされていません

@jonathanheilmann 、あなたの解決策は@ helios1138の元の回避策に似ています。 ただし、サブスクリプションクライアントのconnect() APIはプライベートとしてマークされているため、理想的には使用しないでください。

パブリックAPIであるsubscriptionClient.close(false, false)を実行しようとしましたが、再接続を強制する必要がありましたが、サブスクリプションは機能しません。 現在機能しているのは次のとおりです。

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

これに関するクリーンな新しいアイデアはありますか? ここで解決策を見つけるのが大好きです。 ロード時にapolloリンクを作成しますが、ユーザーがIDにログインすると、コンテキストで送信されるトークンを更新したいと思います。

これは、私たちが思いついた最高のソリューションです: https://github.com/apollographql/apollo-server/issues/1505。

contextはすべての操作で作成され、ユーザーを動的に検索するだけです。

これは私に役立ちました
https://github.com/apollographql/apollo-link/issues/446#issuecomment -410073779

WebSocketチャネルが開かれると(Authorizationヘッダー=トークンAAAを使用)、WebSocketリンクを使用する後続の各リクエストが常にAAAトークンとして識別されることを誰かが確認できますか?

または、リクエストごとに異なるAuthorizationヘッダーを送信する方法はありますか(別のwsチャネルを再度開く以外)?

wsの低レベルプロトコルで何が起こっているのかを理解したいと思います。

返信ありがとうございます!

これまでの私のコードは次のとおりです(1つのトークンで正しく機能します):

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を使用しており、理由は不明ですが、コンテキストを介してトークンを送信できません。

サーバーのミドルウェアのトークンにアクセスする方法はありますか?
私はYogaを使用していますが、これがコンテキストでトークンにアクセスする方法です

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

ソケットを閉じないと、一連のapolloネットワークエラーが発生します(それ自体は修正されますが、トースト通知の束にこれらのエラーが表示されるため、理想的とは言えません)。トークンを更新した後、WebSocketを閉じてから、 reconnect: trueを閉じる必要があります。

編集: SubscriptionClient.close() SubscriptionClient.client.close()を呼び出すことで修正されました....🤔

5と23のいいねを含む100個の散在するセミソリューションの代わりに新しいトークンで再接続する方法の完全なクライアントとサーバーの例が1つありますか?

このようなサブスクリプションをインストールします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も公開するので、接続されたときにディスパッチ呼び出しを追加できます(たとえば、WebSocketが接続されていることをreduxストアに通知します)。

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

generateApolloClient ApolloProviderコンポーネントのコードは次のとおりです。

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の回答に基づくReact / NextJSアプリのクライアントを更新するためにreactcontextapiを使用することをお勧めします。

-----------コンテキスト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>

-------- authを使用して新しいクライアントを生成するutil関数

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 評価