从Web Socket @ServerEndpoint中的HttpServletRequest访问HttpSession

时间:2013-07-30 00:26:27

标签: websocket java-ee-7

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

6 个答案:

答案 0 :(得分:72)

  

更新(2016年11月):此答案中提供的信息适用于JSR356规范,规范的各个实现可能会在此信息之外发生变化。在评论和其他答案中找到的其他建议都是JSR356规范之外的特定于实现的行为。

     

如果此处的建议导致您出现问题,请升级Jetty,Tomcat,Wildfly或Glassfish / Tyrus的各种安装。据报道,这些实施的所有当前版本都按照下面概述的方式工作。

现在回到 2013年8月的原始答案 ...

Martin Andersson的回答有并发性缺陷。 Configurator可以同时被多个线程调用,很可能您无法在modifyHandshake()getEndpointInstance()的调用之间访问正确的HttpSession对象。

或者说另一种方式......

  • 申请A
  • 修改握手A
  • 请求B
  • 修改握手B
  • 获取端点实例A< - 这将有请求B的HttpSession
  • 获取端点实例B

以下是对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个含义。

  1. JSR的默认行为,其中每个传入的升级请求都会生成端点类的新对象实例
  2. 将对象端点实例及其配置连接到特定逻辑连接的javax.websocket.Session
  3. 可以将单个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应用程序自动创建。
    4. 这些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上进行了以下操作。

      1. 路径是否与任何ServerEndpointConfig.getPath()条目匹配
        • 如果不匹配,请返回404升级
      2. 将升级请求传递到ServerEndpointConfig.Configurator.checkOrigin()
        • 如果无效,则返回错误以升级响应
        • 创建HandshakeResponse
      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)上使用class
      9. 创建会话,关联端点实例和EndpointConfig对象。
      10. 通知连接的端点实例
      11. 需要EndpointConfig的带注释的方法获取与此Session相关联的方法。
      12. 调用Session.getUserProperties()返回EndpointConfig.getUserProperties()
      13. 要注意,ServerEndpointConfig.Configurator是每个映射的ServerContainer端点的单例。

        这是有意的,也是期望的,允许实现者有几个功能。

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

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

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

答案 1 :(得分:17)

前言

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

其次,找到WebSocket HttpServletRequestHttpSessiongetUserPrincipal()中存在的一些属性。事实证明,有两个候选人: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 } } 对象。

PrincipalWithSession.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() { }
}

WebSocketFilter.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();
    }
}

WebSocketEndpoint.java

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请求中使用相同名称提供自己的请求参数来欺骗过滤器中添加的特殊参数。

SessionTracker.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() { }
}

WebSocketFilter.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);
        }
    }
}

WebSocketEndpoint.java

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 { } } 方便地访问您的媒体资源。

WebSocketFilter.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);
    }
}

WebSocketEndpoint.java

{{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。参见:

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