我做了一个JSR-356 @ServerEndpoint
,我想限制来自单个IP地址的活动连接,以防止简单的DDOS攻击。
请注意,我正在寻找Java解决方案(JSR-356,Tomcat或Servlet 3.0规范)。
我已经尝试了自定义端点配置器,但即使在HandshakeRequest
对象中我也无法访问 IP 地址。
如何在没有iptables等外部软件的情况下限制单个IP地址的JSR-356连接数?
根据Tomcat开发人员的说法,@mark-thomas客户端IP不是通过JSR-356公开的,因此不可能用纯JSR-356 API-s实现这样的功能。
您必须使用一个相当丑陋的黑客来解决标准的局限性。
需要做的归结为:
- 在
- 初始请求时(在 websocket 握手之前)为每个用户生成一个包含其 IP 的令牌
- 将令牌沿链向下传递,直到它到达端点实现
至少有两个黑客选项可以实现这一目标。
使用 HttpSession
- 使用
ServletRequestListener
侦听传入的 HTTP 请求 - 对传入请求调用
request.getSession()
,以确保它具有会话并将客户端 IP 存储为会话属性。 - 创建一个
ServerEndpointConfig.Configurator
,该从HandshakeRequest#getHttpSession
中提升客户端 IP,并使用modifyHandshake
方法将其作为用户属性附加到EndpointConfig
。 - 从
EndpointConfig
用户属性中获取客户端 IP,将其存储在映射或其他任何内容中,并在每个 IP 的会话数超过阈值时触发清理逻辑。
您也可以使用@WebFilter
而不是ServletRequestListener
请注意,此选项可能会消耗大量资源,除非您的应用程序已经使用会话(例如用于身份验证目的)。
在 URL 中将 IP 作为加密令牌传递
- 创建一个附加到非 websocket 入口点的 servlet 或过滤器,例如
/mychat
- 获取客户端 IP,使用随机盐和密钥对其进行加密以生成令牌。
- 使用
ServletRequest#getRequestDispatcher
将请求转发到/mychat/TOKEN
- 配置端点以使用路径参数,例如
@ServerEndpoint("/mychat/{token}")
- 从
@PathParam
中获取令牌并解密以获取客户端 IP。将其存储在映射或其他任何内容中,并在每个 IP 的会话数超过阈值时触发清理逻辑。
为了便于安装,您可能希望在应用程序启动时生成加密密钥。
请注意,即使您正在执行客户端不可见的内部调度,也需要加密 IP。没有什么可以阻止攻击者直接连接到/mychat/2.3.4.5
从而欺骗客户端 IP(如果未加密)。
另请参阅:
- Apache Tomcat 8 Websocket 源和客户端地址
- 查找从给定客户端 IP 创建的活动会话数
- 从 Web 套接字@ServerEndpoint中的 HttpServletRequest 访问 HttpSession
- https://tyrus.java.net/documentation/1.4/index/websocket-api.html
- http://docs.oracle.com/javaee/7/tutorial/doc/websocket010.htm#BABJAIGH
套接字对象隐藏在 WsSession 中,因此您可以使用反射来获取 IP 地址。 此方法的执行时间约为 1ms。 此解决方案并不完美,但很有用。
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>
如果您使用的是符合 JSR-356 的 Tyrus,然后,您可以从会话实例获取 IP 地址,但这是一种非标准方法。
看这里。
Undertow Websocket 引擎的 Springboot,请尝试以下方式获取 IP。
@OnOpen
public void onOpen(Session session) {
UndertowSession us = (UndertowSession) session;
String ip = us.getWebSocketChannel().getSourceAddress().getHostString();
如果使用:实现 '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"); }