Сопряжение сеанса комет с HTTP-сеансом
У нас есть веб-приложение, использующее Jetty 8.1, dojo и cometd, которое взаимодействует между браузером и веб-контейнером, используя (1) JSON/HTTP REST API для синхронных операций и (2) API Cometd для получения многочисленных событий от сервера.
Что нам не совсем понятно, так это как элегантно управлять сеансами аутентификации этих двух разных API, тем более что Cometd для нас будет использовать websocket вместо обычного HTTP всякий раз, когда это возможно. Приложение использует аутентификацию на основе форм с использованием стандартного модуля Jetty LDAP. Таким образом, с точки зрения HTTP, контейнер предоставляет браузеру стандартный jsessionid, который выглядит следующим образом:
Cookie: jsessionid=758E2FAD7C199D722DA8B5E243E0E27D
Исходя из сообщения Симоны Бордет здесь, кажется, что рекомендуемое решение состоит в том, чтобы передать этот токен во время рукопожатия комет, что мы и делаем.
Проблема в том, что у нас есть два принципиально разных сеанса - сеанс HTTP и сеанс Bayeux Cometd. По таким причинам, как потенциальные утечки памяти и проблемы безопасности, мы хотим, чтобы они заканчивались в унисон или были "спарены". Если HTTP-сеанс пользователя завершается, мы хотим, чтобы соответствующий сеанс Bayeux также завершился, и наоборот. Есть ли рекомендуемый способ сделать это?
2 ответа
Сеансы HTTP и сеансы CometD имеют разные жизненные циклы: например, в случае временного сбоя соединения сеанс CometD завершится неудачно, и сервер запросит у клиента повторное рукопожатие, создав тем самым другой сеанс CometD (представляющий тот же пользователь, но с другим CometD clientId
). В том же случае HttpSession
останется прежним.
Имея это в виду, вы должны поддерживать - на уровне приложения - соответствие между именем пользователя и корреспондентом HttpSession
и корреспондент ServerSession
, Давайте назовем это отображение HttpCometDMapper
, Каждый раз, когда новый пользователь входит в систему, вы регистрируете его имя (или другой уникальный идентификатор пользователя), HttpSession
и текущий ServerSession
, Возможно, вам понадобится двухэтапный процесс, где вы сначала свяжете имя пользователя и HttpSession
и затем то же имя пользователя с ServerSession
,
Если повторное рукопожатие CometD выполнено, вы обновите маппер новым ServerSession
,
Вы можете связать две сессии, зарегистрировав HttpSessionListener
к HttpSession
так что когда он будет уничтожен, вы получите текущий CometD ServerSession
из картографа и звоните ServerSession.disconnect()
в теме.
Наоборот, немного сложнее, потому что CometD не имеет понятия тайм-аута бездействия, как HttpSession
есть. Это должно быть реализовано в приложении с вашей собственной логикой.
Одной из частей этого является регистрация RemoveListener
на ServerSession
, как это:
serverSession.addListener(new ServerSession.RemoveListener()
{
public void removed(ServerSession session, boolean timeout);
{
if (!timeout)
{
// Explicitly disconnected, invalidate the HttpSession
httpCometDMapper.invalidate(session);
}
}
});
Этот слушатель наблюдает за явными отключениями от клиента (а сервер - остерегается повторного входа).
Немного сложнее реализовать тот же механизм для неявных разъединений. В этом случае timeout
Параметр будет иметь значение true, но мог произойти из-за временного сбоя сети (в отличие от исчезновения клиента навсегда), и тот же пользователь, возможно, уже повторно рукопожатие с новым ServerSession
,
Я думаю, что в этом случае тайм-аут приложения может решить проблему: когда вы видите ServerSession
удален из-за тайм-аута, вы заметите, что пользователь и запустить приложение тайм-аут. Если тот же пользователь повторно рукопожатие, отмените тайм-аут приложения; в противном случае пользователь действительно исчезнет, истечет время ожидания приложения, и вы аннулируете HttpSession
тоже.
Что выше, это просто идеи и предложения; фактическая реализация сильно зависит от деталей приложения (и именно поэтому CometD не предоставляет "из коробки").
Ключевыми моментами являются картограф, HttpSessionListener
и RemoveListener
и зная жизненные циклы этих компонентов. Как только вы справитесь с этим, вы можете написать правильный код, который будет подходить для вашего приложения.
Наконец, обратите внимание, что CometD обладает транспортно-независимым способом взаимодействия с HttpSession
через BayeuxContext
экземпляр, который вы можете получить от BayeuxServer.getContext()
, Я предлагаю вам также взглянуть на это, чтобы увидеть, может ли это упростить вещи, особенно для извлечения токенов, хранящихся в HttpSession
,
Есть ли проблема, если мы собираемся создать BayeuxClient после временного сбоя подключения?
Вы можете попробовать с этим кодом ниже.
try {
log.info("Running streaming client example....");
makeConnect();
} catch (Exception e) {
handleException("Error while setup the salesforce connection.", e);
}
}
private void makeConnect() {
try{
client = makeClient();
client.getChannel(Channel.META_HANDSHAKE).addListener
(new ClientSessionChannel.MessageListener() {
public void onMessage(ClientSessionChannel channel, Message message) {
log.info("[CHANNEL:META_HANDSHAKE]: " + message);
boolean success = message.isSuccessful();
if (!success) {
String error = (String) message.get("error");
if (error != null) {
log.error("Error during HANDSHAKE: " + error);
}
Exception exception = (Exception) message.get("exception");
if (exception != null) {
handleException("Exception during HANDSHAKE: ", exception);
}
}
}
});
client.getChannel(Channel.META_CONNECT).addListener(
new ClientSessionChannel.MessageListener() {
public void onMessage(ClientSessionChannel channel, Message message) {
log.info("[CHANNEL:META_CONNECT]: " + message);
boolean success = message.isSuccessful();
if (!success) {
client.disconnect();
makeConnect();
String error = (String) message.get("error");
if (error != null) {
//log.error("Error during CONNECT: " + error);
}
}
}
});
client.getChannel(Channel.META_SUBSCRIBE).addListener(
new ClientSessionChannel.MessageListener() {
public void onMessage(ClientSessionChannel channel, Message message) {
log.info("[CHANNEL:META_SUBSCRIBE]: " + message);
boolean success = message.isSuccessful();
if (!success) {
String error = (String) message.get("error");
if (error != null) {
makeConnect();
log.error("Error during SUBSCRIBE: " + error);
}
}
}
});
client.handshake();
log.info("Waiting for handshake");
boolean handshaken = client.waitFor(waitTime, BayeuxClient.State.CONNECTED);
if (!handshaken) {
log.error("Failed to handshake: " + client);
}
log.info("Subscribing for channel: " + channel);
client.getChannel(channel).subscribe(new MessageListener() {
public void onMessage(ClientSessionChannel channel, Message message) {
injectSalesforceMessage(message);
}
});
log.info("Waiting for streamed data from your organization ...");
}catch (Exception e) {
handleException("Error while setup the salesforce connection.", e);
}
}