<p>apollo-link-ws:初始设置后如何更改连接参数(和令牌)?</p>

创建于 2017-10-31  ·  28评论  ·  资料来源: apollographql/apollo-link

文档将 websockets 上的身份验证描述为在最初创建 WsLink 时在connectionParams中提供令牌。

但是如果用户注销并再次登录(可能使用不同的令牌),则不清楚如何稍后更改令牌并重新初始化连接

我确实设法达到了这样的预期结果:

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

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

但感觉有点 hacky,因为wsLink.subscriptionClient被标记为私有并且不应该从外部访问

enhancement

最有用的评论

由于新的WebSocketLink仍在创建subscriptions-transport-ws的实例,因此我手动将中间件应用到订阅传输客户端,而不是应用到 WebSocketLink。

我正在使用与 v1 中相同的过程,尽管不将其作为 WebSocketLink API 的一部分感觉有点怪异。

// 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服务器端解析的响应上?

这是我为你准备的,以我使用 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的实例,因此我手动将中间件应用到订阅传输客户端,而不是应用到 WebSocketLink。

我正在使用与 v1 中相同的过程,尽管不将其作为 WebSocketLink API 的一部分感觉有点怪异。

// 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有什么想法吗?

几周前,我创建了一个解决方案(?在我的情况下它正在工作):

在我的app.module.ts中导入了一个模块GraphQLModule #$ :

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我不确定。 我正在尝试使用来自 React-Native 的 AsyncStorage 作为连接参数。 但是由于我添加了异步,它不断向服务器发送未定义的内容。
我有这个问题?

更新:
有一个拉取请求解决了这个问题,但尚未合并到主分支中

@jonathanheilmann ,您的解决方案类似于@helios1138原始解决方法。 但是,订阅客户端的connect() API 被标记为私有,理想情况下不应使用。

我尝试做一个subscriptionClient.close(false, false)这是一个公共 API,应该强制重新连接,它确实如此,但订阅仍然不起作用。 现在唯一有效的是:

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

对此有什么全新的想法吗? 喜欢在这里找到解决方案。 我在加载时创建了我的 apollo 链接,但是一旦我的用户登录 id 真的很想更新在上下文中发送的令牌。

这是我们能够提出的最佳解决方案: https :

context是在每个操作上创建的,我们只是动态地查找用户。

有人可以确认,一旦打开了 websocket 通道(授权标头 = 令牌 AAA),使用 websocket 链接的每个后续请求都将始终被标识为 AAA 令牌。

或者有没有办法在每个请求上发送不同的 Authorization 标头(除了重新打开另一个 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,由于未知原因,我无法通过上下文发送令牌。

有没有办法在服务器的中间件中访问令牌?
我正在使用 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}`);
});

在不关闭套接字的情况下,我会收到一堆阿波罗网络错误(尽管它会自行纠正,但我们在一堆 toast 通知中显示这些错误并不理想)所以我必须在更新令牌后关闭 websocket,然后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 ,因此我可以在它连接时添加调度调用(例如,告诉 redux 商店 websocket 已连接)。

/**
 * 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的回答使用react context 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>

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

IE
只是改变:

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

到:

connectionParams: () => ({
            token: cookie.get('token'),
}),
此页面是否有帮助?
0 / 5 - 0 等级