是否可以在@ServerEndpoint 中获取 HttpServletRequest?主要是我想得到它,所以我可以访问 HttpSession 对象。
6 回答
更新(2016 年 11 月):此答案中提供的信息适用于 JSR356 规范,该规范的各个实现可能在此信息之外有所不同。在评论和其他答案中找到的其他建议都是 JSR356 规范之外的特定于实现的行为。
如果此处的建议给您带来问题,请升级您的各种 Jetty、Tomcat、Wildfly 或 Glassfish/Tyrus 安装。据报道,这些实现的所有当前版本都以下面概述的方式工作。
现在回到2013 年 8 月的原始答案......
Martin Andersson 的回答存在并发缺陷。Configurator 可以被多个线程同时调用,很可能在从modifyHandshake()
和的调用之间您将无法访问正确的 HttpSession 对象getEndpointInstance()
。
或者换一种说法...
- 请求 A
- 修改握手 A
- 请求 B
- 修改握手 B
- 获取端点实例 A <-- 这将具有请求 B 的 HttpSession
- 获取端点实例 B
这是对 Martin 代码的修改,它使用ServerEndpointConfig.getUserProperties()
map在方法调用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);
}
}
获取HttpSessionSocket.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 个含义。
- JSR 的默认行为,其中每个传入的升级请求都会导致端点类的新对象实例
- 将
javax.websocket.Session
对象端点实例及其配置与特定逻辑连接联系在一起的A。
可以将单个 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 连接的升级过程定制到最终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,该调用始终可用。此用户属性映射始终可用,无论是通过带注释的技术(例如 、 、 或调用期间的 Session 参数@OnOpen
)@OnClose
,@OnError
通过@OnMessage
Session 的 CDI 注入,甚至使用从javax.websocket.Endpoint
.
升级的工作原理
如前所述,每一个定义的端点都会有一个ServerEndpointConfig
与之关联的。
这些是代表最终可用于可能且最终创建的端点实例ServerEndpointConfigs
的默认状态的单个实例。EndpointConfig
当传入的升级请求到达时,它在 JSR 上经过了以下处理。
- 路径是否与任何 ServerEndpointConfig.getPath() 条目匹配
- 如果不匹配,返回 404 进行升级
- 将升级请求传递到 ServerEndpointConfig.Configurator.checkOrigin()
- 如果无效,则返回错误升级响应
- 创建握手响应
- 将升级请求传递到 ServerEndpointConfig.Configurator.getNegotiatedSubprotocol()
- 在 HandshakeResponse 中存储答案
- 将升级请求传递到 ServerEndpointConfig.Configurator.getNegotiatedExtensions()
- 在 HandshakeResponse 中存储答案
- 创建新的端点特定的 ServerEndpointConfig 对象。复制编码器、解码器和用户属性。这个新的 ServerEndpointConfig 包装了路径、扩展、端点类、子协议、配置器的默认值。
- 将升级请求、响应和新的 ServerEndpointConfig 传递到 ServerEndpointConfig.Configurator.modifyHandshake()
- 调用 ServerEndpointConfig.getEndpointClass()
- 在 ServerEndpointConfig.Configurator.getEndpointInstance(Class) 上使用类
- 创建 Session,关联端点实例和 EndpointConfig 对象。
- 通知连接的端点实例
- 需要 EndpointConfig 的注释方法获取与此 Session 关联的方法。
- 调用 Session.getUserProperties() 返回 EndpointConfig.getUserProperties()
需要注意的是,ServerEndpointConfig.Configurator 是一个单例,每个映射的 ServerContainer 端点。
这是有意的,也是期望的,以允许实现者有几个特性。
- 如果他们愿意,为多个对等点返回相同的端点实例。所谓的 websocket 编写的无状态方法。
- 对所有 Endpoint 实例的昂贵资源进行单点管理
如果实现为每次握手都创建了一个新的配置器,那么这种技术是不可能的。
(披露:我为 Jetty 9 编写和维护 JSR-356 实现)
前言
目前尚不清楚您是否想要HttpServletRequest
、HttpSession
或 属性HttpSession
。我的回答将显示如何获取HttpSession
或单个属性。
为简洁起见,我省略了空值和索引边界检查。
注意事项
这很棘手。Martin Andersson 的答案不正确,因为ServerEndpointConfig.Configurator
每个连接都使用相同的实例,因此存在竞争条件。虽然文档声明“实现为每个逻辑端点创建了一个新的配置器实例”,但规范并未明确定义“逻辑端点”。根据使用该短语的所有地方的上下文,它似乎意味着类、配置器、路径和其他选项的绑定,即 a ServerEndpointConfig
,这是明确共享的。toString()
无论如何,您可以通过从内部打印出它来轻松查看实现是否使用相同的实例modifyHandshake(...)
。
更令人惊讶的是,Joakim Erdfelt 的回答也不能可靠地工作。JSR 356 本身的文本没有提及EndpointConfig.getUserProperties()
,它只在 JavaDoc 中,并且似乎没有指定它与Session.getUserProperties()
. 在实践中,一些实现(例如,Glassfish)Map
为所有调用返回相同的实例,ServerEndpointConfig.getUserProperties()
而其他实现(例如,Tomcat 8)则不会。您可以通过在修改地图内容之前打印出地图内容进行检查modifyHandshake(...)
。
为了验证,我直接从其他答案中复制了代码,然后针对我编写的多线程客户端对其进行了测试。在这两种情况下,我都观察到与端点实例关联的会话不正确。
解决方案概要
我开发了两个解决方案,在针对多线程客户端进行测试时,我已经验证它们可以正常工作。有两个关键技巧。
首先,使用与 WebSocket 路径相同的过滤器。这将使您可以访问HttpServletRequest
and HttpSession
。如果会话尚不存在,它还为您提供了创建会话的机会(尽管在这种情况下使用 HTTP 会话似乎很可疑)。
其次,找到一些同时存在于 WebSocketSession
和HttpServletRequest
or中的属性HttpSession
。原来有两个候选者:getUserPrincipal()
和getRequestParameterMap()
。我会告诉你如何滥用他们两个:)
使用用户主体的解决方案
最简单的方法是利用Session.getUserPrincipal()
and HttpServletRequest.getUserPrincipal()
。缺点是这可能会干扰该属性的其他合法用途,因此只有在您准备好应对这些影响时才使用它。
如果您只想存储一个字符串,例如用户 ID,这实际上并不算滥用,尽管它可能应该以某种容器管理的方式设置,而不是像我将向您展示的那样覆盖包装器。无论如何,您只需覆盖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, e.g., 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();
}
}
使用请求参数的解决方案
第二个选项使用Session.getRequestParameterMap()
and HttpServletRequest.getParameterMap()
。请注意,它使用ServerEndpointConfig.getUserProperties()
但在这种情况下是安全的,因为我们总是将相同的对象放入地图中,所以它是否共享没有区别。唯一会话标识符不是通过用户参数传递,而是通过请求参数传递,每个请求都是唯一的。
这个解决方案稍微不那么hacky,因为它不会干扰用户的主体属性。请注意,如果除了插入的请求参数之外,您还需要传递实际的请求参数,您可以轻松地这样做:只需从现有的请求参数映射开始,而不是此处显示的新的空请求参数映射。但请注意,用户不能通过在实际 HTTP 请求中提供同名的请求参数来欺骗过滤器中添加的特殊参数。
会话跟踪器.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);
}
}
}
单一属性的解决方案
如果您只需要某些属性HttpSession
而不是整个HttpSession
本身,例如用户 ID,那么您可以取消整个SessionTracker
业务,只需将必要的参数放在您从覆盖返回的映射中HttpServletRequestWrapper.getParameterMap()
。然后你也可以摆脱习惯Configurator
;Session.getRequestParameterMap()
您可以从Endpoint方便地访问您的属性。
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);
}
}
可能吗?
让我们回顾一下Java API for WebSocket规范,看看是否可以获取HttpSession
对象。该规范在第 29 页上说:
因为 websocket 连接是通过 http 请求启动的,所以在客户端运行的 HttpSession 与在该 HttpSession 中建立的任何 websocket 之间存在关联。API 允许在打开握手时访问与同一客户端对应的唯一 HttpSession。
所以是的,这是可能的。
但是,我认为您不可能获得对该HttpServletRequest
对象的引用。您可以使用 侦听所有新的 servlet 请求ServletRequestListener
,但您仍然需要确定哪个请求属于哪个服务器端点。如果您找到解决方案,请告诉我!
摘要方法
How-to 在规范的第 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;
}
}
以上所有答案都值得一读,但没有一个能解决 OP(和我)的问题。
您可以在 WS 端点打开时访问 HttpSession 并将其传递给新创建的端点实例,但没有人保证存在 HttpSession 实例!
所以我们需要在这次黑客攻击之前第 0 步(我讨厌 WebSocket 的 JSR 365 实现)。 Websocket - httpSession 返回 null
所有可能的解决方案都基于:
A. 客户端浏览器实现通过作为 HTTP 标头传递的 Cookie 值维护会话 ID,或者(如果禁用 cookie)由 Servlet 容器管理,该容器将为生成的 URL 生成会话 ID 后缀
B. 只能在 HTTP 握手期间访问 HTTP Request Headers;之后就是 Websocket 协议
以便...
方案一:使用“握手”访问HTTP
解决方案 2:在客户端的 JavaScript 中,动态生成 HTTP 会话 ID 参数并发送包含此会话 ID 的第一条消息(通过 Websocket)。将“端点”连接到维护 Session ID -> Session 映射的缓存/实用程序类;避免内存泄漏,您可以使用 Session Listener 例如从缓存中删除会话。
PS 我感谢 Martin Andersson 和 Joakim Erdfelt 的回答。不幸的是,马丁的解决方案不是线程安全的......
在所有应用程序服务器上工作的唯一方法是使用 ThreadLocal。看: