Reputation: 71
I'm trying to implement a websocket on using Spring Boot (1.5.13).
The messaging works fine but after about 30 minutes the connection is terminated by the server (Reason 1008 - "This connection was established under an authenticated HTTP session that has ended"). I've tried to set different timeouts but doesn't seem to have any effect.
@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
A TextMessage (WebSocket) is sent every 30 minutes to keep the connection alive.
I've seen this question about session timeouts but I can't see a solution there
Upvotes: 7
Views: 19299
Reputation: 11
From the Docs in 4.2.4. Server Configuration is the following, hope it can help you:
Each underlying WebSocket engine exposes configuration properties that control runtime characteristics, such as the size of message buffer sizes, idle timeout, and others.
For Tomcat, WildFly, and GlassFish, you can add a ServletServerContainerFactoryBean to your WebSocket Java config, as the following example shows:
@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {
@Bean
public ServletServerContainerFactoryBean createWebSocketContainer() {
ServletServerContainerFactoryBean container = new ServletServerContainerFactoryBean();
container.setMaxSessionIdleTimeout(120000L); // set the timeout to 2min
// ...
return container;
}
}
For Jetty, you need to supply a pre-configured Jetty WebSocketServerFactory and plug that into Spring’s DefaultHandshakeHandler through your WebSocket Java config. The following example shows how to do so:
@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {
@Override
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
registry.addHandler(echoWebSocketHandler(),
"/echo").setHandshakeHandler(handshakeHandler());
}
@Bean
public DefaultHandshakeHandler handshakeHandler() {
WebSocketPolicy policy = new WebSocketPolicy(WebSocketBehavior.SERVER);
policy.setInputBufferSize(8192);
policy.setIdleTimeout(600000); // set the timeout to 10min
return new DefaultHandshakeHandler(
new JettyRequestUpgradeStrategy(new WebSocketServerFactory(policy)));
}
}
Upvotes: 1
Reputation: 1
In my case, timeout of HttpSession in SpringBoot is set to
server.servlet.session.timeout: 1
(1 minute)
Set websocket endpoint in Spring Security config to httpBasic
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable();
http.antMatcher("/ocpp/**")
.authorizeRequests()
.anyRequest().permitAll()//.hasAnyRole("CS","SYSTEM_ADMIN")
.and().httpBasic();
}
use basic authentication in websocket client
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
) {
...
In this condition, if the endpoint is a protected resource, websocket close 1008 occurs when the HttpSession is terminated.
Reference: Read Paragraph 7.2
**The solution is to directly implement http basic processing in websocket addInterceptor. (Spring Security httpBasic not use) **
add addInterceptor
in WebSocketConfig.java
:
.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;
}
Upvotes: 0
Reputation: 61
I found my WebSocket closed after 30 minutes too.
The root reason is the http session will close after 30 minutes by default in SpringBoot.
In SpringBoot config property server.servlet.session.timeout
will change the default behavior, but there might have some limit.
Besides, WebSocket connections have pingpong messages to keep alive, so the connection should never be closed, until the pingpong stops.
After some tracing, I found a way to solve this problem:
io.undertow.server.session.InMemorySessionManager.SessionImpl
in my case.io.undertow.server.session.InMemorySessionManager.SessionImpl#setMaxInactiveInterval
will reset the timer.javax.servlet.http.HttpSession#setMaxInactiveInterval
.setMaxInactiveInterval
is called before each timeout, the connection will never be closed.Here is my implementation:
HandshakeRequest
into the javax.websocket.EndpointConfig#getUserProperties
in the Configurator javax.websocket.server.ServerEndpointConfig.Configurator#modifyHandshake
onMessage
method, fetch the HandshakeRequest
from javax.websocket.Session#getUserProperties
, thenHttpSession httpSession = (HttpSession) handshakeRequest.getHttpSession();
httpSession.setMaxInactiveInterval((int) (session.getMaxIdleTimeout() / 1000));
That's all, hope it helps.
Upvotes: 4
Reputation: 26926
The connection can be closed if any between server or client closes it, and also by a firewall if it notes that there is no activity on the established connection.
So you need to check timeout also on the client side. It is reasanable that 30 minutes is the default timeout for this kind of connection so it seams that on the client side it uses default values.
Additionally it is a good design to check periodically the status of the connection, for example sending a sort of ping (message from the client) /pong (response from the server) message. If you do it every minute for example you have an idea of the connection status and the connection will never be closed for inactivity.
Upvotes: 0