是否可以在@ServerEndpoint中获取HttpServletRequest?主要是我试图得到它,所以我可以访问HttpSession对象。
答案 0 :(得分:72)
更新(2016年11月):此答案中提供的信息适用于JSR356规范,规范的各个实现可能会在此信息之外发生变化。在评论和其他答案中找到的其他建议都是JSR356规范之外的特定于实现的行为。
如果此处的建议导致您出现问题,请升级Jetty,Tomcat,Wildfly或Glassfish / Tyrus的各种安装。据报道,这些实施的所有当前版本都按照下面概述的方式工作。
现在回到 2013年8月的原始答案 ...
Martin Andersson的回答有并发性缺陷。 Configurator可以同时被多个线程调用,很可能您无法在modifyHandshake()
和getEndpointInstance()
的调用之间访问正确的HttpSession对象。
或者说另一种方式......
以下是对Martin代码的修改,该代码在ServerEndpointConfig.getUserProperties()
方法调用期间使用HttpSession
映射使@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);
}
}
奖励功能:无instanceof
或需要投射。
某些EndpointConfig知识
每个&#34;端点实例&#34;。确实存在 EndpointConfig
个对象
然而,&#34;端点实例&#34;与规范有2个含义。
javax.websocket.Session
。可以将单个Endpoint实例用于多个javax.websocket.Session
实例(这是ServerEndpointConfig.Configurator
支持的功能之一)
ServerContainer实现将跟踪一组ServerEndpointConfig,它们代表服务器可以响应websocket升级请求的所有已部署端点。
这些ServerEndpointConfig对象实例可以来自几个不同的来源。
javax.websocket.server.ServerContainer.addEndpoint(ServerEndpointConfig)
手动提供
javax.servlet.ServletContextInitializer.contextInitialized(ServletContextEvent sce)
电话javax.websocket.server.ServerApplicationConfig.getEndpointConfigs(Set)
来电。@ServerEndpoint
带注释的类的Web应用程序自动创建。这些ServerEndpointConfig
个对象实例作为最终创建javax.websocket.Session
时的默认值存在。
ServerEndpointConfig.Configurator实例
在收到或处理任何升级请求之前,所有ServerEndpointConfig.Configurator
对象现在都已存在并准备好执行其主要目的,以允许自定义websocket连接到最终的升级过程{ {1}}
访问特定于会话的EndpointConfig
注意,您无法从端点实例中访问javax.websocket.Session
个对象实例。您只能访问ServerEndpointConfig
个实例。
这意味着如果您在部署期间提供EndpointConfig
并稍后尝试通过注释访问它,则无法使用。
以下所有内容均无效。
ServerContainer.addEndpoint(new MyCustomServerEndpointConfig())
您可以在Endpoint对象实例的生命周期内访问EndpointConfig,但是在有限的时间内。 @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
*/
}
,带注释的javax.websocket.Endpoint.onOpen(Session,Endpoint)
方法,或通过使用CDI。 EndpointConfig不能以任何其他方式或在任何其他时间使用。
但是,您始终可以通过@OnOpen
调用访问UserProperties,该调用始终可用。通过CDI,此用户属性映射始终可用,通过带注释的技术(例如Session.getUserProperties()
期间的会话参数,@OnOpen
,@OnClose
或@OnError
调用)注入Session,甚至使用从@OnMessage
延伸的非注释的websockets。
升级如何运作
如前所述,每个定义的端点都会有javax.websocket.Endpoint
与之关联。
那些ServerEndpointConfig
是一个单个实例,表示ServerEndpointConfigs
的默认状态,最终可用于最终可能并最终创建的端点实例。
当传入的升级请求到达时,它已经在JSR上进行了以下操作。
要注意,ServerEndpointConfig.Configurator是每个映射的ServerContainer端点的单例。
这是有意的,也是期望的,允许实现者有几个功能。
如果实现为每次握手创建了一个新的Configurator,那么这种技术是不可能的。
(披露:我编写并维护Jetty 9的JSR-356实现)
答案 1 :(得分:17)
目前还不清楚您是否想要HttpServletRequest
,HttpSession
或HttpSession
中的HttpSession
属性。我的答案将说明如何获取ServerEndpointConfig.Configurator
或个别属性。
为了简洁起见,我省略了null和索引边界检查。
这很棘手。 Martin Andersson的答案不正确,因为ServerEndpointConfig
的相同实例用于每个连接,因此存在竞争条件。虽然文档声明&#34;实现为每个逻辑端点创建配置器的新实例,&#34;规范没有明确定义一个&#34;逻辑端点。&#34;基于所使用的短语的所有位置的上下文,它似乎意味着类,配置器,路径和其他选项的绑定,即明确共享的toString()
。无论如何,您可以通过在modifyHandshake(...)
内打印出EndpointConfig.getUserProperties()
来轻松查看实现是否正在使用同一个实例。
更令人惊讶的是,Joakim Erdfelt的答案也无法可靠地运作。 JSR 356本身的文本没有提到Session.getUserProperties()
,它只在JavaDoc中,似乎没有指明它与Map
的确切关系。在实践中,一些实现(例如,Glassfish)为ServerEndpointConfig.getUserProperties()
的所有呼叫返回相同的modifyHandshake(...)
实例,而其他实例(例如,Tomcat 8)则不返回。您可以在HttpServletRequest
中修改地图内容之前打印出地图内容进行检查。
为了验证,我直接从其他答案中复制了代码,然后针对我编写的多线程客户端对其进行了测试。在这两种情况下,我都观察到与端点实例关联的错误会话。
我已经开发了两个解决方案,在针对多线程客户端进行测试时,我已经验证了这些解决方案的正常工作。有两个关键技巧。
首先,使用与WebSocket具有相同路径的过滤器。这样,您就可以访问HttpSession
和Session
。它还为您提供了创建会话的机会,如果它尚未存在(尽管在这种情况下使用HTTP会话似乎很可疑)。
其次,找到WebSocket HttpServletRequest
和HttpSession
或getUserPrincipal()
中存在的一些属性。事实证明,有两个候选人:getRequestParameterMap()
和Session.getUserPrincipal()
。我会告诉你如何滥用它们:)
最简单的方法是利用HttpServletRequest.getUserPrincipal()
和Principal.getName()
。缺点是这可能会干扰此属性的其他合法使用,因此只有在您为这些影响做好准备时才使用它。
如果你只想存储一个字符串,例如用户ID,这实际上并没有太大的滥用,虽然它可能应该以某种容器管理的方式设置而不是覆盖包装器,因为我会这样做。告诉你无论如何,你只需覆盖Endpoint
即可。那你甚至不需要把它投在HttpSession
。但是如果你可以忍受它,你也可以按如下方式传递整个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();
}
}
Session.getRequestParameterMap()
第二个选项使用HttpServletRequest.getParameterMap()
和ServerEndpointConfig.getUserProperties()
。请注意,它使用/* 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);
}
}
但在这种情况下它是安全的,因为我们总是将相同的对象放入地图中,因此它是否共享没有区别。唯一会话标识符不是通过用户参数传递,而是通过请求参数传递, 每个请求唯一。
这个解决方案稍微不那么狡猾,因为它不会干扰用户主要属性。请注意,如果您需要通过实际请求参数以及插入的参数,您可以轻松地执行此操作:只需从现有的请求参数映射开始,而不是新的空一个如此处所示。但请注意,用户不能通过在实际HTTP请求中使用相同名称提供自己的请求参数来欺骗过滤器中添加的特殊参数。
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);
}
}
}
HttpSession
如果您只需要HttpSession
中的某些属性而不是整个SessionTracker
本身,例如说出用户ID,那么您可以取消整个HttpServletRequestWrapper.getParameterMap()
业务,将必要参数放在您从覆盖Configurator
返回的地图中。然后你也可以摆脱自定义Session.getRequestParameterMap()
;您可以通过端点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);
}
}
{{1}}
答案 2 :(得分:7)
让我们回顾一下 Java API for WebSocket 规范,看看是否可以获取HttpSession
对象。 specification在第29页上说明了:
因为websocket连接是使用http请求启动的, 客户端下的HttpSession之间存在关联 正在运行,并在其中建立任何websockets HttpSession中。 API允许在打开握手时访问 与同一客户端对应的唯一HttpSession。
所以是的,这是可能的。
但是,我认为您不可能获得对HttpServletRequest
对象的引用。您可以使用ServletRequestListener
侦听所有新的servlet请求,但您仍然需要确定哪个请求属于哪个服务器端点。如果您找到解决方案,请告诉我们!
规范中的第13页和第14页详细描述了方法,并在下一个标题的代码中以我为例。
在英语中,我们需要拦截握手过程以获取HttpSession
对象。然后,为了将HttpSession引用传递给我们的服务器端点,我们还需要在容器创建服务器端点实例并手动注入引用时进行拦截。我们通过提供自己的ServerEndpointConfig.Configurator
并覆盖方法modifyHandshake()
和getEndpointInstance()
来完成所有这些工作。
自定义配置程序将按逻辑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());
}
}
这是自定义配置程序:
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;
}
}
答案 3 :(得分:4)
以上所有答案都值得一读,但没有一个能解决OP(和我)的问题。
您可以在WS端点打开时访问HttpSession并将其传递给新创建的端点实例,但没有人保证存在HttpSession实例!
所以我们在此黑客攻击之前需要第0步(我讨厌WebSocket的JSR 365实现)。 Websocket - httpSession returns null
答案 4 :(得分:0)
所有可能的解决方案均基于:
一个。客户端浏览器实现通过作为HTTP头传递的Cookie值维护会话ID,或者(如果禁用cookie)它由Servlet容器管理,该容器将为生成的URL生成会话ID后缀
B中。您只能在HTTP握手期间访问HTTP请求标头;之后,它是Websocket协议
那样......
解决方案1:使用&#34;握手&#34;访问HTTP
解决方案2:在客户端的JavaScript中,动态生成HTTP会话ID参数,并发送包含此会话ID的第一条消息(通过Websocket)。连接&#34;端点&#34;到缓存/实用程序类维护会话ID - &gt;会话映射;避免内存泄漏,例如,您可以使用会话侦听器从缓存中删除会话。
P.S。我很欣赏Martin Andersson和Joakim Erdfelt的答案。 不幸的是,Martin的解决方案不是线程安全的......
答案 5 :(得分:0)
跨所有应用程序服务器的唯一方法是使用ThreadLocal。参见: