Документы описывают аутентификацию в веб-сокетах как предоставление токена в 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
помечен как частный и не должен быть доступен извне.
Вы можете использовать функцию, которая разрешается в объект в 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'),
}),
Самый полезный комментарий
Поскольку новый
WebSocketLink
по-прежнему создает экземплярsubscriptions-transport-ws
, я вручную применяю промежуточное ПО к клиенту Subscription Transport, а не к WebSocketLink.Я использую тот же процесс, что и в v1, хотя мне кажется, что не использовать его как часть API WebSocketLink.
Любые значения, которые вы добавляете в объект параметров, доступны в
onOperation
на стороне сервера.