从Web Socket @ServerEndpoint中的HttpServletRequest访问HttpSession

Arc*_*ano 45 websocket java-ee-7

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

Joa*_*elt 72

更新(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);
    }
}
Run Code Online (Sandbox Code Playgroud)

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);
    }
}
Run Code Online (Sandbox Code Playgroud)

额外功能:不需要instanceof或需要铸造.

一些EndpointConfig知识

EndpointConfig 每个"端点实例"都存在对象.

但是,"端点实例"与规范有2个含义.

  1. JSR的默认行为,其中每个传入的升级请求都会生成端点类的新对象实例
  2. javax.websocket.Session将对象端点实例及其配置连接到特定逻辑连接的A.

可以将单个端点实例用于多个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. 通过扫描Web应用程序为@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
     */
}
Run Code Online (Sandbox Code Playgroud)

您可以在Endpoint对象实例的生命周期内访问EndpointConfig,但是在有限的时间内.的javax.websocket.Endpoint.onOpen(Session,Endpoint),带有注释的@OnOpen方法,或通过使用CDI的.EndpointConfig不以任何其他方式或任何其他时间提供.

但是,您始终可以通过Session.getUserProperties()呼叫访问UserProperties,该呼叫始终可用.此用户属性地图总是可用的,通过注释的技术(如在会话期间的参数是它@OnOpen,@OnClose,@OnError,或@OnMessage呼叫),通过CDI注射会话的,或者即使使用,从延长非注释的WebSockets的javax.websocket.Endpoint.

升级如何运作

如前所述,每个定义的端点都将ServerEndpointConfig与之关联.

这些ServerEndpointConfigs是单个实例,表示EndpointConfig最终可用于最终创建的端点实例的默认状态.

当传入的升级请求到达时,它已在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. 创建Session,关联端点实例和EndpointConfig对象.
  10. 通知连接的端点实例
  11. 需要EndpointConfig的带注释的方法获取与此Session关联的方法.
  12. 调用Session.getUserProperties()返回EndpointConfig.getUserProperties()

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

这是有意的,并且是期望的,以允许实现者具有若干特征.

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

如果实现为每次握手创建了新的Configurator,则该技术将无法实现.

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

  • **1.**您是否知道传递给`@OnOpen`方法的`EndpointConfig`对象,如果您正在使用Tyrus,则不是您的自定义配置对象,但它是默认值?这让我不相信用户属性图.此外,您不必使用instanceof运算符.如果未来的程序员不恰当地使用自定义类,我用它来确保我们得到一个合适的异常类型.**2.**它可能看起来像并发漏洞,但事实并非如此.我已经在答案中强调了这一点.JavaDoc说:"实现为每个逻辑端点创建一个配置器的新实例." (3认同)
  • @studro它实际上指的是`javax.websocket`的一个糟糕的规范,而不是"WebSocket标准". (3认同)
  • 为了完成,Tomcat确实将每会话endpointConfig传递给`modifyHanshake`,因此Tomcat的答案是正确的,但Jetty和GlassFish的答案是正确的.规范似乎没有指定任何特定的行为. (3认同)

Cha*_*N B 18

前言

目前还不清楚你是否想要HttpServletRequest这些HttpSession,或者属性HttpSession.我的答案将展示如何获得HttpSession或个人财产.

为简洁起见,我省略了null和索引边界检查.

注意事项

这很棘手.Martin Andersson的答案是不正确的,因为ServerEndpointConfig.Configurator每个连接使用相同的实例,因此存在竞争条件.虽然文档声明"实现为每个逻辑端点创建配置器的新实例",但规范并未明确定义"逻辑端点".基于使用该短语的所有地方的上下文,它似乎意味着类,配置器,路径和其他选项的绑定,即ServerEndpointConfig明确共享的a.无论如何,您可以通过toString()从内部打印出来来轻松查看实现是否正在使用相同的实例modifyHandshake(...).

更令人惊讶的是,Joakim Erdfelt的答案也无法可靠地运作.JSR 356本身的文本没有提到EndpointConfig.getUserProperties(),它只在JavaDoc中,并且似乎没有指明它与它的确切关系Session.getUserProperties().在实践中,一些实现(例如,Glassfish)Map为所有调用返回相同的实例,ServerEndpointConfig.getUserProperties()而其他实现(例如,Tomcat 8)则不返回.您可以在打印之前打印出地图内容进行检查modifyHandshake(...).

为了验证,我直接从其他答案中复制了代码,然后针对我编写的多线程客户端对其进行了测试.在这两种情况下,我都观察到与端点实例关联的错误会话.

解决方案概要

我已经开发了两个解决方案,在针对多线程客户端进行测试时,我已经验证了它是否正常工作.有两个关键技巧.

首先,使用与WebSocket具有相同路径的过滤器.这将使您访问HttpServletRequestHttpSession.它还为您提供了创建会话的机会(如果它尚不存在)(尽管在这种情况下使用HTTP会话似乎很可疑).

其次,找到WebSocket SessionHttpServletRequestor 中存在的一些属性HttpSession.事实证明有两个候选人:getUserPrincipal()getRequestParameterMap().我会告诉你如何滥用他们:)

使用用户主体的解决方案

最简单的方法是利用Session.getUserPrincipal()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
    }
}
Run Code Online (Sandbox Code Playgroud)

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() { }
}
Run Code Online (Sandbox Code Playgroud)

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();
    }
}
Run Code Online (Sandbox Code Playgroud)

解决方案使用请求参数

第二个选项使用Session.getRequestParameterMap()HttpServletRequest.getParameterMap().请注意它使用ServerEndpointConfig.getUserProperties()但在这种情况下它是安全的,因为我们总是将相同的对象放入地图中,因此它是否共享没有区别.唯一会话标识符不是通过用户参数传递,而是通过请求参数传递,该请求参数对于每个请求唯一的.

此解决方案稍微不那么苛刻,因为它不会干扰用户主要属性.请注意,如果您需要传递除插入的参数之外的实际请求参数,您可以轻松地执行此操作:只需从现有的请求参数映射开始,而不是如此处所示的新空映射映射.但请注意,用户不能通过在实际HTTP请求中使用相同名称提供自己的请求参数来欺骗过滤器中添加的特殊参数.

SessionTracker.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);
    }
}
Run Code Online (Sandbox Code Playgroud)

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() { }
}
Run Code Online (Sandbox Code Playgroud)

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);
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

单一属性的解决方案

如果您只需要某些属性HttpSession而不是整个HttpSession本身,比如说一个用户ID,那么您可以取消整个SessionTracker业务,只需将必要的参数放入您从覆盖中返回的地图中HttpServletRequestWrapper.getParameterMap().然后你也可以摆脱习俗Configurator; 您可以从Session.getRequestParameterMap()端点方便地访问您的房产.

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 {
    }
}
Run Code Online (Sandbox Code Playgroud)

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);
    }
}
Run Code Online (Sandbox Code Playgroud)


Mar*_*son 7

可能吗?

让我们回顾一下Java API for WebSocket规范,看看是否可以获取HttpSession对象.该规范说,第29页:

因为websocket连接是使用http请求启动的,所以在客户端运行的HttpSession和在该HttpSession中建立的任何websockets之间存在关联.API允许在打开握手中访问对应于同一客户端的唯一HttpSession.

所以是的,这是可能的.

但是,我认为你不可能获得HttpServletRequest对象的引用.您可以使用a 监听所有新的servlet请求ServletRequestListener,但您仍然需要确定哪个请求属于哪个服务器端点.如果您找到解决方案,请告诉我们!

抽象的方法

如何在说明书的第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());
    }
}
Run Code Online (Sandbox Code Playgroud)

这是自定义配置器:

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;
    }
}
Run Code Online (Sandbox Code Playgroud)

  • 妈的.抱歉! 从已经实现的解决方案复制粘贴该部分.在我的情况下,端点被称为ConnectionWebSocket =)我编辑了答案.谢谢!如果它对您有用,请不要忘记upvote. (2认同)