diff --git a/backend/src/main/java/io/dataease/auth/api/AuthApi.java b/backend/src/main/java/io/dataease/auth/api/AuthApi.java index 2b15b62e51..9b7f232543 100644 --- a/backend/src/main/java/io/dataease/auth/api/AuthApi.java +++ b/backend/src/main/java/io/dataease/auth/api/AuthApi.java @@ -3,6 +3,7 @@ package io.dataease.auth.api; import com.github.xiaoymin.knife4j.annotations.ApiSupport; import io.dataease.auth.api.dto.CurrentUserDto; import io.dataease.auth.api.dto.LoginDto; +import io.dataease.auth.api.dto.SeizeLoginDto; import io.swagger.annotations.Api; import io.swagger.annotations.ApiOperation; import org.springframework.web.bind.annotation.GetMapping; @@ -21,6 +22,9 @@ public interface AuthApi { @PostMapping("/login") Object login(LoginDto loginDto) throws Exception; + @PostMapping("/seizeLogin") + Object seizeLogin(SeizeLoginDto loginDto) throws Exception; + @ApiOperation("获取用户信息") @PostMapping("/userInfo") CurrentUserDto userInfo(); diff --git a/backend/src/main/java/io/dataease/auth/api/dto/SeizeLoginDto.java b/backend/src/main/java/io/dataease/auth/api/dto/SeizeLoginDto.java new file mode 100644 index 0000000000..9e017e9fae --- /dev/null +++ b/backend/src/main/java/io/dataease/auth/api/dto/SeizeLoginDto.java @@ -0,0 +1,13 @@ +package io.dataease.auth.api.dto; + +import lombok.Data; + +import java.io.Serializable; + +@Data +public class SeizeLoginDto implements Serializable { + + private static final long serialVersionUID = -3318473577764636483L; + + private String token; +} diff --git a/backend/src/main/java/io/dataease/auth/server/AuthServer.java b/backend/src/main/java/io/dataease/auth/server/AuthServer.java index ed7cdb0328..69f05c3f7a 100644 --- a/backend/src/main/java/io/dataease/auth/server/AuthServer.java +++ b/backend/src/main/java/io/dataease/auth/server/AuthServer.java @@ -4,6 +4,7 @@ import io.dataease.auth.api.AuthApi; import io.dataease.auth.api.dto.CurrentRoleDto; import io.dataease.auth.api.dto.CurrentUserDto; import io.dataease.auth.api.dto.LoginDto; +import io.dataease.auth.api.dto.SeizeLoginDto; import io.dataease.auth.config.RsaProperties; import io.dataease.auth.entity.AccountLockStatus; import io.dataease.auth.entity.SysUserEntity; @@ -28,6 +29,8 @@ import io.dataease.plugins.xpack.oidc.service.OidcXpackService; import io.dataease.service.sys.SysUserService; import io.dataease.service.system.SystemParameterService; +import io.dataease.websocket.entity.WsMessage; +import io.dataease.websocket.service.WsService; import org.apache.commons.lang3.ObjectUtils; import org.apache.commons.lang3.StringUtils; import org.apache.shiro.SecurityUtils; @@ -61,6 +64,9 @@ public class AuthServer implements AuthApi { @Resource private SystemParameterService systemParameterService; + @Autowired + private WsService wsService; + @Override public Object login(@RequestBody LoginDto loginDto) throws Exception { Map result = new HashMap<>(); @@ -164,6 +170,23 @@ public class AuthServer implements AuthApi { return result; } + @Override + public Object seizeLogin(@RequestBody SeizeLoginDto loginDto) throws Exception { + String token = loginDto.getToken(); + Map result = new HashMap<>(); + result.put("token", token); + ServletUtils.setToken(token); + TokenInfo tokenInfo = JWTUtils.tokenInfoByToken(token); + Long userId = tokenInfo.getUserId(); + JWTUtils.seizeSign(userId, token); + DeLogUtils.save(SysLogConstants.OPERATE_TYPE.LOGIN, SysLogConstants.SOURCE_TYPE.USER, userId, null, null, null); + WsMessage message = new WsMessage(userId, "/web-seize-topic", IPUtils.get()); + wsService.releaseMessage(message); + authUserService.clearCache(userId); + Thread.sleep(3000L); + return result; + } + private String appendLoginErrorMsg(String msg, AccountLockStatus lockStatus) { if (ObjectUtils.isEmpty(lockStatus)) return msg; if (ObjectUtils.isNotEmpty(lockStatus.getRemainderTimes())) { diff --git a/backend/src/main/java/io/dataease/auth/service/impl/ShiroServiceImpl.java b/backend/src/main/java/io/dataease/auth/service/impl/ShiroServiceImpl.java index e355173d78..6fb74fc1bf 100644 --- a/backend/src/main/java/io/dataease/auth/service/impl/ShiroServiceImpl.java +++ b/backend/src/main/java/io/dataease/auth/service/impl/ShiroServiceImpl.java @@ -81,6 +81,7 @@ public class ShiroServiceImpl implements ShiroService { filterChainDefinitionMap.put("/api/auth/login", ANON); + filterChainDefinitionMap.put("/api/auth/seizeLogin", ANON); filterChainDefinitionMap.put("/api/auth/logout", ANON); filterChainDefinitionMap.put("/api/auth/isPluginLoaded", ANON); filterChainDefinitionMap.put("/system/requestTimeOut", ANON); diff --git a/backend/src/main/java/io/dataease/auth/util/JWTUtils.java b/backend/src/main/java/io/dataease/auth/util/JWTUtils.java index a541433842..65d92e8f83 100644 --- a/backend/src/main/java/io/dataease/auth/util/JWTUtils.java +++ b/backend/src/main/java/io/dataease/auth/util/JWTUtils.java @@ -6,15 +6,24 @@ import com.auth0.jwt.JWTCreator.Builder; import com.auth0.jwt.algorithms.Algorithm; import com.auth0.jwt.interfaces.DecodedJWT; import com.auth0.jwt.interfaces.Verification; +import com.google.gson.Gson; import io.dataease.auth.entity.TokenInfo; import io.dataease.auth.entity.TokenInfo.TokenInfoBuilder; -import io.dataease.commons.utils.CommonBeanFactory; +import io.dataease.commons.exception.DEException; +import io.dataease.commons.model.OnlineUserModel; +import io.dataease.commons.utils.*; import io.dataease.exception.DataEaseException; +import org.apache.commons.collections4.CollectionUtils; import org.apache.commons.lang3.ObjectUtils; import org.apache.commons.lang3.StringUtils; import org.springframework.core.env.Environment; -import java.util.Date; +import javax.servlet.http.Cookie; +import javax.servlet.http.HttpServletResponse; +import java.net.URLEncoder; +import java.util.*; +import java.util.stream.Collectors; +import java.util.stream.Stream; public class JWTUtils { @@ -68,23 +77,105 @@ public class JWTUtils { * @return 加密的token */ public static String sign(TokenInfo tokenInfo, String secret) { + return sign(tokenInfo, secret, true); + } + + private static boolean tokenValid(OnlineUserModel model) { + String token = model.getToken(); + // 如果已经加入黑名单 则直接返回无效 + boolean invalid = TokenCacheUtils.invalid(token); + if (invalid) return false; + + Long loginTime = model.getLoginTime(); + if (ObjectUtils.isEmpty(expireTime)) { + expireTime = CommonBeanFactory.getBean(Environment.class).getProperty("dataease.login_timeout", Long.class, 480L); + } + long expireTimeMillis = expireTime * 60000L; + // 如果当前时间减去登录时间小于超时时间则说明token未过期 返回有效状态 + return System.currentTimeMillis() - loginTime < expireTimeMillis; + + } + + private static Set filterValid(Set userModels) { + Set models = userModels.stream().filter(JWTUtils::tokenValid).collect(Collectors.toSet()); + + return models; + } + + private static String models2Json(Set models, boolean withCurToken, String token) { + Gson gson = new Gson(); + List userModels = models.stream().map(item -> { + item.setToken(null); + return item; + }).collect(Collectors.toList()); + if (withCurToken) { + userModels.get(0).setToken(token); + } + String json = gson.toJson(userModels); try { - if (ObjectUtils.isEmpty(expireTime)) { - expireTime = CommonBeanFactory.getBean(Environment.class).getProperty("dataease.login_timeout", Long.class, 480L); - } - long expireTimeMillis = expireTime * 60000L; - Date date = new Date(System.currentTimeMillis() + expireTimeMillis); - Algorithm algorithm = Algorithm.HMAC256(secret); - Builder builder = JWT.create() - .withClaim("username", tokenInfo.getUsername()) - .withClaim("userId", tokenInfo.getUserId()); - String sign = builder.withExpiresAt(date).sign(algorithm); - return sign; + return URLEncoder.encode(json, "utf-8"); } catch (Exception e) { return null; } } + public static String seizeSign(Long userId, String token) { + Set userModels = Optional.ofNullable(TokenCacheUtils.onlineUserTokens(userId)).orElse(new LinkedHashSet<>()); + userModels.stream().forEach(model -> { + TokenCacheUtils.add(model.getToken(), userId); + }); + userModels.clear(); + OnlineUserModel curModel = TokenCacheUtils.buildModel(token); + userModels.add(curModel); + TokenCacheUtils.resetOnlinePools(userId, userModels); + return IPUtils.get(); + } + public static String sign(TokenInfo tokenInfo, String secret, boolean writeOnline) { + + Long userId = tokenInfo.getUserId(); + String multiLoginType = null; + if (writeOnline && StringUtils.equals("1", (multiLoginType = TokenCacheUtils.multiLoginType()))) { + Set userModels = TokenCacheUtils.onlineUserTokens(userId); + if (CollectionUtils.isNotEmpty(userModels) && CollectionUtils.isNotEmpty((userModels = filterValid(userModels)))) { + TokenCacheUtils.resetOnlinePools(userId, userModels); + HttpServletResponse response = ServletUtils.response(); + Cookie cookie_token = new Cookie("MultiLoginError1", models2Json(userModels, false, null));cookie_token.setPath("/"); + cookie_token.setPath("/"); + response.addCookie(cookie_token); + DataEaseException.throwException("MultiLoginError1"); + } + + } + if (ObjectUtils.isEmpty(expireTime)) { + expireTime = CommonBeanFactory.getBean(Environment.class).getProperty("dataease.login_timeout", Long.class, 480L); + } + long expireTimeMillis = expireTime * 60000L; + Date date = new Date(System.currentTimeMillis() + expireTimeMillis); + Algorithm algorithm = Algorithm.HMAC256(secret); + Builder builder = JWT.create() + .withClaim("username", tokenInfo.getUsername()) + .withClaim("userId", userId); + String sign = builder.withExpiresAt(date).sign(algorithm); + if (writeOnline && !StringUtils.equals("0", multiLoginType)) { + if (StringUtils.equals("2", multiLoginType)) { + Set userModels = TokenCacheUtils.onlineUserTokens(userId); + if (CollectionUtils.isNotEmpty(userModels) && CollectionUtils.isNotEmpty((userModels = filterValid(userModels)))) { + HttpServletResponse response = ServletUtils.response(); + Cookie cookie_token = new Cookie("MultiLoginError2", models2Json(userModels, true, sign)); + cookie_token.setPath("/"); + response.addCookie(cookie_token); + userModels = userModels.stream().filter(mode -> !StringUtils.equals(mode.getToken(), sign)).collect(Collectors.toSet()); + TokenCacheUtils.resetOnlinePools(userId, userModels); + DataEaseException.throwException("MultiLoginError"); + } + } + TokenCacheUtils.add2OnlinePools(sign, userId); + } + return sign; + + } + + public static String signLink(String resourceId, Long userId, String secret) { Algorithm algorithm = Algorithm.HMAC256(secret); if (userId == null) { diff --git a/backend/src/main/java/io/dataease/commons/constants/ParamConstants.java b/backend/src/main/java/io/dataease/commons/constants/ParamConstants.java index 5ad20dc5ae..2b1ca6686f 100644 --- a/backend/src/main/java/io/dataease/commons/constants/ParamConstants.java +++ b/backend/src/main/java/io/dataease/commons/constants/ParamConstants.java @@ -127,6 +127,8 @@ public interface ParamConstants { LOGIN_LIMIT_OPEN("loginlimit.open"), SCAN_CREATE_USER("loginlimit.scanCreateUser"), + + MULTI_LOGIN("loginlimit.multiLogin"), TEMPLATE_ACCESS_KEY("basic.templateAccessKey"); private String value; diff --git a/backend/src/main/java/io/dataease/commons/model/OnlineUserModel.java b/backend/src/main/java/io/dataease/commons/model/OnlineUserModel.java new file mode 100644 index 0000000000..d7ca78d025 --- /dev/null +++ b/backend/src/main/java/io/dataease/commons/model/OnlineUserModel.java @@ -0,0 +1,19 @@ +package io.dataease.commons.model; + +import lombok.Data; +import net.minidev.json.annotate.JsonIgnore; + +import java.io.Serializable; + +@Data +public class OnlineUserModel implements Serializable { + + private static final long serialVersionUID = 190044376129186283L; + + @JsonIgnore + private String token; + + private String ip; + + private Long loginTime; +} diff --git a/backend/src/main/java/io/dataease/commons/utils/TokenCacheUtils.java b/backend/src/main/java/io/dataease/commons/utils/TokenCacheUtils.java index 32b68267ab..6797de44b7 100644 --- a/backend/src/main/java/io/dataease/commons/utils/TokenCacheUtils.java +++ b/backend/src/main/java/io/dataease/commons/utils/TokenCacheUtils.java @@ -1,13 +1,18 @@ package io.dataease.commons.utils; +import io.dataease.commons.model.OnlineUserModel; import io.dataease.listener.util.CacheUtils; +import io.dataease.service.system.SystemParameterService; import org.apache.commons.lang3.ObjectUtils; import org.apache.commons.lang3.StringUtils; import org.springframework.beans.factory.annotation.Value; import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.data.redis.core.SetOperations; import org.springframework.data.redis.core.ValueOperations; import org.springframework.stereotype.Component; +import java.util.LinkedHashSet; +import java.util.Set; import java.util.concurrent.TimeUnit; @@ -17,6 +22,8 @@ public class TokenCacheUtils { private static final String KEY = "sys_token_store"; + private static final String ONLINE_TOKEN_POOL_KEY = "online_token_store"; + private static String cacheType; private static Long expTime; @@ -76,4 +83,63 @@ public class TokenCacheUtils { return ObjectUtils.isNotEmpty(sys_token_store) && StringUtils.isNotBlank(sys_token_store.toString()); } + public static void resetOnlinePools(Long userId, Set sets) { + if (useRedis()) { + RedisTemplate redisTemplate = (RedisTemplate) CommonBeanFactory.getBean("redisTemplate"); + redisTemplate.delete(ONLINE_TOKEN_POOL_KEY + userId); + SetOperations setOperations = redisTemplate.opsForSet(); + Object[] modelArray = sets.stream().toArray(); + setOperations.add(ONLINE_TOKEN_POOL_KEY + userId, modelArray); + return; + } + CacheUtils.removeAll(ONLINE_TOKEN_POOL_KEY); + CacheUtils.put(ONLINE_TOKEN_POOL_KEY, userId, sets, null, null); + CacheUtils.flush(ONLINE_TOKEN_POOL_KEY); + } + + public static void add2OnlinePools(String token, Long userId) { + if (useRedis()) { + RedisTemplate redisTemplate = (RedisTemplate) CommonBeanFactory.getBean("redisTemplate"); + SetOperations setOperations = redisTemplate.opsForSet(); + setOperations.add(ONLINE_TOKEN_POOL_KEY + userId, buildModel(token)); + return; + } + Object listObj = null; + Set models = null; + if (ObjectUtils.isEmpty(listObj = CacheUtils.get(ONLINE_TOKEN_POOL_KEY, userId))) { + models = new LinkedHashSet<>(); + } else { + models = (Set) listObj; + } + models.add(buildModel(token)); + CacheUtils.put(ONLINE_TOKEN_POOL_KEY, userId, models, null, null); + CacheUtils.flush(ONLINE_TOKEN_POOL_KEY); + } + + public static String multiLoginType() { + SystemParameterService service = CommonBeanFactory.getBean(SystemParameterService.class); + return service.multiLoginType(); + } + + public static Set onlineUserTokens(Long userId) { + if (useRedis()) { + RedisTemplate redisTemplate = (RedisTemplate) CommonBeanFactory.getBean("redisTemplate"); + SetOperations setOperations = redisTemplate.opsForSet(); + Set tokens = setOperations.members(ONLINE_TOKEN_POOL_KEY + userId); + return tokens; + } + Object o = CacheUtils.get(ONLINE_TOKEN_POOL_KEY, userId); + if (ObjectUtils.isNotEmpty(o)) + return (Set) o; + return null; + } + + public static OnlineUserModel buildModel(String token) { + OnlineUserModel model = new OnlineUserModel(); + model.setToken(token); + model.setIp(IPUtils.get()); + model.setLoginTime(System.currentTimeMillis()); + return model; + } + } diff --git a/backend/src/main/java/io/dataease/job/sechedule/strategy/impl/EmailTaskHandler.java b/backend/src/main/java/io/dataease/job/sechedule/strategy/impl/EmailTaskHandler.java index c30dd02c10..7d49457f92 100644 --- a/backend/src/main/java/io/dataease/job/sechedule/strategy/impl/EmailTaskHandler.java +++ b/backend/src/main/java/io/dataease/job/sechedule/strategy/impl/EmailTaskHandler.java @@ -6,13 +6,10 @@ import io.dataease.auth.entity.TokenInfo; import io.dataease.auth.service.AuthUserService; import io.dataease.auth.service.impl.AuthUserServiceImpl; import io.dataease.auth.util.JWTUtils; +import io.dataease.commons.utils.*; import io.dataease.dto.PermissionProxy; import io.dataease.dto.chart.ViewOption; import io.dataease.ext.ExtTaskMapper; -import io.dataease.commons.utils.CommonBeanFactory; -import io.dataease.commons.utils.CronUtils; -import io.dataease.commons.utils.LogUtil; -import io.dataease.commons.utils.ServletUtils; import io.dataease.job.sechedule.ScheduleManager; import io.dataease.job.sechedule.strategy.TaskHandler; import io.dataease.plugins.common.base.domain.SysUserAssist; @@ -164,6 +161,7 @@ public class EmailTaskHandler extends TaskHandler implements Job { AuthUserServiceImpl userService = SpringContextUtil.getBean(AuthUserServiceImpl.class); SysUserService sysUserService = SpringContextUtil.getBean(SysUserService.class); List files = null; + String token = null; try { XpackEmailTemplateDTO emailTemplateDTO = emailXpackService.emailTemplate(taskInstance.getTaskId()); XpackEmailTaskRequest taskForm = emailXpackService.taskForm(taskInstance.getTaskId()); @@ -173,7 +171,7 @@ public class EmailTaskHandler extends TaskHandler implements Job { } String panelId = emailTemplateDTO.getPanelId(); String url = panelUrl(panelId); - String token = tokenByUser(user); + token = tokenByUser(user); XpackPixelEntity xpackPixelEntity = buildPixel(emailTemplateDTO); LogUtil.info("url is " + url); LogUtil.info("token is " + token); @@ -349,6 +347,9 @@ public class EmailTaskHandler extends TaskHandler implements Job { error(taskInstance, e); LogUtil.error(e.getMessage(), e); } finally { + if (StringUtils.isNotBlank(token)) { + TokenCacheUtils.add(token, user.getUserId()); + } if (CollectionUtils.isNotEmpty(files)) { files.forEach(file -> { if (file.exists()) { @@ -381,7 +382,7 @@ public class EmailTaskHandler extends TaskHandler implements Job { private String tokenByUser(SysUserEntity user) { TokenInfo tokenInfo = TokenInfo.builder().userId(user.getUserId()).username(user.getUsername()).build(); - String token = JWTUtils.sign(tokenInfo, user.getPassword()); + String token = JWTUtils.sign(tokenInfo, user.getPassword(), false); return token; } diff --git a/backend/src/main/java/io/dataease/service/system/SystemParameterService.java b/backend/src/main/java/io/dataease/service/system/SystemParameterService.java index d162a2f3e8..6d8fe979db 100644 --- a/backend/src/main/java/io/dataease/service/system/SystemParameterService.java +++ b/backend/src/main/java/io/dataease/service/system/SystemParameterService.java @@ -21,6 +21,7 @@ import io.dataease.service.datasource.DatasourceService; import org.apache.commons.collections.CollectionUtils; import org.apache.commons.lang3.ObjectUtils; import org.apache.commons.lang3.StringUtils; +import org.springframework.cache.annotation.CacheEvict; import org.springframework.context.annotation.Lazy; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -34,6 +35,7 @@ import java.util.*; import java.util.concurrent.atomic.AtomicReference; import io.dataease.ext.*; +import springfox.documentation.annotations.Cacheable; @Service @Transactional(rollbackFor = Exception.class) @@ -125,6 +127,13 @@ public class SystemParameterService { boolean open = StringUtils.equals("true", param.getParamValue()); result.setScanCreateUser(open ? "true" : "false"); } + if (StringUtils.equals(param.getParamKey(), ParamConstants.BASIC.MULTI_LOGIN.getValue())) { + String paramValue = param.getParamValue(); + result.setMultiLogin("0"); + if (StringUtils.isNotBlank(paramValue)) { + result.setMultiLogin(paramValue); + } + } } } @@ -150,6 +159,7 @@ public class SystemParameterService { public CasSaveResult editBasic(List parameters) { CasSaveResult casSaveResult = afterSwitchDefaultLogin(parameters); BasicInfo basicInfo = basicInfo(); + String oldMultiLogin = this.getValue("loginlimit.multiLogin"); for (int i = 0; i < parameters.size(); i++) { SystemParameter parameter = parameters.get(i); SystemParameterExample example = new SystemParameterExample(); @@ -163,6 +173,10 @@ public class SystemParameterService { example.clear(); } datasourceService.updateDatasourceStatusJob(basicInfo, parameters); + String newMultiLogin = this.getValue("loginlimit.multiLogin"); + if (!StringUtils.equals(oldMultiLogin, newMultiLogin)) { + clearMultiLoginCache(); + } return casSaveResult; } @@ -364,4 +378,17 @@ public class SystemParameterService { return basicInfo; } + @Cacheable(value = "multiLogin") + public String multiLoginType() { + String value = getValue("loginlimit.multiLogin"); + if (StringUtils.isBlank(value)) { + value = "0"; + } + return value; + } + + @CacheEvict("multiLogin") + public void clearMultiLoginCache() { + } + } diff --git a/backend/src/main/resources/db/migration/V52__1.18.5.sql b/backend/src/main/resources/db/migration/V52__1.18.5.sql index 6367a06935..b44d1a9529 100644 --- a/backend/src/main/resources/db/migration/V52__1.18.5.sql +++ b/backend/src/main/resources/db/migration/V52__1.18.5.sql @@ -52,4 +52,6 @@ END if; END ;; -delimiter ; \ No newline at end of file +delimiter ; + +INSERT INTO `system_parameter` (`param_key`, `param_value`, `type`, `sort`) VALUES ('loginlimit.multiLogin', '0', 'text', '3'); \ No newline at end of file diff --git a/backend/src/main/resources/ehcache/ehcache.xml b/backend/src/main/resources/ehcache/ehcache.xml index 144cdb7ad6..a29e16d5be 100644 --- a/backend/src/main/resources/ehcache/ehcache.xml +++ b/backend/src/main/resources/ehcache/ehcache.xml @@ -283,7 +283,30 @@ + + + + \ No newline at end of file diff --git a/frontend/src/api/user.js b/frontend/src/api/user.js index 587b472952..0b3e11a487 100644 --- a/frontend/src/api/user.js +++ b/frontend/src/api/user.js @@ -8,6 +8,15 @@ export function login(data) { }) } +export function seizeLogin(data) { + return request({ + url: '/api/auth/seizeLogin', + method: 'post', + data, + loading: true + }) +} + export function getInfo(token) { return request({ url: '/api/auth/userInfo', diff --git a/frontend/src/lang/en.js b/frontend/src/lang/en.js index d75a80f00d..a3dfda0eb1 100644 --- a/frontend/src/lang/en.js +++ b/frontend/src/lang/en.js @@ -2870,5 +2870,14 @@ export default { reset: 'Reset', preview: 'Preview', save: 'Save' + }, + multi_login_lang: { + title: 'The current account is online!', + ip: 'IP', + time: 'Login time', + label: 'Prohibit multi-terminal login!', + confirm_title: 'Forced login will cause other clients to go offline', + confirm: 'Whether to force login?', + forced_offline: '`The current account is logged in on the client [${ip}],and you have been pushed off the line!`' } } diff --git a/frontend/src/lang/tw.js b/frontend/src/lang/tw.js index 8735fd18bf..2bd08998ea 100644 --- a/frontend/src/lang/tw.js +++ b/frontend/src/lang/tw.js @@ -2863,5 +2863,14 @@ export default { reset: '重置', preview: '預覽', save: '保存' + }, + multi_login_lang: { + title: '當前賬號已在線!', + ip: 'IP', + time: '登錄時間', + label: '禁止多端登錄!', + confirm_title: '強行登錄會導致其他客戶端掉線', + confirm: '是否強行登錄?', + forced_offline: '`當前賬號在客戶端【${ip}】登錄,您已被擠下線!`' } } diff --git a/frontend/src/lang/zh.js b/frontend/src/lang/zh.js index f9cbd75b23..0cba79a323 100644 --- a/frontend/src/lang/zh.js +++ b/frontend/src/lang/zh.js @@ -1,3 +1,5 @@ +import {$confirm} from "@/utils/message"; + export default { fu: { search_bar: { @@ -2863,5 +2865,14 @@ export default { reset: '重置', preview: '预览', save: '保存' + }, + multi_login_lang: { + title: '当前账号已在线!', + ip: 'IP', + time: '登录时间', + label: '禁止多端登录!', + confirm_title: '强行登录会导致其他客户端掉线', + confirm: '是否强行登录?', + forced_offline: '`当前账号在客户端【${ip}】登录,您已被挤下线!`' } } diff --git a/frontend/src/layout/index.vue b/frontend/src/layout/index.vue index 5794c2ee35..713665e8e9 100644 --- a/frontend/src/layout/index.vue +++ b/frontend/src/layout/index.vue @@ -61,6 +61,7 @@ import DeMainContainer from '@/components/dataease/DeMainContainer' import DeContainer from '@/components/dataease/DeContainer' import DeAsideContainer from '@/components/dataease/DeAsideContainer' import bus from '@/utils/bus' +import { showMultiLoginMsg } from '@/utils/index' import { needModifyPwd, removePwdTips } from '@/api/user' @@ -131,11 +132,22 @@ export default { }, mounted() { bus.$on('PanelSwitchComponent', this.panelSwitchComponent) + bus.$on('web-seize-topic-call', this.webMsgTopicCall) }, beforeDestroy() { bus.$off('PanelSwitchComponent', this.panelSwitchComponent) + bus.$off('web-seize-topic-call', this.webMsgTopicCall) + }, + created() { + showMultiLoginMsg() }, methods: { + webMsgTopicCall(param) { + const ip = param + const msg = this.$t('multi_login_lang.forced_offline') + this.$error(eval(msg)) + bus.$emit('sys-logout') + }, panelSwitchComponent(c) { this.componentName = c.name }, diff --git a/frontend/src/utils/index.js b/frontend/src/utils/index.js index cc19716828..e99a454d1a 100644 --- a/frontend/src/utils/index.js +++ b/frontend/src/utils/index.js @@ -1,4 +1,10 @@ import Cookies from 'js-cookie' +import i18n from '@/lang' +import { $error, $confirm } from '@/utils/message' +import {seizeLogin} from '@/api/user' +import router from '@/router' +import store from "@/store"; +import { Loading } from 'element-ui'; export function timeSection(date, type, labelFormat = 'yyyy-MM-dd') { if (!date) { return null @@ -352,3 +358,45 @@ export const inOtherPlatform = () => { } return false } + +export const showMultiLoginMsg = () => { + const multiLoginError1 = Cookies.get('MultiLoginError1') + if (multiLoginError1) { + Cookies.remove('MultiLoginError1') + const infos = JSON.parse(multiLoginError1) + const content = infos.map(info => buildMultiLoginErrorItem(info)).join('
') + let msgContent = '' + i18n.t('multi_login_lang.title') + '' + msgContent += content + '

' + i18n.t('multi_login_lang.label') + '

' + $error(msgContent, 10000, true); + } + const multiLoginError2 = Cookies.get('MultiLoginError2') + if (multiLoginError2) { + const infos = JSON.parse(multiLoginError2) + Cookies.remove('MultiLoginError2') + const content = infos.map(info => buildMultiLoginErrorItem(info)).join('
') + let msgContent = '' + i18n.t('multi_login_lang.confirm_title') + '' + msgContent += content + '

' + i18n.t('multi_login_lang.confirm') + '

' + $confirm(msgContent, () => seize(infos[0]), { + dangerouslyUseHTMLString: true + }); + } +} +const seize = model => { + let loadingInstance = Loading.service({}); + const token = model.token + const param = { + token + } + seizeLogin(param).then(res => { + const resultToken = res.data.token + store.dispatch('user/refreshToken', resultToken) + router.push('/') + loadingInstance.close(); + }) +} +const buildMultiLoginErrorItem = (info) => { + if (!info) return null + const ip = i18n.t('multi_login_lang.ip') + const time = i18n.t('multi_login_lang.time') + return '

' + ip + ': ' + info.ip + ', ' + time + ': ' + new Date(info.loginTime).format('yyyy-MM-dd hh:mm:ss') + '

' +} diff --git a/frontend/src/utils/message.js b/frontend/src/utils/message.js index da7a19d12d..45f51e3906 100644 --- a/frontend/src/utils/message.js +++ b/frontend/src/utils/message.js @@ -47,12 +47,13 @@ export const $warning = (message, duration) => { }) } -export const $error = (message, duration) => { +export const $error = (message, duration, useHtml) => { Message.error({ message: message, type: 'error', showClose: true, - duration: duration || 10000 + duration: duration || 10000, + dangerouslyUseHTMLString: useHtml }) } diff --git a/frontend/src/utils/request.js b/frontend/src/utils/request.js index 477c20817c..664dab4258 100644 --- a/frontend/src/utils/request.js +++ b/frontend/src/utils/request.js @@ -118,7 +118,7 @@ service.interceptors.response.use(response => { if (msg.length > 600) { msg = msg.slice(0, 600) } - !config.hideMsg && (!headers['authentication-status']) && $error(msg) + !config.hideMsg && (!headers['authentication-status']) && !msg?.startsWith("MultiLoginError") && $error(msg) return Promise.reject(config.url === '/dataset/table/sqlPreview' ? msg : error) }) const checkDownError = response => { diff --git a/frontend/src/views/login/index.vue b/frontend/src/views/login/index.vue index c577ca6809..50df6c744b 100644 --- a/frontend/src/views/login/index.vue +++ b/frontend/src/views/login/index.vue @@ -212,7 +212,7 @@ import { encrypt } from '@/utils/rsaEncrypt' import { ldapStatus, oidcStatus, getPublicKey, pluginLoaded, defaultLoginType, wecomStatus, dingtalkStatus, larkStatus, larksuiteStatus, casStatus, casLoginPage } from '@/api/user' import { getSysUI } from '@/utils/auth' -import { changeFavicon } from '@/utils/index' +import { changeFavicon, showMultiLoginMsg } from '@/utils/index' import { initTheme } from '@/utils/ThemeUtil' import PluginCom from '@/views/system/plugin/PluginCom' import Cookies from 'js-cookie' @@ -395,6 +395,7 @@ export default { this.$error(Cookies.get('LarksuiteError')) } this.clearLarksuiteMsg() + showMultiLoginMsg() }, methods: { @@ -476,14 +477,18 @@ export default { this.$store.dispatch('user/login', user).then(() => { this.$router.push({ path: this.redirect || '/' }) this.loading = false - }).catch(() => { + }).catch((e) => { this.loading = false + e?.response?.data?.message?.startsWith('MultiLoginError') && this.showMessage() }) } else { return false } }) }, + showMessage() { + showMultiLoginMsg() + }, changeLoginType(val) { if (val !== 2 && val !== 7) return this.clearOidcMsg() diff --git a/frontend/src/views/system/sysParam/BasicSetting.vue b/frontend/src/views/system/sysParam/BasicSetting.vue index e544bf03e2..d61ca4dfae 100644 --- a/frontend/src/views/system/sysParam/BasicSetting.vue +++ b/frontend/src/views/system/sysParam/BasicSetting.vue @@ -159,6 +159,13 @@ component-name="LoginLimitSetting" /> + +