48

是否可以在@ServerEndpoint 中获取 HttpServletRequest?主要是我想得到它,所以我可以访问 HttpSession 对象。

4

6 回答 6

83

更新(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 个含义。

  1. JSR 的默认行为,其中每个传入的升级请求都会导致端点类的新对象实例
  2. javax.websocket.Session对象端点实例及其配置与特定逻辑连接联系在一起的A。

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

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. @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通过@OnMessageSession 的 CDI 注入,甚至使用从javax.websocket.Endpoint.

升级的工作原理

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

这些是代表最终可用于可能且最终创建的端点实例ServerEndpointConfigs的默认状态的单个实例。EndpointConfig

当传入的升级请求到达时,它在 JSR 上经过了以下处理。

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

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

这是有意的,也是期望的,以允许实现者有几个特性。

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

如果实现为每次握手都创建了一个新的配置器,那么这种技术是不可能的。

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

于 2013-08-01T12:38:18.453 回答
20

前言

目前尚不清楚您是否想要HttpServletRequestHttpSession或 属性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 路径相同的过滤器。这将使您可以访问HttpServletRequestand HttpSession。如果会话尚不存在,它还为您提供了创建会话的机会(尽管在这种情况下使用 HTTP 会话似乎很可疑)。

其次,找到一些同时存在于 WebSocketSessionHttpServletRequestor中的属性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()。然后你也可以摆脱习惯ConfiguratorSession.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);
    }
}
于 2015-05-12T00:58:42.360 回答
7

可能吗?

让我们回顾一下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;
    }
}
于 2013-07-31T11:39:31.600 回答
4

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

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

所以我们需要在这次黑客攻击之前第 0 步(我讨厌 WebSocket 的 JSR 365 实现)。 Websocket - httpSession 返回 null

于 2014-11-28T05:59:23.560 回答
0

所有可能的解决方案都基于:

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 的回答。不幸的是,马丁的解决方案不是线程安全的......

于 2014-09-17T17:01:36.503 回答
0

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

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

于 2017-02-10T16:51:59.977 回答