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); + } + } +}