web-dev-qa-db-ja.com

Web Socket @ServerEndpointのHttpServletRequestからHttpSessionにアクセスする

@ServerEndpoint内でHttpServletRequestを取得することは可能ですか?主に、HttpSessionオブジェクトにアクセスできるように取得しようとしています。

45

序文

HttpServletRequestHttpSession、またはHttpSessionのプロパティのどれが必要かは不明です。私の答えは、HttpSessionまたは個々のプロパティを取得する方法を示します。

簡潔にするために、nullとインデックスの境界チェックを省略しました。

ご注意

トリッキーです。 _ServerEndpointConfig.Configurator_の同じインスタンスがすべての接続に使用されるため、Martin Anderssonからの答えは正しくありません。したがって、競合状態が存在します。ドキュメントでは「実装は論理エンドポイントごとにコンフィグレータの新しいインスタンスを作成します」と述べていますが、仕様は「論理エンドポイント」を明確に定義していません。フレーズが使用されるすべての場所のコンテキストに基づいて、クラス、コンフィギュレーター、パス、および他のオプション、つまり、明らかに共有されるServerEndpointConfigのバインディングを意味するように見えます。とにかく、toString()内からmodifyHandshake(...)を出力することで、実装が同じインスタンスを使用しているかどうかを簡単に確認できます。

さらに驚くべきことに、Joakim Erdfeltからの回答も確実に機能しません。 JSR 356のテキスト自体はEndpointConfig.getUserProperties()に言及しておらず、JavaDocにのみ記載されており、Session.getUserProperties()との正確な関係がどこにも指定されていないようです。実際には、一部の実装(Glassfishな​​ど)は、ServerEndpointConfig.getUserProperties()へのすべての呼び出しに対して同じMapインスタンスを返しますが、他の実装(Tomcat 8など)は返しません。 modifyHandshake(...)内で変更する前に、マップの内容を印刷して確認できます。

確認するために、他の回答からコードを直接コピーし、作成したマルチスレッドクライアントに対してテストしました。どちらの場合も、エンドポイントインスタンスに関連付けられている誤ったセッションを観察しました。

ソリューションの概要

私は2つのソリューションを開発しましたが、マルチスレッドクライアントに対してテストしたときに正しく動作することを確認しました。 2つの重要なトリックがあります。

まず、WebSocketと同じパスを持つフィルターを使用します。これにより、HttpServletRequestおよびHttpSessionにアクセスできます。また、セッションがまだ存在しない場合は、セッションを作成する機会も与えられます(ただし、その場合、HTTPセッションの使用はまったく疑わしいようです)。

次に、WebSocket SessionHttpServletRequestまたはHttpSessionの両方に存在するプロパティを見つけます。 getUserPrincipal()getRequestParameterMap()の2つの候補があることがわかりました。両方を悪用する方法を紹介します:)

ユーザープリンシパルを使用したソリューション

最も簡単な方法は、Session.getUserPrincipal()HttpServletRequest.getUserPrincipal()を利用することです。欠点は、このプロパティの他の正当な使用を妨げる可能性があるため、これらの影響に備えている場合にのみ使用してください。

ユーザーIDなどの文字列を1つだけ保存する場合、これは実際にはあまり悪用されませんが、おそらく、後で説明するようにラッパーをオーバーライドするのではなく、コンテナー管理の方法で設定する必要があります。とにかく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();
    }
}
_

リクエストパラメータを使用したソリューション

2番目のオプションは、Session.getRequestParameterMap()およびHttpServletRequest.getParameterMap()を使用します。 ServerEndpointConfig.getUserProperties()を使用しますが、この場合は常に同じオブジェクトをマップに配置するので安全であり、共有されているかどうかは関係ありません。一意のセッション識別子は、ユーザーパラメータではなく、リクエストパラメータを介して渡されます。リクエストパラメータisリクエストごとに一意です。

このソリューションは、ユーザープリンシパルプロパティに干渉しないため、ややハッキングが少なくなります。挿入されたものに加えてactualリクエストパラメータを渡す必要がある場合は、簡単に行うことができます。ここに示すように、新しい空のリクエストマップではなく、既存のリクエストパラメータマップから開始するだけです。 。ただし、ユーザーが実際の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);
    }
}
_

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);
        }
    }
}
_

単一のプロパティのソリューション

ユーザーIDのように、HttpSession全体ではなくHttpSessionから特定のプロパティのみが必要な場合は、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 {
    }
}
_

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);
    }
}
_
17
Chad N B

出来ますか?

WebSocketのJava API仕様を確認して、HttpSessionオブジェクトを取得できるかどうかを確認しましょう。 specification は29ページに記載しています:

Webソケット接続はhttpリクエストで開始されるため、クライアントが動作しているHttpSessionと、そのHttpSession内で確立されたすべてのwebsocketの間には関連付けがあります。このAPIを使用すると、オープニングハンドシェイクで、同じクライアントに対応する一意のHttpSessionにアクセスできます。

はい、可能です。

ただし、HttpServletRequestオブジェクトへの参照を取得することはできないと思います。 allServletRequestListener を使用して新しいサーブレットリクエストをリッスンできますが、どのリクエストがどのサーバーエンドポイントに属しているかを把握する必要があります。解決策を見つけたら教えてください!

抽象ハウツー

How-toは、仕様の13ページと14ページに大まかに説明されており、次の見出しの下のコードで例示されています。

英語では、HttpSessionオブジェクトを取得するためにハンドシェイクプロセスをインターセプトする必要があります。その後、HttpSession参照をサーバーエンドポイントに転送するには、コンテナーがサーバーエンドポイントインスタンスを作成するときにインターセプトし、参照を手動で挿入する必要もあります。独自の_ServerEndpointConfig.Configurator_を提供し、メソッドmodifyHandshake()およびgetEndpointInstance()をオーバーライドすることにより、これらすべてを行います。

カスタムコンフィギュレーターは、論理ServerEndpointごとに1回インスタンス化されます( 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;
    }
}
_
7

上記のすべての回答は読む価値がありますが、OP(および私の)の問題を解決できるものはありません。

WSエンドポイントが開いているときにHttpSessionにアクセスして、新しく作成されたエンドポイントインスタンスに渡すことができますが、HttpSessionインスタンスが存在することを保証するものはありません!

したがって、このハッキングの前にステップ0が必要です(WebSocketのJSR 365実装が嫌いです)。 Websocket-httpSessionはnullを返します

4
9dan

すべてのアプリケーションサーバーで機能する唯一の方法は、ThreadLocalを使用することです。見る:

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

0
Ryan

可能なソリューションはすべて次のものに基づいています。

A.クライアントブラウザーの実装は、HTTPヘッダーとして渡されるCookie値を介してセッションIDを維持します。または(Cookieが無効な場合)生成されたURLのセッションID後置を生成するサーブレットコンテナーによって管理されます。

B. HTTPハンドシェイク中にのみHTTP要求ヘッダーにアクセスできます。その後、それはWebsocketプロトコルです

そのため...

解決策1:「ハンドシェイク」を使用してHTTPにアクセスする

解決策2:クライアント側のJavaScriptで、HTTPセッションIDパラメーターを動的に生成し、このセッションIDを含む最初のメッセージを(Websocket経由で)送信します。 「エンドポイント」をセッションIDを維持するキャッシュ/ユーティリティクラスに接続します->セッションマッピング。メモリリークを回避するには、インスタンスのセッションリスナーを使用して、キャッシュからセッションを削除します。

追伸Martin AnderssonとJoakim Erdfeltからの回答に感謝します。残念ながら、Martinのソリューションはスレッドセーフではありません...

0
Fuad Efendi