【发布时间】:2013-07-29 23:24:12
【问题描述】:
是否可以在 @ServerEndpoint 中获取 HttpServletRequest?主要是我想得到它,以便我可以访问 HttpSession 对象。
【问题讨论】:
是否可以在 @ServerEndpoint 中获取 HttpServletRequest?主要是我想得到它,以便我可以访问 HttpSession 对象。
【问题讨论】:
更新(2016 年 11 月):此答案中提供的信息适用于 JSR356 规范,该规范的各个实现可能在此信息之外有所不同。在 cmets 和其他答案中找到的其他建议都是 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 知识
EndpointConfig 对象确实存在于每个“端点实例”中。
但是,“端点实例”在规范中有 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 注释类。这些ServerEndpointConfig 对象实例作为javax.websocket.Session 最终创建时的默认值存在。
ServerEndpointConfig.Configurator 实例
在接收或处理任何升级请求之前,所有 ServerEndpointConfig.Configurator 对象现在都存在并准备好执行其主要和唯一目的,以允许自定义升级过程的 websocket 连接到最终的 javax.websocket.Session
访问特定于会话的 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 调用期间的会话参数),通过会话的 CDI 注入,甚至使用使用从 javax.websocket.Endpoint 扩展的非注释 websocket。
升级的工作原理
如前所述,每个已定义的端点都有一个与之关联的ServerEndpointConfig。
那些ServerEndpointConfigs 是代表EndpointConfig 的默认状态的单个实例,最终可供可能并最终创建的端点实例使用。
当一个传入的升级请求到达时,它在 JSR 上经过了以下处理。
请注意,ServerEndpointConfig.Configurator 是一个单例,每个映射的 ServerContainer 端点。
这是有意的,也是期望的,允许实现者使用多个功能。
如果实现为每次握手都创建一个新的配置器,那么这种技术是不可能的。
(披露:我为 Jetty 9 编写和维护 JSR-356 实现)
【讨论】:
@OnOpen 方法的 EndpointConfig 对象不是您的自定义配置对象,但它是默认?这让我不信任用户属性映射。此外,您不必使用 instanceof 运算符。如果未来的程序员不恰当地使用自定义类,我用它来确保我们获得正确的异常类型。 2. 它可能看起来像一个并发缺陷,但事实并非如此。我已经在我的回答中强调了这一点。 JavaDoc 说:“实现为每个逻辑端点创建了一个新的配置器实例。”
javax.websocket 的糟糕规范,而不是“WebSocket 标准”。
modifyHandshake 接收原始的 ServerEndpointConfig 而不是每次握手的副本。与您描述的流程相比,modifyHandshake(第 6 点)甚至发生在 checkOrigin(第 2 点)之前。它与 GlassFish 4 完全相同,所以我看不出使用 userProperties() 比使用配置器的实例变量更线程安全。事实上,我看不出如何使用当前的 JSR 356 实现在配置器和端点实例之间可靠地传递任何东西。
modifyHanshake,因此答案对于 Tomcat 是正确的,但对于 Jetty 和 GlassFish 则不正确。该规范似乎没有指定任何特定的行为。
目前尚不清楚您是否想要HttpServletRequest、HttpSession 或HttpSession 之外的属性。我的回答将展示如何获取HttpSession 或个别属性。
为简洁起见,我省略了 null 和索引边界检查。
这很棘手。 Martin Andersson 的答案不正确,因为每个连接都使用相同的 ServerEndpointConfig.Configurator 实例,因此存在竞争条件。虽然文档声明“实现为每个逻辑端点创建了一个新的配置器实例”,但规范并未明确定义“逻辑端点”。根据使用该短语的所有地方的上下文,它似乎意味着类、配置器、路径和其他选项的绑定,即ServerEndpointConfig,它显然是共享的。无论如何,您可以通过从modifyHandshake(...) 中打印出toString() 来轻松查看实现是否使用相同的实例。
更令人惊讶的是,Joakim Erdfelt 的回答也不能可靠地工作。 JSR 356 的文本本身并没有提到EndpointConfig.getUserProperties(),它只在JavaDoc 中,而且似乎没有指定它与Session.getUserProperties() 的确切关系。在实践中,一些实现(例如 Glassfish)为所有对 ServerEndpointConfig.getUserProperties() 的调用返回相同的 Map 实例,而其他实现(例如 Tomcat 8)则不会。您可以在modifyHandshake(...)内修改之前打印出地图内容进行检查。
为了验证,我直接从其他答案中复制了代码,然后针对我编写的多线程客户端对其进行了测试。在这两种情况下,我都观察到与端点实例关联的会话不正确。
我开发了两种解决方案,在针对多线程客户端进行测试时,我已经验证它们可以正常工作。有两个关键技巧。
首先,使用与 WebSocket 路径相同的过滤器。这将使您可以访问HttpServletRequest 和HttpSession。如果会话尚不存在,它还为您提供了创建会话的机会(尽管在这种情况下使用 HTTP 会话似乎很可疑)。
其次,找到 WebSocket Session 和 HttpServletRequest 或 HttpSession 中都存在的一些属性。原来有两个候选者:getUserPrincipal() 和 getRequestParameterMap()。我会告诉你如何滥用它们:)
最简单的方法是利用Session.getUserPrincipal() 和HttpServletRequest.getUserPrincipal()。缺点是这可能会干扰此属性的其他合法用途,因此请仅在您准备好应对这些影响时才使用它。
如果您只想存储一个字符串,例如用户 ID,这实际上并不算滥用,尽管它可能应该以某种容器管理的方式设置,而不是像我将向您展示的那样覆盖包装器.无论如何,您只需覆盖Principal.getName() 即可。然后,您甚至不需要将其转换为 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(),但在这种情况下它是安全的,因为我们总是将相同的对象放入地图中,因此它是否共享没有区别。唯一会话标识符不是通过用户参数传递,而是通过请求参数传递,每个请求都是唯一的。
这个解决方案稍微不那么hacky,因为它不会干扰用户主体属性。请注意,如果除了插入的请求参数之外,您还需要传递 实际 请求参数,您可以轻松地这样做:只需从现有请求参数映射开始,而不是像此处所示的新的空请求参数映射.但请注意,用户不能通过在实际 HTTP 请求中提供同名的请求参数来欺骗过滤器中添加的特殊参数。
/* 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);
}
}
}
如果您只需要HttpSession 之外的某些属性而不是整个HttpSession 本身,例如用户ID,那么您可以取消整个SessionTracker 业务,只需将必要的参数放入映射您从覆盖 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);
}
}
【讨论】:
session.getUserPrincipal() 总是返回 null。 Undertow WS 是否可能不使用来自过滤器的 servlet 请求?
让我们回顾一下Java API for WebSocket 规范,看看是否可以获取HttpSession 对象。 specification 在第 29 页上说:
因为 websocket 连接是通过 http 请求启动的, 客户端下的 HttpSession 之间存在关联 正在运行,并且在其中建立的任何 websocket HttpSession。 API 允许在打开握手时访问 对应同一个客户端的唯一 HttpSession。
所以是的,这是可能的。
但是,我认为您不可能获得对HttpServletRequest 对象的引用。您可以使用ServletRequestListener 侦听所有 个新的 servlet 请求,但您仍然需要确定哪个请求属于哪个服务器端点。如果您找到解决方案,请告诉我!
如何在规范的第 13 页和第 14 页上进行了粗略的描述,并由我在下一个标题下的代码中举例说明。
在英语中,我们需要拦截握手过程以获取HttpSession 对象。为了然后将 HttpSession 引用传输到我们的服务器端点,我们还需要在容器创建服务器端点实例时进行拦截并手动注入引用。我们通过提供自己的ServerEndpointConfig.Configurator 并覆盖modifyHandshake() 和getEndpointInstance() 方法来完成所有这些工作。
自定义配置器将根据逻辑ServerEndpoint 实例化一次(请参阅JavaDoc)。
这是服务器端点类(我在这段代码sn-p之后提供了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;
}
}
【讨论】:
ConnectionWebSocket?
以上所有答案都值得一读,但没有一个能解决 OP(和我)的问题。
您可以在 WS 端点打开时访问 HttpSession 并将其传递给新创建的端点实例,但没有人保证存在 HttpSession 实例!
所以我们需要在这次黑客攻击之前第 0 步(我讨厌 WebSocket 的 JSR 365 实现)。 Websocket - httpSession returns null
【讨论】:
所有可能的解决方案都基于:
A.客户端浏览器实现通过作为 HTTP 标头传递的 Cookie 值维护会话 ID,或者(如果禁用 cookie)由 Servlet 容器管理,该容器将为生成的 URL 生成会话 ID 后缀
B.您只能在 HTTP 握手期间访问 HTTP 请求标头;之后就是Websocket协议
所以……
方案一:使用“握手”访问HTTP
解决方案 2:在客户端的 JavaScript 中,动态生成 HTTP 会话 ID 参数并发送包含此会话 ID 的第一条消息(通过 Websocket)。将“端点”连接到维护 Session ID -> Session 映射的缓存/实用程序类;避免内存泄漏,您可以使用 Session Listener 例如从缓存中删除会话。
附:感谢 Martin Andersson 和 Joakim Erdfelt 的回答。 不幸的是,Martin 的解决方案不是线程安全的......
【讨论】:
适用于所有应用服务器的唯一方法是使用 ThreadLocal。见:
【讨论】: