【问题标题】:Spring websocket is automatically closed after 30 minutes (timeout)Spring websocket 30分钟后自动关闭(超时)
【发布时间】:2018-11-08 07:10:30
【问题描述】:

我正在尝试使用 Spring Boot (1.5.13) 实现 websocket。

消息传递工作正常,但大约 30 分钟后连接被服务器终止(原因 1008 - “此连接是在已结束的经过身份验证的 HTTP 会话下建立的”)。我尝试设置不同的超时时间,但似乎没有任何效果。

@Service
@RequiredArgsConstructor
@Slf4j
public class OCPPSocketHandler extends TextWebSocketHandler {
    @Override
    public void handleTextMessage(WebSocketSession webSocketSession, TextMessage textMessage)
        throws IOException {
      ...
    }
}

@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {

    public static final String ENDPOINT = "/pp/v2.0";

    @Autowired
    private CustomSocketHandler socketHandler;

    public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
        registry.addHandler(
            new CustomExceptionWebSocketHandlerDecorator(socketHandler), ENDPOINT
        )
        .setAllowedOrigins("*");
    }
}

application.properties:

#6h as milliseconds
server.connection-timeout=3600000 
server.servlet.session.timeout=6h

每 30 分钟发送一次 TextMessage (WebSocket) 以保持连接有效。

我见过this question about session timeouts,但在那里我看不到解决方案

【问题讨论】:

  • 原因 1008 是“POLICY_VIOLATION”1008 表示端点正在终止连接,因为它收到了违反其策略的消息”?您是否检查了日志以了解是否有违反策略的消息正在发送?

标签: java spring-boot websocket


【解决方案1】:

如果服务器或客户端之间的任何连接关闭它,则可以关闭连接,如果防火墙注意到已建立的连接上没有活动,也可以关闭它。

因此,您还需要在客户端检查超时。 30 分钟是这种连接的默认超时是合理的,因此它会在客户端使用默认值。

此外,定期检查连接状态是一个很好的设计,例如发送一种 ping(来自客户端的消息)/pong(来自服务器的响应)消息。例如,如果您每分钟都这样做,您就会了解连接状态,并且连接永远不会因不活动而关闭。

【讨论】:

  • 我今天每 2 分钟发送一条短信进行测试。结果保持不变。大约 30 分钟后连接关闭。
  • 很奇怪...它关闭了与什么消息的连接?
  • 连接已关闭:/127.0.0.1:58859 关闭状态:1008 原因:此连接是在已结束的经过身份验证的 HTTP 会话下建立的。
【解决方案2】:

我发现我的 WebSocket 在 30 分钟后也关闭了。

根本原因是SpringBoot默认30分钟后http会话会关闭。

在 SpringBoot 配置属性server.servlet.session.timeout 会改变默认行为,但可能有some limit。

此外,WebSocket 连接有 pingpong 消息要保持活动状态,因此在 pingpong 停止之前永远不要关闭连接。

经过一番追踪,我找到了解决这个问题的方法:

  1. 在我的例子中,关闭连接的计时器在这里是io.undertow.server.session.InMemorySessionManager.SessionImpl。
  2. 我们可以看到io.undertow.server.session.InMemorySessionManager.SessionImpl#setMaxInactiveInterval 将重置计时器。
  3. 这个方法会被javax.servlet.http.HttpSession#setMaxInactiveInterval调用。
  4. 因此,一旦在每次超时之前调用setMaxInactiveInterval,连接将永远不会关闭。

这是我的实现:

  1. 将HandshakeRequest 存储到配置器javax.websocket.EndpointConfig#getUserProperties 中的javax.websocket.server.ServerEndpointConfig.Configurator#modifyHandshake
  2. 我们的客户会发送 String 消息来保持活动状态,所以在 onMessage 方法中,从 javax.websocket.Session#getUserProperties 获取 HandshakeRequest,然后
HttpSession httpSession = (HttpSession) handshakeRequest.getHttpSession();
httpSession.setMaxInactiveInterval((int) (session.getMaxIdleTimeout() / 1000));

就是这样,希望对你有帮助。

【讨论】:

  • setMaxInactiveInterval 将重置计时器 对我不起作用,但只需切换到 3 小时会话即可,因为我的客户端会在服务器关闭会话时自动连接跨度>
【解决方案3】:
  1. 在我的例子中,SpringBoot 中 HttpSession 的超时设置为 server.servlet.session.timeout: 1(1 分钟)

  2. 将 Spring Security 配置中的 websocket 端点设置为 httpBasic

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.csrf().disable();
        http.antMatcher("/ocpp/**")
        .authorizeRequests()                         
        .anyRequest().permitAll()//.hasAnyRole("CS","SYSTEM_ADMIN")
        .and().httpBasic();
    }
    
  3. 在 websocket 客户端中使用基本身份验证

        String id = "testuser";
        String pw = "1234";
        String idpw = new String(Base64.getEncoder().encode((id+":"+pw).getBytes()));
    
        System.out.println("id["+id+"]pw["+pw+"]idpw["+idpw+"]");
        ;
        Map<String, String> httpHeaders = new HashMap<String, String>();
        httpHeaders.put("authorization", "Basic "+idpw);
        WebSocketClient webSocketClient = new WebSocketClient(new URI("ws://localhost:9112/ocpp/testuser")
                , new Draft_6455(Collections.emptyList(), Collections.singletonList(new Protocol("ocpp2.0.1")))
                , httpHeaders
        ) {
        ...
    

在这种情况下,如果端点是受保护的资源,则在终止 HttpSession 时会发生 websocket close 1008。

Reference:阅读第 7.2 段

**解决方法是直接在websocket addInterceptor中实现http基础处理。 (Spring Security httpBasic 不使用) **

在WebSocketConfig.java中添加addInterceptor:

.addInterceptors(new HandshakeInterceptor() {
    @Override
    public boolean beforeHandshake(ServerHttpRequest serverHttpRequest, ServerHttpResponse serverHttpResponse, WebSocketHandler wsHandler, Map<String, Object> attributes) throws Exception {
        log.debug("===============================================================");
        log.debug("beforeHandshake[]"+attributes);
        ServletServerHttpRequest ssreq = (ServletServerHttpRequest)serverHttpRequest;
        ServletServerHttpResponse ssres = (ServletServerHttpResponse)serverHttpResponse;
        HttpServletRequest req = ssreq.getServletRequest();
        HttpServletResponse res = ssres.getServletResponse();
        HttpSession session = req.getSession();
        log.debug("session["+session.getId());
        log.debug("session["+session.getMaxInactiveInterval());

        //authentication
        try {
            String header = req.getHeader("Authorization");
            log.debug("header[" + header + "]");
            if (header == null) {
                log.debug("The Authorization header is empty");
//                                    throw new BadCredentialsException("The Authorization header is empty");
            } else {
                header = header.trim();
                if (!StringUtils.startsWithIgnoreCase(header, "Basic")) {
                    log.debug("The Authorization header does not start with Basic.");
                } else if (header.equalsIgnoreCase("Basic")) {
                    throw new BadCredentialsException("Empty basic authentication token");
                } else {
                    byte[] base64Token = header.substring(6).getBytes(StandardCharsets.UTF_8);
                    byte[] decoded;
                    try {
                        decoded = Base64.getDecoder().decode(base64Token);
                    } catch (IllegalArgumentException var8) {
                        throw new BadCredentialsException("Failed to decode basic authentication token");
                    }
                    String token = new String(decoded, "UTF-8");
                    int delim = token.indexOf(":");
                    if (delim == -1) {
                        throw new BadCredentialsException("Invalid basic authentication token");
                    } else {
                        log.info("TOKEN [" +token+"]");
                        String principal = token.substring(0, delim);
                        String credencial = token.substring(delim + 1);
                        //your 
                        
                        if(principal.equals("testuser") && credencial.equals("1234")){
                            log.debug("login OK");
                        }else{
                            throw new BadCredentialsException("Invalid basic authentication token");
                        }
                    }
                }
            }
        }catch(Exception e){
            log.error("Basic Authentication error", e);

            res.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
            res.addHeader("WWW-Authenticate", "Basic realm=" + "Realm" + "");
            PrintWriter pw = res.getWriter();
            pw.println("Invalid status code received: 401 Status line: HTTP/1.1 401");
            return false;
        }

        return true;
    }

【讨论】:

    猜你喜欢
    • 2022-10-20
    • 1970-01-01
    • 2023-03-12
    • 1970-01-01
    • 2018-07-26
    • 2021-10-25
    • 2020-09-11
    • 1970-01-01
    • 2011-10-18
    相关资源
    最近更新 更多