文档将 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
被标记为私有并且不应该从外部访问
您可以使用解析为 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();
这会在应用启动时打开订阅客户端,并在登录/注销时完全重新初始化。
理想情况下,这也将支持一个承诺返回函数,所以如果我们必须异步获取令牌,我们可以。
@jonathanheilmann ,您的解决方案类似于@helios1138原始解决方法。 但是,订阅客户端的connect()
API 被标记为私有,理想情况下不应使用。
我尝试做一个subscriptionClient.close(false, false)
这是一个公共 API,应该强制重新连接,它确实如此,但订阅仍然不起作用。 现在唯一有效的是:
subscriptionClient.close();
subscriptionClient.connect(); // this is a private API :-(
对此有什么全新的想法吗? 喜欢在这里找到解决方案。 我在加载时创建了我的 apollo 链接,但是一旦我的用户登录 id 真的很想更新在上下文中发送的令牌。
这是我们能够提出的最佳解决方案: https :
context
是在每个操作上创建的,我们只是动态地查找用户。
这对我有帮助
https://github.com/apollographql/apollo-link/issues/446#issuecomment -410073779
有人可以确认,一旦打开了 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'),
}),
最有用的评论
由于新的
WebSocketLink
仍在创建subscriptions-transport-ws
的实例,因此我手动将中间件应用到订阅传输客户端,而不是应用到 WebSocketLink。我正在使用与 v1 中相同的过程,尽管不将其作为 WebSocketLink API 的一部分感觉有点怪异。
您添加到选项对象的任何值都可以在服务器端的
onOperation
回调中使用。