diff --git a/pom.xml b/pom.xml
index ea3286128d7b70a54e84bb1b45e0313e3a87d8a7..8c4f245d33a22d2c3ed89a45768a0cca8f79b6c5 100644
--- a/pom.xml
+++ b/pom.xml
@@ -95,10 +95,6 @@
snakeyaml
org.yaml
-
- tomcat-embed-websocket
- org.apache.tomcat.embed
-
tomcat-embed-el
org.apache.tomcat.embed
@@ -173,6 +169,11 @@
tomcat-annotations-api
${tomcat.embed.version}
+
+ org.springframework.boot
+ spring-boot-starter-websocket
+ 2.1.18.RELEASE
+
org.apache.tomcat.embed
diff --git a/src/main/java/org/edgegallery/website/GatewayApplication.java b/src/main/java/org/edgegallery/website/GatewayApplication.java
index 442500bc7d8e9e067cc2138579a85063252f92a2..bc1de71b404c181409b6db2d7d6eca1f43886adf 100644
--- a/src/main/java/org/edgegallery/website/GatewayApplication.java
+++ b/src/main/java/org/edgegallery/website/GatewayApplication.java
@@ -16,8 +16,6 @@
package org.edgegallery.website;
-import com.marcosbarbero.cloud.autoconfigure.zuul.ratelimit.config.repository.DefaultRateLimiterErrorHandler;
-import com.marcosbarbero.cloud.autoconfigure.zuul.ratelimit.config.repository.RateLimiterErrorHandler;
import java.security.KeyManagementException;
import java.security.NoSuchAlgorithmException;
import java.security.cert.X509Certificate;
@@ -34,12 +32,14 @@ import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration;
import org.springframework.boot.autoconfigure.security.servlet.UserDetailsServiceAutoConfiguration;
+import org.springframework.boot.web.servlet.ServletComponentScan;
import org.springframework.cloud.netflix.zuul.EnableZuulProxy;
import org.springframework.context.annotation.Bean;
@SpringBootApplication(exclude = {SecurityAutoConfiguration.class, UserDetailsServiceAutoConfiguration.class})
@EnableZuulProxy
@EnableServiceComb
+@ServletComponentScan
public class GatewayApplication {
/**
* main.
diff --git a/src/main/java/org/edgegallery/website/common/Consts.java b/src/main/java/org/edgegallery/website/common/Consts.java
new file mode 100644
index 0000000000000000000000000000000000000000..918a87898ef3c0d83eb71043b1a342bd0b976931
--- /dev/null
+++ b/src/main/java/org/edgegallery/website/common/Consts.java
@@ -0,0 +1,51 @@
+/*
+ * Copyright 2021 Huawei Technologies Co., Ltd.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.edgegallery.website.common;
+
+/**
+ * constant define.
+ */
+public class Consts {
+ /**
+ * http session timeout.
+ */
+ public static final long HTTP_SESSION_TIMEOUT = 3600;
+ /**
+ * advance notify time for http session timeout.
+ */
+ public static final long ADV_NOTIFY_TIME_FOR_HTTP_SESSION_TIMEOUT = 60;
+ /**
+ * http session invalid scene.
+ */
+ public static final class HttpSessionInvalidScene {
+ private HttpSessionInvalidScene() {
+ }
+
+ /**
+ * timeout.
+ */
+ public static final int TIMEOUT = 1;
+ /**
+ * logout.
+ */
+ public static final int LOGOUT = 2;
+ /**
+ * server stopped.
+ */
+ public static final int SERVER_STOP = 3;
+ }
+}
diff --git a/src/main/java/org/edgegallery/website/config/ClientWebSecurityConfigurer.java b/src/main/java/org/edgegallery/website/config/ClientWebSecurityConfigurer.java
index ab2fb6cf4e36300ce75c62237d9b9df19964ef86..9ca8866e82b5ee5587dc2fdf0e3057dc266760c8 100644
--- a/src/main/java/org/edgegallery/website/config/ClientWebSecurityConfigurer.java
+++ b/src/main/java/org/edgegallery/website/config/ClientWebSecurityConfigurer.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2020 Huawei Technologies Co., Ltd.
+ * Copyright 2020-2021 Huawei Technologies Co., Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -16,6 +16,8 @@
package org.edgegallery.website.config;
+import com.netflix.zuul.ZuulFilter;
+import com.netflix.zuul.context.RequestContext;
import java.io.IOException;
import java.util.Map;
import javax.servlet.ServletContext;
@@ -23,6 +25,8 @@ import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
+import org.edgegallery.website.common.Consts;
+import org.edgegallery.website.sessionmgr.WebSocketSessionServer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
@@ -45,10 +49,10 @@ import org.springframework.security.oauth2.provider.authentication.OAuth2Authent
import org.springframework.security.oauth2.provider.token.DefaultTokenServices;
import org.springframework.security.oauth2.provider.token.TokenStore;
import org.springframework.security.web.authentication.SimpleUrlAuthenticationSuccessHandler;
+import org.springframework.security.web.authentication.logout.LogoutHandler;
import org.springframework.security.web.authentication.www.BasicAuthenticationFilter;
import org.springframework.security.web.csrf.CookieCsrfTokenRepository;
-import com.netflix.zuul.ZuulFilter;
-import com.netflix.zuul.context.RequestContext;
+import org.springframework.web.socket.server.standard.ServerEndpointExporter;
@Configuration
@EnableWebSecurity
@@ -85,9 +89,20 @@ public class ClientWebSecurityConfigurer extends WebSecurityConfigurerAdapter {
.antMatchers(HttpMethod.GET, "/mecm-inventory/inventory/v1/mechosts").permitAll()
.antMatchers(HttpMethod.GET, "/health")
.permitAll().antMatchers("/webssh").permitAll()
+ .antMatchers("/wsserver/**").permitAll()
.anyRequest()
.authenticated().and()
.addFilterBefore(oauth2ClientAuthenticationProcessingFilter(), BasicAuthenticationFilter.class).logout()
+ .addLogoutHandler(new LogoutHandler() {
+ @Override
+ public void logout(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse,
+ Authentication authentication) {
+ HttpSession httpSession = httpServletRequest.getSession();
+ if (httpSession != null) {
+ WebSocketSessionServer.notifyHttpSessionInvalid(httpSession.getId(), Consts.HttpSessionInvalidScene.LOGOUT);
+ }
+ }
+ })
.logoutUrl("/logout").logoutSuccessUrl(authServerAddress + "/auth/logout")
.and().csrf()
.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse());
@@ -152,4 +167,9 @@ public class ClientWebSecurityConfigurer extends WebSecurityConfigurerAdapter {
}
};
}
+
+ @Bean
+ public ServerEndpointExporter serverEndpointExporter() {
+ return new ServerEndpointExporter();
+ }
}
diff --git a/src/main/java/org/edgegallery/website/controller/OAuthClientController.java b/src/main/java/org/edgegallery/website/controller/OAuthClientController.java
index 84d042049473f5306be58f2b67e7197f7dbea06b..cf2d2113afd4052c01bccbe2ca8cd92fb419a9fe 100644
--- a/src/main/java/org/edgegallery/website/controller/OAuthClientController.java
+++ b/src/main/java/org/edgegallery/website/controller/OAuthClientController.java
@@ -55,7 +55,7 @@ public class OAuthClientController {
@RequestMapping(value = "/login-info", method = RequestMethod.GET, produces = "application/json")
@ApiOperation(value = "get user information", response = LoginInfoRespDto.class,
notes = "The API can " + "receive the get user information request")
- public ResponseEntity getLoginInfo() {
+ public ResponseEntity getLoginInfo(HttpServletRequest request) {
OAuth2AuthenticationDetails details = jwtServer.getAuthDetails();
Map additionalInformation = jwtServer.getToken(details.getTokenValue())
.getAdditionalInformation();
@@ -71,6 +71,7 @@ public class OAuthClientController {
loginInfoRespDto.setForceModifyPwPage(authServerAddress + "/index.html#/usermgmt/forcemodifypwd");
}
loginInfoRespDto.setAuthorities(additionalInformation.get("authorities"));
+ loginInfoRespDto.setSessId(request.getSession().getId());
return new ResponseEntity<>(loginInfoRespDto, HttpStatus.OK);
}
@@ -86,7 +87,7 @@ public class OAuthClientController {
try {
session.invalidate();
} catch (IllegalStateException e) {
- log.info("The session {} already invalid.", session.getId());
+ log.info("The session already invalid.");
}
servletContext.removeAttribute(ssoSessionId);
}
diff --git a/src/main/java/org/edgegallery/website/model/LoginInfoRespDto.java b/src/main/java/org/edgegallery/website/model/LoginInfoRespDto.java
index db2d4952f381453a6adc62a16cf2d91def95a067..b55d5d893e1d0c8b3746aea39f2e3c49993acf5b 100644
--- a/src/main/java/org/edgegallery/website/model/LoginInfoRespDto.java
+++ b/src/main/java/org/edgegallery/website/model/LoginInfoRespDto.java
@@ -37,4 +37,6 @@ public class LoginInfoRespDto {
private Object accessToken;
private Object authorities;
+
+ private Object sessId;
}
diff --git a/src/main/java/org/edgegallery/website/sessionmgr/CustomHttpSessionListener.java b/src/main/java/org/edgegallery/website/sessionmgr/CustomHttpSessionListener.java
new file mode 100644
index 0000000000000000000000000000000000000000..bfd4129199ff5a8008c00fc998461a37eda8e8bc
--- /dev/null
+++ b/src/main/java/org/edgegallery/website/sessionmgr/CustomHttpSessionListener.java
@@ -0,0 +1,47 @@
+/*
+ * Copyright 2021 Huawei Technologies Co., Ltd.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.edgegallery.website.sessionmgr;
+
+import javax.servlet.annotation.WebListener;
+import javax.servlet.http.HttpSessionEvent;
+import javax.servlet.http.HttpSessionListener;
+
+/**
+ * Http Session Listener.
+ */
+@WebListener
+public class CustomHttpSessionListener implements HttpSessionListener {
+ /**
+ * listen session created.
+ *
+ * @param se session created event
+ */
+ @Override
+ public void sessionCreated(HttpSessionEvent se) {
+ CustomHttpSessionManager.getInstance().addSession(se.getSession());
+ }
+
+ /**
+ * listen session destroyed.
+ *
+ * @param se session destroyed event
+ */
+ @Override
+ public void sessionDestroyed(HttpSessionEvent se) {
+ CustomHttpSessionManager.getInstance().removeSession(se.getSession());
+ }
+}
diff --git a/src/main/java/org/edgegallery/website/sessionmgr/CustomHttpSessionManager.java b/src/main/java/org/edgegallery/website/sessionmgr/CustomHttpSessionManager.java
new file mode 100644
index 0000000000000000000000000000000000000000..4cef3e563356935f933ba3667318ceb8c8779461
--- /dev/null
+++ b/src/main/java/org/edgegallery/website/sessionmgr/CustomHttpSessionManager.java
@@ -0,0 +1,77 @@
+/*
+ * Copyright 2021 Huawei Technologies Co., Ltd.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.edgegallery.website.sessionmgr;
+
+import java.util.HashMap;
+import java.util.Map;
+import javax.servlet.http.HttpSession;
+
+/**
+ * Http Session Manager.
+ */
+public final class CustomHttpSessionManager {
+ private static final CustomHttpSessionManager INSTANCE = new CustomHttpSessionManager();
+
+ private static final Map SESSION_MAP = new HashMap<>();
+
+ private static final Object LOCK_OBJ = new Object();
+
+ private CustomHttpSessionManager() {}
+
+ /**
+ * get single instance.
+ *
+ * @return HttpSessionManager Instance
+ */
+ public static CustomHttpSessionManager getInstance() {
+ return INSTANCE;
+ }
+
+ /**
+ * add session.
+ *
+ * @param httpSession Http Session
+ */
+ public static void addSession(HttpSession httpSession) {
+ synchronized (LOCK_OBJ) {
+ SESSION_MAP.put(httpSession.getId(), httpSession);
+ }
+ }
+
+ /**
+ * remove session.
+ *
+ * @param httpSession Http Session
+ */
+ public void removeSession(HttpSession httpSession) {
+ synchronized (LOCK_OBJ) {
+ SESSION_MAP.remove(httpSession.getId());
+ }
+ }
+
+ /**
+ * get session by id.
+ *
+ * @param httpSessionId Http Session Id
+ * @return Http Session
+ */
+ public HttpSession getSession(String httpSessionId) {
+ synchronized (LOCK_OBJ) {
+ return SESSION_MAP.get(httpSessionId);
+ }
+ }
+}
diff --git a/src/main/java/org/edgegallery/website/sessionmgr/WebSocketSessionServer.java b/src/main/java/org/edgegallery/website/sessionmgr/WebSocketSessionServer.java
new file mode 100644
index 0000000000000000000000000000000000000000..33ba08943f535853d95f8aa69da233540a58c6d1
--- /dev/null
+++ b/src/main/java/org/edgegallery/website/sessionmgr/WebSocketSessionServer.java
@@ -0,0 +1,120 @@
+/*
+ * Copyright 2021 Huawei Technologies Co., Ltd.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.edgegallery.website.sessionmgr;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import javax.servlet.http.HttpSession;
+import javax.websocket.OnMessage;
+import javax.websocket.OnOpen;
+import javax.websocket.Session;
+import javax.websocket.server.PathParam;
+import javax.websocket.server.ServerEndpoint;
+import org.edgegallery.website.common.Consts;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.stereotype.Component;
+import org.springframework.util.StringUtils;
+
+/**
+ * Websocket Session Server.
+ */
+@ServerEndpoint("/wsserver/{httpSessionId}")
+@Component
+public class WebSocketSessionServer {
+ private static final Logger LOGGER = LoggerFactory.getLogger(WebSocketSessionServer.class);
+
+ private static final Map> WS_SESSION_FINDER = new HashMap<>();
+
+ private static final Map HTTP_SESSIONID_FINDER = new HashMap<>();
+
+ private static final Object LOCK_OBJ = new Object();
+
+ /**'
+ * open websocket session.
+ *
+ * @param wsSession websocket session
+ * @param httpSessionId http session id
+ */
+ @OnOpen
+ public void onOpen(Session wsSession, @PathParam("httpSessionId") String httpSessionId) {
+ if (StringUtils.isEmpty(httpSessionId)) {
+ LOGGER.warn("invalid http session id.");
+ return;
+ }
+
+ LOGGER.debug("ws client opened.");
+ synchronized (LOCK_OBJ) {
+ List wsSessList = WS_SESSION_FINDER.get(httpSessionId);
+ if (wsSessList == null) {
+ wsSessList = new ArrayList<>();
+ WS_SESSION_FINDER.put(httpSessionId, wsSessList);
+ }
+
+ wsSessList.add(wsSession);
+ HTTP_SESSIONID_FINDER.put(wsSession.getId(), httpSessionId);
+ }
+ }
+
+ /**
+ * receive message from ws client.
+ *
+ * @param message message
+ * @param wsSession websocket session
+ */
+ @OnMessage
+ public void onMessage(String message, Session wsSession) {
+ LOGGER.debug("receive message from ws client");
+ synchronized (LOCK_OBJ) {
+ String httpSessId = HTTP_SESSIONID_FINDER.get(wsSession.getId());
+ HttpSession httpSession = CustomHttpSessionManager.getInstance().getSession(httpSessId);
+ if (httpSession != null) {
+ if ((System.currentTimeMillis() - httpSession.getLastAccessedTime()) / 1000
+ > Consts.HTTP_SESSION_TIMEOUT - Consts.ADV_NOTIFY_TIME_FOR_HTTP_SESSION_TIMEOUT) {
+ notifyHttpSessionInvalid(httpSessId, Consts.HttpSessionInvalidScene.TIMEOUT);
+ }
+ }
+ }
+ }
+
+ /**
+ * notify http session invalid.
+ *
+ * @param httpSessionId Http Session Id
+ * @param invalidScene Session invalidation scene
+ */
+ public static void notifyHttpSessionInvalid(String httpSessionId, int invalidScene) {
+ synchronized (LOCK_OBJ) {
+ List wsSessList = WS_SESSION_FINDER.get(httpSessionId);
+ if (wsSessList == null) {
+ return;
+ }
+ wsSessList.forEach(wsSession -> {
+ LOGGER.info("notify http session timeout. wsId = {}", wsSession.getId());
+ try {
+ wsSession.getBasicRemote().sendText(String.valueOf(invalidScene));
+ } catch (Exception e) {
+ LOGGER.error("notify failed: {}", e.getMessage());
+ }
+ HTTP_SESSIONID_FINDER.remove(wsSession.getId());
+ });
+ WS_SESSION_FINDER.remove(httpSessionId);
+ }
+ }
+}