JSR-356 WebSockets with Tomcat - 如何限制单个IP地址内的连接数?

7
我制作了一个JSR-356的@ServerEndpoint,我希望限制单个IP地址的连接数量,以防止简单的DDOS攻击。
请注意,我正在寻找Java解决方案(JSR-356,Tomcat或Servlet 3.0规范)。
我已经尝试过自定义端点配置程序,但是即使在HandshakeRequest对象中也无法访问IP地址。
如何在不使用iptables等外部软件的情况下限制JSR-356连接计数单个IP地址呢?

2
不要为此编写代码。使用防火墙。当你进入Java代码时,已经太晚了。 - user207421
@EJP 正如问题所述 - 需要 Java 解决方案。我不是在寻求最佳解决方案。假设我们希望在发送到许多环境的产品中实现最大的简单性和可移植性。从单个主机建立 40,000 个连接非常容易,但自动断开这些连接将使这个过程变得更加困难。Tomcat 在 WebSockets 上卡住了最大连接数,因此,如果它被配置为处理 ~200k 连接,我只想防止通过仅保持连接来轻松阻塞服务,而不需要像僵尸网络那样消耗大量资源。 - Piotr Müller
你假设有一个Java解决方案,但没有证据。如果你正在发布产品或有其他限制,你应该在问题中说明它们。大多数人并没有这样做。事实上,从单个主机建立40,000个连接并不容易。许多环境无法超过几千个连接。 - user207421
5个回答

13
根据Tomcat开发者@mark-thomas的说法,JSR-356无法公开客户端IP地址,因此使用纯JSR-356 API实现此功能是不可能的。您需要使用相当丑陋的技巧来解决标准的限制。需要做的事情归结为:
  1. 为每个用户生成一个令牌,在初始请求(websocket握手之前)中包含其IP地址
  2. 将令牌传递到终端实现之前的链路上
至少有两种hacky选项可以实现这一点。

使用HttpSession

  1. 使用 ServletRequestListener 监听传入的 HTTP 请求。
  2. 在传入请求上调用 request.getSession() 确保它有一个会话,并将客户端 IP 存储为会话属性。
  3. 创建一个 ServerEndpointConfig.Configurator,从 HandshakeRequest#getHttpSession 中提取客户端 IP,并使用 modifyHandshake 方法将其作为用户属性附加到 EndpointConfig 上。
  4. EndpointConfig 用户属性中获取客户端 IP,将其存储在映射表或其他位置,并在每个 IP 的会话数超过阈值时触发清理逻辑。

您也可以使用 @WebFilter 替代 ServletRequestListener

请注意,除非您的应用程序已经使用会话(例如用于身份验证),否则此选项可能会消耗大量资源。

在 URL 中以加密令牌的形式传递 IP

  1. 创建一个servlet或过滤器,附加到非websocket入口点,例如/mychat
  2. 获取客户端IP,使用随机salt和秘密密钥进行加密以生成令牌。
  3. 使用ServletRequest#getRequestDispatcher将请求转发到/mychat/TOKEN
  4. 配置你的端点使用路径参数,例如@ServerEndpoint("/mychat/{token}")
  5. @PathParam中提取令牌并解密以获取客户端IP。将其存储在地图或其他位置,并触发清理逻辑,如果每个IP的会话数超过阈值。

为了方便安装,您可能希望在应用程序启动时生成加密密钥。

请注意,即使您正在执行对客户端不可见的内部调度,也需要加密IP。如果未加密,则没有任何防止攻击者直接连接到/mychat/2.3.4.5,从而伪造客户端IP的措施。

另请参见:


你确定你的第二个建议可行吗?也许你可以提供一下要点?当我尝试在Jetty 9.3.6.v20151106中转发升级请求时,至少我会收到500错误。直接连接WebSocket是没有问题的。 - Kalle
为了回答我自己的评论,第二个建议根本不起作用,而在第一个选项中无法使用过滤器,但使用监听器可以解决问题。请参见https://dev59.com/4IHba4cB1Zd3GeqPV8yX以获取更多信息。 - Kalle
更正我之前的评论 - 在Tomcat(7/8)中使用过滤器确实有效,但在Jetty中无效。有一个针对规范的错误报告,以澄清是否应该使用(servlet)过滤器,详情请参见http://stackoverflow.com/questions/26103939/how-tomcat-8-handles-websocket-upgrade-request - Kalle

4

Socket对象被隐藏在WsSession中,因此您可以使用反射来获取IP地址。该方法的执行时间约为1毫秒。这个解决方案并不完美,但是很有用。

public static InetSocketAddress getRemoteAddress(WsSession session) {
    if(session == null){
        return null;
    }

    Async async = session.getAsyncRemote();
    InetSocketAddress addr = (InetSocketAddress) getFieldInstance(async, 
            "base#sos#socketWrapper#socket#sc#remoteAddress");

    return addr;
}

private static Object getFieldInstance(Object obj, String fieldPath) {
    String fields[] = fieldPath.split("#");
    for(String field : fields) {
        obj = getField(obj, obj.getClass(), field);
        if(obj == null) {
            return null;
        }
    }

    return obj;
}

private static Object getField(Object obj, Class<?> clazz, String fieldName) {
    for(;clazz != Object.class; clazz = clazz.getSuperclass()) {
        try {
            Field field;
            field = clazz.getDeclaredField(fieldName);
            field.setAccessible(true);
            return field.get(obj);
        } catch (Exception e) {
        }            
    }

    return null;
}

并且 pom 配置是:

<dependency>
  <groupId>javax.websocket</groupId>
  <artifactId>javax.websocket-all</artifactId>
  <version>1.1</version>
  <type>pom</type>
  <scope>provided</scope>
</dependency>
<dependency>
  <groupId>org.apache.tomcat</groupId>
  <artifactId>tomcat-websocket</artifactId>
  <version>8.0.26</version>
  <scope>provided</scope>
</dependency>

感谢您发布这个解决方法。它适用于Tomcat 8.0.x版本。不幸的是,它不能在Tomcat 8.5.x上使用。您是否有更新的方法可以让它在Tomcat 8.5.x上运行? - FlashDictionary
Tomcat 8.5似乎需要“base#socketWrapper#socket#sc#remoteAddress”(注意缺少sos)。 - lapo
此外,如果套接字由APR(即https)管理,则此方法无法正常工作。 - lapo
已在Tomcat 9.0.10上测试,它与“base#socketWrapper#socket#sc#remoteAddress”一起工作。 - hijack

0

如果您正在使用符合JSR-356标准的Tyrus,则可以从Session实例中获取IP地址,但这是一种非标准方法。

请参见此处。


0
如果使用Springboot和Undertow Websocket引擎,请尝试以下方法获取IP地址。
 @OnOpen
    public void onOpen(Session session) {
        UndertowSession us = (UndertowSession) session;
        String ip = us.getWebSocketChannel().getSourceAddress().getHostString();


0

如果使用:implementation 'io.quarkus:quarkus-websockets'

@OnOpen
  public void onOpen(final Session session, final @PathParam("userId") String userId) {
    UndertowSession us = (UndertowSession) session;
    System.out.println("Remote Address: " + us.getChannel().remoteAddress());

    SESSIONS.put(userId, session);
    log.info("User " + userId + " joined");
  }

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接