在Web Socket @ServerEndpoint中从HttpServletRequest访问HttpSession

是否有可能获得HttpServletRequest @ServerEndpoint内? 主要是我试图得到它,所以我可以访问HttpSession对象。

更新(2016年11月) :此答案中提供的信息适用于JSR356规范,规范的各个实现可能会在此信息之外有所不同。 在评论和其他答案中发现的其他build议是JSR356规范以外的所有特定于实现的行为。

如果这里的build议引起你的问题,请升级你的各种Jetty,Tomcat,Wildfly或者Glassfish / Tyrus。 这些实现的所有当前版本都已经被报告以下述方式工作。

现在回到2013年8月的原始答案

Martin Andersson的答案有一个并发缺陷。 Configurator可以同时被多个线程调用,很可能在modifyHandshake()getEndpointInstance()的调用之间不能访问正确的HttpSession对象。

或者说另一种方式…

  • 要求A
  • 修改握手A
  • 请求B
  • 修改握手B
  • 获取端点实例A < – 这将有请求B的HttpSession
  • 获取端点实例B.

这是对Martin的代码的修改,它使用ServerEndpointConfig.getUserProperties()映射在@OnOpen方法调用期间使HttpSession可用于您的套接字实例

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

奖励function:不需要instanceof或cast。

一些EndpointConfig知识

EndpointConfig对象确实存在于每个“端点实例”。

但是,“端点实例”与规范有两个含义。

  1. JSR的默认行为,其中每个传入的升级请求都会生成端点类的新对象实例
  2. 一个javax.websocket.Session ,将对象端点实例及其configuration连接到特定的逻辑连接。

可以将单例端点实例用于多个javax.websocket.Session实例(这是ServerEndpointConfig.Configurator支持的function之一)

ServerContainer实现将跟踪一组ServerEndpointConfig,它们表示服务器可以响应websocket升级请求的所有已部署端点。

这些ServerEndpointConfig对象实例可以来自几个不同的来源。

  1. javax.websocket.server.ServerContainer.addEndpoint(ServerEndpointConfig)手动提供
    • 通常在javax.servlet.ServletContextInitializer.contextInitialized(ServletContextEvent sce)
  2. javax.websocket.server.ServerApplicationConfig.getEndpointConfigs(Set)调用。
  3. 通过扫描Web应用程序为@ServerEndpoint注释的类自动创build。

这些ServerEndpointConfig对象实例作为javax.websocket.Session最终创build时的默认值存在。

ServerEndpointConfig.Configurator实例

在接收或处理任何升级请求之前,所有ServerEndpointConfig.Configurator对象现在都已存在,并准备好执行其主要和唯一的目的,以允许定制最终的javax.websocket.Session的websocket连接的升级过程

访问会话特定的EndpointConfig

请注意,您不能从端点实例内访问ServerEndpointConfig对象实例。 您只能访问EndpointConfig实例。

这意味着如果您在部署过程中提供了ServerContainer.addEndpoint(new MyCustomServerEndpointConfig()) ,并且稍后尝试通过注释访问它,则无法工作。

以下全部将是无效的。

 @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 */ } 

您可以在Endpoint对象实例的生命周期中访问EndpointConfig,但在有限的时间内。 javax.websocket.Endpoint.onOpen(Session,Endpoint) ,注释@OnOpen方法,或通过使用CDI。 EndpointConfig不能以任何其他方式或在任何其他时间使用。

但是,您始终可以通过Session.getUserProperties()调用访问UserProperties。 此用户属性映射始终可用,无论是通过注释技术(例如@OnOpen@OnClose@OnError@OnMessage调用期间的Session参数),通过CDI注入Session,还是使用非从javax.websocket.Endpoint扩展的javax.websocket.Endpoint websocket。

如何升级工作

如前所述,每个定义的端点将有一个与之关联的ServerEndpointConfig

这些ServerEndpointConfigs是一个单一实例,表示EndpointConfig的最终状态,最终可用于可能并最终创build的端点实例。

当传入的升级请求到达时,它已经在JSR上进行了以下操作。

  1. 该path是否与任何ServerEndpointConfig.getPath()条目相匹配
    • 如果不匹配,则返回404进行升级
  2. 将升级请求传递到ServerEndpointConfig.Configurator.checkOrigin()
    • 如果无效,则返回错误以升级响应
    • 创buildHandshakeResponse
  3. 将升级请求传递到ServerEndpointConfig.Configurator.getNegotiatedSubprotocol()
    • 在HandshakeResponse中存储答案
  4. 将升级请求传递到ServerEndpointConfig.Configurator.getNegotiatedExtensions()
    • 在HandshakeResponse中存储答案
  5. 创build新的端点特定的ServerEndpointConfig对象。 复制编码器,解码器和用户属性。 这个新的ServerEndpointConfig包装path,扩展,端点类,子协议,configuration器的默认值。
  6. 将升级请求,响应和新的ServerEndpointConfig传递到ServerEndpointConfig.Configurator.modifyHandshake()
  7. 调用ServerEndpointConfig.getEndpointClass()
  8. 使用ServerEndpointConfig.Configurator.getEndpointInstance(Class)上的类
  9. 创build会话,关联端点实例和EndpointConfig对象。
  10. 通知连接的端点实例
  11. 需要EndpointConfig的注释方法获取与此Session相关的方法。
  12. 调用Session.getUserProperties()返回EndpointConfig.getUserProperties()

要注意,ServerEndpointConfig.Configurator是每个映射的ServerContainer端点的单例。

这是有意的,并且是期望的,以允许实现者多个特征。

  • 如果他们愿意,可以为多个对等端返回相同的端点实例。 所谓的无状态方法websocket写作。
  • 为所有Endpoint实例拥有单一的昂贵资源pipe理点

如果实现为每个握手创build了一个新的configuration器,这种技术是不可能的。

(披露:我编写和维护Jetty 9的JSR-356实现)

前言

目前还不清楚是否需要HttpServletRequestHttpSessionHttpSession属性。 我的答案将显示如何获得HttpSession或单个属性。

为了简洁,我省略了空值和索引边界检查。

注意事项

这很棘手。 Martin Andersson的答案是不正确的,因为每个连接都使用同一个ServerEndpointConfig.Configurator实例,因此存在争用条件。 尽pipe文档声明“实现为每个逻辑端点创build了configuration程序的新实例”,但该规范没有明确定义“逻辑端点”。 基于短语所使用的所有位置的上下文,它似乎意味着类,configuration器,path和其他选项(即明确共享的ServerEndpointConfig的绑定。 无论如何,通过在modifyHandshake(...)打印出toString() ,你可以很容易地看到一个实现是否使用同一个实例。

更令人吃惊的是,Joakim Erdfelt的答案也无法可靠。 JSR 356本身并没有提到EndpointConfig.getUserProperties() ,它只是在JavaDoc中,而且它似乎没有指定它与Session.getUserProperties()确切关系。 在实践中,一些实现(例如Glassfish)将所有调用的Map实例返回到ServerEndpointConfig.getUserProperties()而其他实例(例如Tomcat 8)则不会。 您可以通过在modifyHandshake(...)修改之前打印出地图内容来进行modifyHandshake(...)

为了validation,我直接从其他答案复制代码,然后用我写的multithreading客户端进行testing。 在这两种情况下,我都观察到不正确的会话与端点实例相关联。

解决scheme概述

我已经开发了两种解决scheme,在针对multithreading客户端进行testing时,我已经validation了其正确工作。 有两个关键的窍门。

首先,使用与WebSocket具有相同path的filter。 这将使您可以访问HttpServletRequestHttpSession 。 如果它还不存在,它也给你创build一个会话的机会(尽pipe在这种情况下使用HTTP会话似乎是可疑的)。

其次,findWebSocket SessionHttpServletRequestHttpSession中存在的一些属性。 原来有两个候选者: getUserPrincipal()getRequestParameterMap() 。 我会告诉你如何滥用他们两个:)

使用用户主体解决scheme

最简单的方法是利用Session.getUserPrincipal()HttpServletRequest.getUserPrincipal() 。 缺点是这可能会干扰这个属性的其他合法使用,所以只有在您准备好这些影响时才使用它。

如果你只想存储一个string,比如用户ID,这实际上并不是太多的滥用,虽然它可能应该被设置成一些容器pipe理的方式,而不是重写封装,因为我会告诉你。 无论如何,你只要重写Principal.getName() 。 那么你甚至不需要把它扔在Endpoint 。 但是,如果你能忍受它,你也可以按如下方式传递整个HttpSession对象。

PrincipalWithSession.java

 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, eg, user ID } } 

WebSocketFilter.java

 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() { } } 

WebSocketEndpoint.java

 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(); } } 

解决scheme使用请求参数

第二个选项使用Session.getRequestParameterMap()HttpServletRequest.getParameterMap() 。 请注意,它使用了ServerEndpointConfig.getUserProperties()但在这种情况下它是安全的,因为我们总是将相同的对象放入地图中,所以不pipe它是否共享都没有区别。 唯一的会话标识符不是通过用户parameter passing,而是通过请求parameter passing的,每个请求都是唯一的。

这个解决scheme稍微不简单,因为它不会干扰用户的主要属性。 请注意,如果您需要通过实际的请求参数以及插入的请求参数,则可以轻松地完成此操作:只需从现有的请求参数映射开始,而不是像这里显示的那样使用新的空参数映射。 但要注意的是,用户不能通过在实际的HTTP请求中提供自己的请求参数来欺骗filter中添加的特殊参数。

SessionTracker.java

 /* 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); } } 

WebSocketFilter.java

 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() { } } 

WebSocketEndpoint.java

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

单一属性的解决scheme

如果你只需要HttpSession某些属性而不是整个HttpSession本身,比如说一个用户ID,那么你可以不用整个SessionTracker业务,只需要把你的重载HttpServletRequestWrapper.getParameterMap()返回的映射放在必要的参数中HttpServletRequestWrapper.getParameterMap() 。 那么你也可以摆脱自定义的Configurator ; 您的属性将可以方便地从Endpoint中的Session.getRequestParameterMap()访问。

WebSocketFilter.java

 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 { } } 

WebSocketEndpoint.java

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

可能吗?

让我们回顾一下WebSocket规范的Java API,看看是否可以获得HttpSession对象。 规范在第29页上说:

因为websocket连接是通过http请求启动的,所以客户端运行的HttpSession和HttpSession中build立的任何websocket之间都有关联。 API允许以开头的握手方式访问对应于同一客户端的唯一HttpSession。

所以是的,这是可能的。

但是,我不认为你有可能获得HttpServletRequest对象的引用。 您可以使用ServletRequestListener侦听所有新的servlet请求,但是您仍然必须找出哪个请求属于哪个服务器端点。 请让我知道,如果你find一个解决scheme!

摘要如何

在说明书的第13页和第14页中粗略地描述了方法,在下一个标题下用代码示例。

在英文中,我们需要拦截握手过程来获取HttpSession对象。 为了将HttpSession引用传递给我们的服务器端点,我们还需要拦截容器创build服务器端点实例并手动注入引用。 我们通过提供自己的ServerEndpointConfig.Configurator完成所有这些工作,并覆盖方法modifyHandshake()getEndpointInstance()

自定义configuration器将在每个逻辑ServerEndpoint实例化一次(请参阅JavaDoc )。

代码示例

这是服务器端点类(我在此代码片段后提供了CustomConfigurator类的实现):

 @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()); } } 

这是自定义configuration器:

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

以上所有的答案值得一读,但是没有一个能够解决OP的(和我的)问题。

您可以在WS端点打开时访问HttpSession,并将其传递给新创build的端点实例,但是没有人保证存在HttpSession实例!

所以我们需要这个黑客之前的第0步(我讨厌WebSocket的JSR 365实现)。 WebSocket – httpSession返回null

所有可能的解决scheme都基于:

A.客户端浏览器实现通过作为HTTP头传递的Cookie值维护会话ID,或者(如果禁用了cookie)由Servlet容器pipe理,这将为生成的URL生成会话ID后缀

B.您只能在HTTP握手期间访问HTTP请求标题; 之后是Websocket协议

以便…

解决scheme1:使用“握手”来访问HTTP

解决scheme2:在客户端的JavaScript中,dynamic生成HTTP会话ID参数并发送包含此会话ID的第一条消息(通过Websocket)。 将“端点”连接到caching/实用程序类,维护会话ID – >会话映射; 避免内存泄漏,您可以使用Session Listener来从caching中删除会话。

PS我很欣赏Martin Andersson和Joakim Erdfelt的回答。 不幸的是,Martin的解决scheme不是线程安全的。

在所有应用程序服务器上工作的唯一方法是使用ThreadLocal。 看到:

https://java.net/jira/browse/WEBSOCKET_SPEC-235