Est-il possible d'obtenir le HttpServletRequest dans un @ServerEndpoint? J'essaie principalement de l'obtenir afin de pouvoir accéder à l'objet HttpSession.
Mise à jour (novembre 2016): Les informations fournies dans cette réponse concernent la spécification JSR356, les implémentations individuelles de la spécification peuvent varier en dehors de ces informations. Les autres suggestions trouvées dans les commentaires et autres réponses sont toutes des comportements spécifiques à l'implémentation en dehors de la spécification JSR356.
Si les suggestions ici vous posent des problèmes, mettez à niveau vos différentes installations de Jetty, Tomcat, Wildfly ou Glassfish/Tyrus. Il a été signalé que toutes les versions actuelles de ces implémentations fonctionnent de la manière décrite ci-dessous.
Revenons maintenant à la réponse originale de août 201 ...
La réponse de Martin Andersson a un défaut de concurrence. Le configurateur peut être appelé par plusieurs threads en même temps, il est probable que vous n'aurez pas accès à l'objet HttpSession correct entre les appels de modifyHandshake()
et getEndpointInstance()
.
Ou dit une autre façon ...
Voici une modification du code de Martin qui utilise ServerEndpointConfig.getUserProperties()
map pour rendre le HttpSession
disponible pour votre instance de socket lors de l'appel de la méthode _@OnOpen
_
GetHttpSessionConfigurator.Java
_package examples;
import javax.servlet.http.HttpSession;
import javax.websocket.HandshakeResponse;
import javax.websocket.server.HandshakeRequest;
import javax.websocket.server.ServerEndpointConfig;
public class GetHttpSessionConfigurator extends ServerEndpointConfig.Configurator
{
@Override
public void modifyHandshake(ServerEndpointConfig config,
HandshakeRequest request,
HandshakeResponse response)
{
HttpSession httpSession = (HttpSession)request.getHttpSession();
config.getUserProperties().put(HttpSession.class.getName(),httpSession);
}
}
_
GetHttpSessionSocket.Java
_package examples;
import Java.io.IOException;
import javax.servlet.http.HttpSession;
import javax.websocket.EndpointConfig;
import javax.websocket.OnMessage;
import javax.websocket.OnOpen;
import javax.websocket.Session;
import javax.websocket.server.ServerEndpoint;
@ServerEndpoint(value = "/example",
configurator = GetHttpSessionConfigurator.class)
public class GetHttpSessionSocket
{
private Session wsSession;
private HttpSession httpSession;
@OnOpen
public void open(Session session, EndpointConfig config) {
this.wsSession = session;
this.httpSession = (HttpSession) config.getUserProperties()
.get(HttpSession.class.getName());
}
@OnMessage
public void echo(String msg) throws IOException {
wsSession.getBasicRemote().sendText(msg);
}
}
_
Bonus: pas de instanceof
ou de casting requis.
Quelques connaissances EndpointConfig
EndpointConfig
les objets existent par "Endpoint Instance".
Cependant, une "instance de point de terminaison" a 2 significations avec la spécification.
javax.websocket.Session
_ qui relie l'instance de point de terminaison d'objet, avec sa configuration, à une connexion logique spécifique.Il est possible d'avoir une instance de point de terminaison singleton utilisée pour plusieurs instances de _javax.websocket.Session
_ (c'est l'une des fonctionnalités prises en charge par _ServerEndpointConfig.Configurator
_)
L'implémentation de ServerContainer suivra un ensemble de ServerEndpointConfig qui représentent tous les points de terminaison déployés auxquels le serveur peut répondre à une demande de mise à niveau Websocket.
Ces instances d'objet ServerEndpointConfig peuvent provenir de différentes sources.
javax.websocket.server.ServerContainer.addEndpoint(ServerEndpointConfig)
javax.servlet.ServletContextInitializer.contextInitialized(ServletContextEvent sce)
javax.websocket.server.ServerApplicationConfig.getEndpointConfigs(Set)
.@ServerEndpoint
_ classes annotées.Ces instances d'objet ServerEndpointConfig
existent en tant que valeurs par défaut lorsqu'un _javax.websocket.Session
_ est finalement créé.
Instance ServerEndpointConfig.Configurator
Avant la réception ou le traitement de toute demande de mise à niveau, tous les objets _ServerEndpointConfig.Configurator
_ existent maintenant et sont prêts à remplir leur fonction principale et unique, pour permettre la personnalisation du processus de mise à niveau d'une connexion Websocket vers un éventuel _javax.websocket.Session
_
Accès à EndpointConfig spécifique à la session
Remarque, vous ne pouvez pas accéder aux instances d'objet ServerEndpointConfig
à partir d'une instance de noeud final. Vous ne pouvez accéder qu'aux instances EndpointConfig
.
Cela signifie que si vous avez fourni ServerContainer.addEndpoint(new MyCustomServerEndpointConfig())
pendant le déploiement et que vous avez ensuite essayé d'y accéder via les annotations, cela ne fonctionnera pas.
Tous les éléments suivants ne seraient pas valides.
_@OnOpen
public void onOpen(Session session, EndpointConfig config)
{
MyCustomServerEndpointConfig myconfig = (MyCustomServerEndpointConfig) config;
/* this would fail as the config is cannot be cast around like that */
}
// --- or ---
@OnOpen
public void onOpen(Session session, ServerEndpointConfig config)
{
/* For @OnOpen, the websocket implementation would assume
that the ServerEndpointConfig to be a declared PathParam
*/
}
// --- or ---
@OnOpen
public void onOpen(Session session, MyCustomServerEndpointConfig config)
{
/* Again, for @OnOpen, the websocket implementation would assume
that the MyCustomServerEndpointConfig to be a declared PathParam
*/
}
_
Vous pouvez accéder à EndpointConfig pendant la durée de vie de l'instance d'objet Endpoint, mais sous un temps limité. Les méthodes javax.websocket.Endpoint.onOpen(Session,Endpoint)
, annotées _@OnOpen
_, ou via l'utilisation de CDI. EndpointConfig n'est pas disponible d'une autre manière ni à aucun autre moment.
Cependant, vous pouvez toujours accéder aux UserProperties via l'appel Session.getUserProperties()
, qui est toujours disponible. Cette carte des propriétés utilisateur est toujours disponible, que ce soit via les techniques annotées (comme un paramètre de session pendant _@OnOpen
_, _@OnClose
_, _@OnError
_ ou _@OnMessage
_ appels), via CDI injection de la Session, ou même avec l'utilisation de websockets non annotées qui s'étendent de _javax.websocket.Endpoint
_.
Comment fonctionne la mise à nivea
Comme indiqué précédemment, chacun des points de terminaison définis sera associé à un ServerEndpointConfig
.
Ces ServerEndpointConfigs
sont une instance unique qui représente l'état par défaut des EndpointConfig
qui sont éventuellement mis à la disposition des instances de point de terminaison qui sont éventuellement et éventuellement créées.
Lorsqu'une demande de mise à niveau entrante arrive, elle a suivi les étapes suivantes sur le JSR.
Pour noter, le ServerEndpointConfig.Configurator est un singleton, par point de terminaison ServerContainer mappé.
Ceci est intentionnel et souhaité pour permettre aux implémenteurs plusieurs fonctionnalités.
Si les implémentations créaient un nouveau configurateur pour chaque prise de contact, cette technique ne serait pas possible.
(Divulgation: j'écris et maintiens l'implémentation JSR-356 pour Jetty 9)
Il n'est pas clair si vous voulez le HttpServletRequest
, le HttpSession
ou les propriétés du HttpSession
. Ma réponse montrera comment obtenir le HttpSession
ou les propriétés individuelles.
J'ai omis les vérifications des limites null et index pour plus de concision.
C'est délicat. La réponse de Martin Andersson n'est pas correcte car la même instance de ServerEndpointConfig.Configurator
Est utilisée pour chaque connexion, donc une condition de concurrence existe. Alors que les documents indiquent que "l'implémentation crée une nouvelle instance du configurateur par point de terminaison logique", la spécification ne définit pas clairement un "point de terminaison logique". Sur la base du contexte de tous les endroits où cette phrase est utilisée, elle semble signifier la liaison d'une classe, d'un configurateur, d'un chemin et d'autres options, c'est-à-dire un ServerEndpointConfig
, qui est clairement partagé. Quoi qu'il en soit, vous pouvez facilement voir si une implémentation utilise la même instance en imprimant sa toString()
depuis modifyHandshake(...)
.
Plus surprenant, la réponse de Joakim Erdfelt ne fonctionne pas non plus de manière fiable. Le texte de JSR 356 lui-même ne mentionne pas EndpointConfig.getUserProperties()
, il est uniquement dans JavaDoc, et nulle part il ne semble être spécifié quelle est sa relation exacte avec Session.getUserProperties()
. En pratique, certaines implémentations (par exemple, Glassfish) renvoient la même instance Map
pour tous les appels à ServerEndpointConfig.getUserProperties()
tandis que d'autres (par exemple, Tomcat 8) ne le font pas. Vous pouvez vérifier en imprimant le contenu de la carte avant de le modifier dans modifyHandshake(...)
.
Pour vérifier, j'ai copié le code directement à partir des autres réponses, puis l'ai testé contre un client multithread que j'ai écrit. Dans les deux cas, j'ai observé que la session incorrecte était associée à l'instance de point de terminaison.
J'ai développé deux solutions dont j'ai vérifié le bon fonctionnement lors d'un test sur un client multithread. Il existe deux astuces clés.
Tout d'abord, utilisez un filtre avec le même chemin d'accès que le WebSocket. Cela vous donnera accès aux HttpServletRequest
et HttpSession
. Cela vous donne également la possibilité de créer une session si elle n'existe pas déjà (bien que dans ce cas, l'utilisation d'une session HTTP semble douteuse).
Deuxièmement, recherchez certaines propriétés qui existent à la fois dans WebSocket Session
et HttpServletRequest
ou HttpSession
. Il s'avère qu'il y a deux candidats: getUserPrincipal()
et getRequestParameterMap()
. Je vais vous montrer comment abuser des deux :)
La manière la plus simple est de profiter de Session.getUserPrincipal()
et HttpServletRequest.getUserPrincipal()
. L'inconvénient est que cela pourrait interférer avec d'autres utilisations légitimes de cette propriété, alors n'utilisez-la que si vous êtes prêt à ces implications.
Si vous souhaitez stocker une seule chaîne, comme un ID utilisateur, ce n'est en fait pas trop abusif, bien qu'il devrait probablement être défini d'une manière gérée par conteneur plutôt que de remplacer le wrapper comme je vais vous le montrer. Quoi qu'il en soit, vous feriez cela en remplaçant simplement Principal.getName()
. Ensuite, vous n'avez même pas besoin de le convertir dans le Endpoint
. Mais si vous pouvez l'estimer, vous pouvez également passer tout l'objet HttpSession
comme suit.
package example1;
import Java.security.Principal;
import javax.servlet.http.HttpSession;
public class PrincipalWithSession implements Principal {
private final HttpSession session;
public PrincipalWithSession(HttpSession session) {
this.session = session;
}
public HttpSession getSession() {
return session;
}
@Override
public String getName() {
return ""; // whatever is appropriate for your app, e.g., user ID
}
}
package example1;
import Java.io.IOException;
import Java.security.Principal;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.annotation.WebFilter;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
@WebFilter("/example1")
public class WebSocketFilter implements Filter {
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest httpRequest = (HttpServletRequest) request;
final PrincipalWithSession p = new PrincipalWithSession(httpRequest.getSession());
HttpServletRequestWrapper wrappedRequest = new HttpServletRequestWrapper(httpRequest) {
@Override
public Principal getUserPrincipal() {
return p;
}
};
chain.doFilter(wrappedRequest, response);
}
public void init(FilterConfig config) throws ServletException { }
public void destroy() { }
}
package example1;
import javax.servlet.http.HttpSession;
import javax.websocket.OnMessage;
import javax.websocket.OnOpen;
import javax.websocket.Session;
import javax.websocket.server.ServerEndpoint;
@ServerEndpoint("/example1")
public class WebSocketEndpoint {
private HttpSession httpSession;
@OnOpen
public void onOpen(Session webSocketSession) {
httpSession = ((PrincipalWithSession) webSocketSession.getUserPrincipal()).getSession();
}
@OnMessage
public String demo(String msg) {
return msg + "; (example 1) session ID " + httpSession.getId();
}
}
La deuxième option utilise Session.getRequestParameterMap()
et HttpServletRequest.getParameterMap()
. Notez qu'il utilise ServerEndpointConfig.getUserProperties()
mais il est sûr dans ce cas car nous mettons toujours le même objet dans la carte, donc qu'il soit partagé ne fait aucune différence. L'identifiant de session unique est transmis non pas via les paramètres utilisateur mais via les paramètres de requête, qui est unique par requête.
Cette solution est légèrement moins hacky car elle n'interfère pas avec la propriété principale de l'utilisateur. Notez que si vous devez passer par les paramètres de demande réels en plus de celui qui est inséré, vous pouvez facilement le faire: commencez simplement par la mappe de paramètres de demande existante au lieu d'une nouvelle carte vide comme indiqué ici . Mais veillez à ce que l'utilisateur ne puisse pas usurper le paramètre spécial ajouté dans le filtre en fournissant son propre paramètre de demande du même nom dans la demande HTTP réelle.
/* A simple, typical, general-purpose servlet session tracker */
package example2;
import Java.util.concurrent.ConcurrentHashMap;
import Java.util.concurrent.ConcurrentMap;
import javax.servlet.ServletContextEvent;
import javax.servlet.ServletContextListener;
import javax.servlet.annotation.WebListener;
import javax.servlet.http.HttpSession;
import javax.servlet.http.HttpSessionEvent;
import javax.servlet.http.HttpSessionListener;
@WebListener
public class SessionTracker implements ServletContextListener, HttpSessionListener {
private final ConcurrentMap<String, HttpSession> sessions = new ConcurrentHashMap<>();
@Override
public void contextInitialized(ServletContextEvent event) {
event.getServletContext().setAttribute(getClass().getName(), this);
}
@Override
public void contextDestroyed(ServletContextEvent event) {
}
@Override
public void sessionCreated(HttpSessionEvent event) {
sessions.put(event.getSession().getId(), event.getSession());
}
@Override
public void sessionDestroyed(HttpSessionEvent event) {
sessions.remove(event.getSession().getId());
}
public HttpSession getSessionById(String id) {
return sessions.get(id);
}
}
package example2;
import Java.io.IOException;
import Java.util.Collections;
import Java.util.Map;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.annotation.WebFilter;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
@WebFilter("/example2")
public class WebSocketFilter implements Filter {
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest httpRequest = (HttpServletRequest) request;
final Map<String, String[]> fakedParams = Collections.singletonMap("sessionId",
new String[] { httpRequest.getSession().getId() });
HttpServletRequestWrapper wrappedRequest = new HttpServletRequestWrapper(httpRequest) {
@Override
public Map<String, String[]> getParameterMap() {
return fakedParams;
}
};
chain.doFilter(wrappedRequest, response);
}
@Override
public void init(FilterConfig config) throws ServletException { }
@Override
public void destroy() { }
}
package example2;
import javax.servlet.http.HttpSession;
import javax.websocket.EndpointConfig;
import javax.websocket.HandshakeResponse;
import javax.websocket.OnMessage;
import javax.websocket.OnOpen;
import javax.websocket.Session;
import javax.websocket.server.HandshakeRequest;
import javax.websocket.server.ServerEndpoint;
import javax.websocket.server.ServerEndpointConfig;
@ServerEndpoint(value = "/example2", configurator = WebSocketEndpoint.Configurator.class)
public class WebSocketEndpoint {
private HttpSession httpSession;
@OnOpen
public void onOpen(Session webSocketSession, EndpointConfig config) {
String sessionId = webSocketSession.getRequestParameterMap().get("sessionId").get(0);
SessionTracker tracker =
(SessionTracker) config.getUserProperties().get(SessionTracker.class.getName());
httpSession = tracker.getSessionById(sessionId);
}
@OnMessage
public String demo(String msg) {
return msg + "; (example 2) session ID " + httpSession.getId();
}
public static class Configurator extends ServerEndpointConfig.Configurator {
@Override
public void modifyHandshake(ServerEndpointConfig sec, HandshakeRequest request,
HandshakeResponse response) {
Object tracker = ((HttpSession) request.getHttpSession()).getServletContext().getAttribute(
SessionTracker.class.getName());
// This is safe to do because it's the same instance of SessionTracker all the time
sec.getUserProperties().put(SessionTracker.class.getName(), tracker);
super.modifyHandshake(sec, request, response);
}
}
}
Si vous n'avez besoin que de certaines propriétés de HttpSession
et non de l'ensemble HttpSession
lui-même, comme par exemple un ID utilisateur, alors vous pouvez supprimer l'ensemble SessionTracker
de l'entreprise et simplement mettre les paramètres nécessaires dans la carte que vous revenez de votre remplacement de HttpServletRequestWrapper.getParameterMap()
. Ensuite, vous pouvez également vous débarrasser du Configurator
personnalisé; vos propriétés seront facilement accessibles à partir de Session.getRequestParameterMap()
dans le point de terminaison.
package example5;
import Java.io.IOException;
import Java.util.HashMap;
import Java.util.Map;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.annotation.WebFilter;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
@WebFilter("/example5")
public class WebSocketFilter implements Filter {
@Override
public void doFilter(ServletRequest request, ServletResponse response,
FilterChain chain) throws IOException, ServletException {
HttpServletRequest httpRequest = (HttpServletRequest) request;
final Map<String, String[]> props = new HashMap<>();
// Add properties of interest from session; session ID
// is just for example
props.put("sessionId", new String[] { httpRequest.getSession().getId() });
HttpServletRequestWrapper wrappedRequest = new HttpServletRequestWrapper(httpRequest) {
@Override
public Map<String, String[]> getParameterMap() {
return props;
}
};
chain.doFilter(wrappedRequest, response);
}
@Override
public void destroy() {
}
@Override
public void init(FilterConfig arg0) throws ServletException {
}
}
package example5;
import Java.util.List;
import Java.util.Map;
import javax.websocket.OnMessage;
import javax.websocket.OnOpen;
import javax.websocket.Session;
import javax.websocket.server.ServerEndpoint;
@ServerEndpoint("/example5")
public class WebSocketEndpoint {
private Map<String, List<String>> params;
@OnOpen
public void onOpen(Session session) {
params = session.getRequestParameterMap();
}
@OnMessage
public String demo(String msg) {
return msg + "; (example 5) session ID " + params.get("sessionId").get(0);
}
}
Examinons la spécification API Java pour WebSocket pour voir s'il est possible de récupérer l'objet HttpSession
. spécification dit à la page 29:
Dans la mesure où les connexions Websocket sont lancées avec une demande http, il existe une association entre la session HttpSession sous laquelle un client fonctionne et toutes les connexions Web établies dans cette session HttpSession. L'API permet l'accès dans la poignée de main d'ouverture à la session HttpSession unique correspondant à ce même client.
Alors oui c'est possible.
Cependant, je ne pense pas qu'il soit possible pour vous d'obtenir une référence à l'objet HttpServletRequest
. Vous pouvez écouter toutes les nouvelles demandes de servlet à l'aide d'un ServletRequestListener
, mais vous devrez encore comprendre quelle demande appartient à quel point de terminaison de serveur. Veuillez me faire savoir si vous trouvez une solution!
Le mode d'emploi est décrit de manière approximative aux pages 13 et 14 de la spécification et illustré par moi dans le code sous l'en-tête suivant.
En anglais, nous devrons intercepter le processus de prise de contact pour mettre la main sur un objet HttpSession
. Pour transférer ensuite la référence HttpSession à notre point de terminaison de serveur, nous devons également intercepter lorsque le conteneur crée l'instance de point de terminaison de serveur et injecter manuellement la référence. Nous faisons tout cela en fournissant notre propre ServerEndpointConfig.Configurator
Et en remplaçant les méthodes modifyHandshake()
et getEndpointInstance()
.
Le configurateur personnalisé sera instancié une fois par ServerEndpoint
logique (Voir JavaDoc ).
Il s'agit de la classe de point de terminaison du serveur (je fournis l'implémentation de la classe CustomConfigurator après cet extrait de code):
@ServerEndpoint(value = "/myserverendpoint", configurator = CustomConfigurator.class)
public class MyServerEndpoint
{
private HttpSession httpSession;
public void setHttpSession(HttpSession httpSession) {
if (this.httpSession != null) {
throw new IllegalStateException("HttpSession has already been set!");
}
this.httpSession = httpSession;
}
@OnOpen
public void onOpen(Session session, EndpointConfig config) {
System.out.println("My Session Id: " + httpSession.getId());
}
}
Et voici le configurateur personnalisé:
public class CustomConfigurator extends ServerEndpointConfig.Configurator
{
private HttpSession httpSession;
// modifyHandshake() is called before getEndpointInstance()!
@Override
public void modifyHandshake(ServerEndpointConfig sec, HandshakeRequest request, HandshakeResponse response) {
httpSession = (HttpSession) request.getHttpSession();
super.modifyHandshake(sec, request, response);
}
@Override
public <T> T getEndpointInstance(Class<T> endpointClass) throws InstantiationException {
T endpoint = super.getEndpointInstance(endpointClass);
if (endpoint instanceof MyServerEndpoint) {
// The injection point:
((MyServerEndpoint) endpoint).setHttpSession(httpSession);
}
else {
throw new InstantiationException(
MessageFormat.format("Expected instanceof \"{0}\". Got instanceof \"{1}\".",
MyServerEndpoint.class, endpoint.getClass()));
}
return endpoint;
}
}
Toutes les réponses ci-dessus méritent d'être lues, mais aucune d'entre elles ne résout le problème de OP (et mon).
Vous pouvez accéder à HttpSession lorsqu'un point de terminaison WS s'ouvre et le transmettre à une instance de point de terminaison nouvellement créée, mais personne ne garantit l'existence d'une instance HttpSession!
Nous avons donc besoin de l'étape 0 avant ce piratage (je déteste l'implémentation JSR 365 de WebSocket). Websocket - httpSession renvoie null
La seule façon qui fonctionne sur tous les serveurs d'applications est d'utiliser ThreadLocal. Voir:
Toutes les solutions possibles sont basées sur:
A. Les implémentations du navigateur client conservent l'ID de session via la valeur de cookie transmise en tant qu'en-tête HTTP ou (si les cookies sont désactivés), il est géré par le conteneur Servlet qui générera le suffixe d'ID de session pour les URL générées
B. Vous ne pouvez accéder aux en-têtes de demande HTTP que pendant la négociation HTTP; après cela, c'est le protocole Websocket
Pour que...
Solution 1: utilisez la "poignée de main" pour accéder à HTTP
Solution 2: dans votre JavaScript côté client, générez dynamiquement le paramètre ID de session HTTP et envoyez le premier message (via Websocket) contenant cet ID de session. Connectez "endpoint" à la classe de cache/utilitaire en conservant l'ID de session -> Mappage de session; pour éviter les fuites de mémoire, vous pouvez utiliser Session Listener par exemple pour supprimer la session du cache.
P.S. J'apprécie les réponses de Martin Andersson et Joakim Erdfelt. Malheureusement, la solution de Martin n'est pas sûre pour les threads ...