在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
对象确实存在于每个“端点实例”。
但是,“端点实例”与规范有两个含义。
- JSR的默认行为,其中每个传入的升级请求都会生成端点类的新对象实例
- 一个
javax.websocket.Session
,将对象端点实例及其configuration连接到特定的逻辑连接。
可以将单例端点实例用于多个javax.websocket.Session
实例(这是ServerEndpointConfig.Configurator
支持的function之一)
ServerContainer实现将跟踪一组ServerEndpointConfig,它们表示服务器可以响应websocket升级请求的所有已部署端点。
这些ServerEndpointConfig对象实例可以来自几个不同的来源。
- 由
javax.websocket.server.ServerContainer.addEndpoint(ServerEndpointConfig)
手动提供- 通常在
javax.servlet.ServletContextInitializer.contextInitialized(ServletContextEvent sce)
- 通常在
- 从
javax.websocket.server.ServerApplicationConfig.getEndpointConfigs(Set)
调用。 - 通过扫描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上进行了以下操作。
- 该path是否与任何ServerEndpointConfig.getPath()条目相匹配
- 如果不匹配,则返回404进行升级
- 将升级请求传递到ServerEndpointConfig.Configurator.checkOrigin()
- 如果无效,则返回错误以升级响应
- 创buildHandshakeResponse
- 将升级请求传递到ServerEndpointConfig.Configurator.getNegotiatedSubprotocol()
- 在HandshakeResponse中存储答案
- 将升级请求传递到ServerEndpointConfig.Configurator.getNegotiatedExtensions()
- 在HandshakeResponse中存储答案
- 创build新的端点特定的ServerEndpointConfig对象。 复制编码器,解码器和用户属性。 这个新的ServerEndpointConfig包装path,扩展,端点类,子协议,configuration器的默认值。
- 将升级请求,响应和新的ServerEndpointConfig传递到ServerEndpointConfig.Configurator.modifyHandshake()
- 调用ServerEndpointConfig.getEndpointClass()
- 使用ServerEndpointConfig.Configurator.getEndpointInstance(Class)上的类
- 创build会话,关联端点实例和EndpointConfig对象。
- 通知连接的端点实例
- 需要EndpointConfig的注释方法获取与此Session相关的方法。
- 调用Session.getUserProperties()返回EndpointConfig.getUserProperties()
要注意,ServerEndpointConfig.Configurator是每个映射的ServerContainer端点的单例。
这是有意的,并且是期望的,以允许实现者多个特征。
- 如果他们愿意,可以为多个对等端返回相同的端点实例。 所谓的无状态方法websocket写作。
- 为所有Endpoint实例拥有单一的昂贵资源pipe理点
如果实现为每个握手创build了一个新的configuration器,这种技术是不可能的。
(披露:我编写和维护Jetty 9的JSR-356实现)
前言
目前还不清楚是否需要HttpServletRequest
, HttpSession
或HttpSession
属性。 我的答案将显示如何获得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。 这将使您可以访问HttpServletRequest
和HttpSession
。 如果它还不存在,它也给你创build一个会话的机会(尽pipe在这种情况下使用HTTP会话似乎是可疑的)。
其次,findWebSocket Session
和HttpServletRequest
或HttpSession
中存在的一些属性。 原来有两个候选者: 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。 看到: