diff --git a/orion-visor-modules/orion-visor-module-infra/orion-visor-module-infra-service/src/main/java/org/dromara/visor/module/infra/controller/SystemUserController.java b/orion-visor-modules/orion-visor-module-infra/orion-visor-module-infra-service/src/main/java/org/dromara/visor/module/infra/controller/SystemUserController.java index a1eb81ba..d6760895 100644 --- a/orion-visor-modules/orion-visor-module-infra/orion-visor-module-infra-service/src/main/java/org/dromara/visor/module/infra/controller/SystemUserController.java +++ b/orion-visor-modules/orion-visor-module-infra/orion-visor-module-infra-service/src/main/java/org/dromara/visor/module/infra/controller/SystemUserController.java @@ -38,6 +38,7 @@ import org.dromara.visor.module.infra.define.operator.SystemUserOperatorType; import org.dromara.visor.module.infra.entity.request.user.*; import org.dromara.visor.module.infra.entity.vo.LoginHistoryVO; import org.dromara.visor.module.infra.entity.vo.SystemUserVO; +import org.dromara.visor.module.infra.entity.vo.UserLockedVO; import org.dromara.visor.module.infra.entity.vo.UserSessionVO; import org.dromara.visor.module.infra.service.OperatorLogService; import org.dromara.visor.module.infra.service.SystemUserManagementService; @@ -190,7 +191,33 @@ public class SystemUserController { } @IgnoreLog(IgnoreLogMode.RET) - @GetMapping("/session/list") + @GetMapping("/locked/list") + @Operation(summary = "获取锁定的用户列表") + @PreAuthorize("@ss.hasPermission('infra:system-user:query-lock')") + public List getLockedUserList() { + return systemUserManagementService.getLockedUserList(); + } + + @OperatorLog(SystemUserOperatorType.UNLOCK) + @IgnoreLog(IgnoreLogMode.RET) + @PutMapping("/locked/unlock") + @Operation(summary = "解锁用户") + @PreAuthorize("@ss.hasPermission('infra:system-user:management:unlock')") + public Boolean unlockLockedUser(@RequestBody UserUnlockRequest request) { + systemUserManagementService.unlockLockedUser(request); + return true; + } + + @IgnoreLog(IgnoreLogMode.RET) + @GetMapping("/session/users/list") + @Operation(summary = "获取全部用户会话列表") + @PreAuthorize("@ss.hasPermission('infra:system-user:query-session')") + public List getUsersSessionList() { + return systemUserManagementService.getUsersSessionList(); + } + + @IgnoreLog(IgnoreLogMode.RET) + @GetMapping("/session/user/list") @Operation(summary = "获取用户会话列表") @PreAuthorize("@ss.hasPermission('infra:system-user:query-session')") public List getUserSessionList(@RequestParam("id") Long id) { diff --git a/orion-visor-modules/orion-visor-module-infra/orion-visor-module-infra-service/src/main/java/org/dromara/visor/module/infra/define/cache/UserCacheKeyDefine.java b/orion-visor-modules/orion-visor-module-infra/orion-visor-module-infra-service/src/main/java/org/dromara/visor/module/infra/define/cache/UserCacheKeyDefine.java index 8a66388f..2c1ddda0 100644 --- a/orion-visor-modules/orion-visor-module-infra/orion-visor-module-infra-service/src/main/java/org/dromara/visor/module/infra/define/cache/UserCacheKeyDefine.java +++ b/orion-visor-modules/orion-visor-module-infra/orion-visor-module-infra-service/src/main/java/org/dromara/visor/module/infra/define/cache/UserCacheKeyDefine.java @@ -26,6 +26,7 @@ import cn.orionsec.kit.lang.define.cache.key.CacheKeyBuilder; import cn.orionsec.kit.lang.define.cache.key.CacheKeyDefine; import cn.orionsec.kit.lang.define.cache.key.struct.RedisCacheStruct; import org.dromara.visor.common.security.LoginUser; +import org.dromara.visor.module.infra.entity.dto.LoginFailedDTO; import org.dromara.visor.module.infra.entity.dto.LoginTokenDTO; import org.dromara.visor.module.infra.entity.dto.UserInfoDTO; @@ -56,12 +57,13 @@ public interface UserCacheKeyDefine { .timeout(8, TimeUnit.HOURS) .build(); - CacheKeyDefine LOGIN_FAILED_COUNT = new CacheKeyBuilder() - .key("user:login-failed:{}") - .desc("用户登录失败次数 ${username}") + CacheKeyDefine LOGIN_FAILED = new CacheKeyBuilder() + .key("user:login-failed-info:{}") + .desc("用户登录失败信息 ${username}") .noPrefix() - .type(Integer.class) + .type(LoginFailedDTO.class) .struct(RedisCacheStruct.STRING) + .timeout(24 * 60, TimeUnit.MINUTES) .build(); CacheKeyDefine LOGIN_TOKEN = new CacheKeyBuilder() diff --git a/orion-visor-modules/orion-visor-module-infra/orion-visor-module-infra-service/src/main/java/org/dromara/visor/module/infra/define/operator/SystemUserOperatorType.java b/orion-visor-modules/orion-visor-module-infra/orion-visor-module-infra-service/src/main/java/org/dromara/visor/module/infra/define/operator/SystemUserOperatorType.java index 7c052b8e..d1d87687 100644 --- a/orion-visor-modules/orion-visor-module-infra/orion-visor-module-infra-service/src/main/java/org/dromara/visor/module/infra/define/operator/SystemUserOperatorType.java +++ b/orion-visor-modules/orion-visor-module-infra/orion-visor-module-infra-service/src/main/java/org/dromara/visor/module/infra/define/operator/SystemUserOperatorType.java @@ -50,6 +50,8 @@ public class SystemUserOperatorType extends InitializingOperatorTypes { public static final String DELETE = "system-user:delete"; + public static final String UNLOCK = "system-user:unlock"; + public static final String OFFLINE = "system-user:offline"; @Override @@ -61,6 +63,7 @@ public class SystemUserOperatorType extends InitializingOperatorTypes { new OperatorType(M, GRANT_ROLE, "分配用户角色 ${username}"), new OperatorType(H, RESET_PASSWORD, "重置用户密码 ${username}"), new OperatorType(H, DELETE, "删除用户 ${username}"), + new OperatorType(M, UNLOCK, "解锁用户 ${username}"), new OperatorType(M, OFFLINE, "下线用户会话 ${username}"), }; } diff --git a/orion-visor-modules/orion-visor-module-infra/orion-visor-module-infra-service/src/main/java/org/dromara/visor/module/infra/entity/dto/LoginFailedDTO.java b/orion-visor-modules/orion-visor-module-infra/orion-visor-module-infra-service/src/main/java/org/dromara/visor/module/infra/entity/dto/LoginFailedDTO.java new file mode 100644 index 00000000..94bf42a6 --- /dev/null +++ b/orion-visor-modules/orion-visor-module-infra/orion-visor-module-infra-service/src/main/java/org/dromara/visor/module/infra/entity/dto/LoginFailedDTO.java @@ -0,0 +1,42 @@ +package org.dromara.visor.module.infra.entity.dto; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.dromara.visor.common.entity.RequestIdentityModel; + +/** + * 登录失败信息 + * + * @author Jiahang Li + * @version 1.0.0 + * @since 2025/10/8 15:44 + */ +@Data +@Builder +@AllArgsConstructor +@NoArgsConstructor +public class LoginFailedDTO { + + /** + * 用户名 + */ + private String username; + + /** + * 失败次数 + */ + private Integer failedCount; + + /** + * 失效时间 + */ + private Long expireTime; + + /** + * 原始登录留痕信息 + */ + private RequestIdentityModel origin; + +} diff --git a/orion-visor-modules/orion-visor-module-infra/orion-visor-module-infra-service/src/main/java/org/dromara/visor/module/infra/entity/vo/UserSessionVO.java b/orion-visor-modules/orion-visor-module-infra/orion-visor-module-infra-service/src/main/java/org/dromara/visor/module/infra/entity/vo/UserSessionVO.java index 6b527d22..b3b1b2cb 100644 --- a/orion-visor-modules/orion-visor-module-infra/orion-visor-module-infra-service/src/main/java/org/dromara/visor/module/infra/entity/vo/UserSessionVO.java +++ b/orion-visor-modules/orion-visor-module-infra/orion-visor-module-infra-service/src/main/java/org/dromara/visor/module/infra/entity/vo/UserSessionVO.java @@ -47,6 +47,12 @@ public class UserSessionVO implements Serializable { private static final long serialVersionUID = 1L; + @Schema(description = "id") + private Long id; + + @Schema(description = "用户名") + private String username; + @Schema(description = "是否为当前会话") private Boolean current; diff --git a/orion-visor-modules/orion-visor-module-infra/orion-visor-module-infra-service/src/main/java/org/dromara/visor/module/infra/service/impl/SystemUserManagementServiceImpl.java b/orion-visor-modules/orion-visor-module-infra/orion-visor-module-infra-service/src/main/java/org/dromara/visor/module/infra/service/impl/SystemUserManagementServiceImpl.java index 07a7c38b..fbbe597d 100644 --- a/orion-visor-modules/orion-visor-module-infra/orion-visor-module-infra-service/src/main/java/org/dromara/visor/module/infra/service/impl/SystemUserManagementServiceImpl.java +++ b/orion-visor-modules/orion-visor-module-infra/orion-visor-module-infra-service/src/main/java/org/dromara/visor/module/infra/service/impl/SystemUserManagementServiceImpl.java @@ -22,30 +22,32 @@ */ package org.dromara.visor.module.infra.service.impl; +import cn.orionsec.kit.lang.utils.Objects1; import cn.orionsec.kit.lang.utils.collect.Lists; import lombok.extern.slf4j.Slf4j; import org.dromara.visor.common.constant.ErrorMessage; +import org.dromara.visor.common.entity.RequestIdentityModel; import org.dromara.visor.common.utils.Assert; import org.dromara.visor.common.utils.Requests; import org.dromara.visor.framework.biz.operator.log.core.utils.OperatorLogs; import org.dromara.visor.framework.redis.core.utils.RedisStrings; import org.dromara.visor.framework.security.core.utils.SecurityUtils; +import org.dromara.visor.module.common.config.AppLoginConfig; import org.dromara.visor.module.infra.dao.SystemUserDAO; import org.dromara.visor.module.infra.define.cache.UserCacheKeyDefine; import org.dromara.visor.module.infra.entity.domain.SystemUserDO; +import org.dromara.visor.module.infra.entity.dto.LoginFailedDTO; import org.dromara.visor.module.infra.entity.dto.LoginTokenDTO; -import org.dromara.visor.module.infra.entity.dto.LoginTokenIdentityDTO; import org.dromara.visor.module.infra.entity.request.user.UserSessionOfflineRequest; +import org.dromara.visor.module.infra.entity.request.user.UserUnlockRequest; +import org.dromara.visor.module.infra.entity.vo.UserLockedVO; import org.dromara.visor.module.infra.entity.vo.UserSessionVO; import org.dromara.visor.module.infra.enums.LoginTokenStatusEnum; import org.dromara.visor.module.infra.service.SystemUserManagementService; import org.springframework.stereotype.Service; import javax.annotation.Resource; -import java.util.Comparator; -import java.util.Date; -import java.util.List; -import java.util.Set; +import java.util.*; import java.util.stream.Collectors; /** @@ -59,6 +61,9 @@ import java.util.stream.Collectors; @Service public class SystemUserManagementServiceImpl implements SystemUserManagementService { + @Resource + private AppLoginConfig appLoginConfig; + @Resource private SystemUserDAO systemUserDAO; @@ -69,6 +74,53 @@ public class SystemUserManagementServiceImpl implements SystemUserManagementServ return Lists.size(keys); } + @Override + public List getLockedUserList() { + // 扫描缓存 + Set keys = RedisStrings.scanKeys(UserCacheKeyDefine.LOGIN_FAILED.format("*")); + if (Lists.isEmpty(keys)) { + return Lists.empty(); + } + // 查询缓存 + List loginFailedList = RedisStrings.getJsonList(keys, UserCacheKeyDefine.LOGIN_FAILED); + if (Lists.isEmpty(loginFailedList)) { + return Lists.empty(); + } + // 返回 + return loginFailedList.stream() + .filter(Objects::nonNull) + .filter(s -> s.getFailedCount() >= appLoginConfig.getLoginFailedLockThreshold()) + .map(s -> { + RequestIdentityModel origin = s.getOrigin(); + return UserLockedVO.builder() + .username(s.getUsername()) + .expireTime(s.getExpireTime()) + .address(origin.getAddress()) + .location(origin.getLocation()) + .userAgent(origin.getUserAgent()) + .loginTime(new Date(origin.getTimestamp())) + .build(); + }) + .sorted(Comparator.comparing(UserLockedVO::getLoginTime).reversed()) + .collect(Collectors.toList()); + } + + @Override + public void unlockLockedUser(UserUnlockRequest request) { + RedisStrings.delete(UserCacheKeyDefine.LOGIN_FAILED.format(request.getUsername())); + } + + @Override + public List getUsersSessionList() { + // 扫描缓存 + Set keys = RedisStrings.scanKeys(UserCacheKeyDefine.LOGIN_TOKEN.format("*", "*")); + if (Lists.isEmpty(keys)) { + return Lists.empty(); + } + // 获取用户会话列表 + return this.getUserSessionList(keys); + } + @Override public List getUserSessionList(Long userId) { // 扫描缓存 @@ -76,23 +128,39 @@ public class SystemUserManagementServiceImpl implements SystemUserManagementServ if (Lists.isEmpty(keys)) { return Lists.empty(); } + // 获取用户会话列表 + return this.getUserSessionList(keys); + } + + /** + * 获取用户会话列表 + * + * @param keys keys + * @return rows + */ + private List getUserSessionList(Set keys) { + Long loginUserId = SecurityUtils.getLoginUserId(); // 查询缓存 List tokens = RedisStrings.getJsonList(keys, UserCacheKeyDefine.LOGIN_TOKEN); if (Lists.isEmpty(tokens)) { return Lists.empty(); } - final boolean isCurrentUser = userId.equals(SecurityUtils.getLoginUserId()); // 返回 return tokens.stream() + .filter(Objects::nonNull) .filter(s -> LoginTokenStatusEnum.OK.getStatus().equals(s.getStatus())) - .map(LoginTokenDTO::getOrigin) - .map(s -> UserSessionVO.builder() - .current(isCurrentUser && s.getLoginTime().equals(SecurityUtils.getLoginTimestamp())) - .address(s.getAddress()) - .location(s.getLocation()) - .userAgent(s.getUserAgent()) - .loginTime(new Date(s.getLoginTime())) - .build()) + .map(s -> { + RequestIdentityModel origin = s.getOrigin(); + return UserSessionVO.builder() + .id(s.getId()) + .username(s.getUsername()) + .current(Objects1.eq(loginUserId, s.getId()) && origin.getTimestamp().equals(SecurityUtils.getLoginTimestamp())) + .address(origin.getAddress()) + .location(origin.getLocation()) + .userAgent(origin.getUserAgent()) + .loginTime(new Date(origin.getTimestamp())) + .build(); + }) .sorted(Comparator.comparing(UserSessionVO::getCurrent).reversed() .thenComparing(Comparator.comparing(UserSessionVO::getLoginTime).reversed())) .collect(Collectors.toList()); @@ -115,11 +183,8 @@ public class SystemUserManagementServiceImpl implements SystemUserManagementServ LoginTokenDTO tokenInfo = RedisStrings.getJson(tokenKey, UserCacheKeyDefine.LOGIN_TOKEN); if (tokenInfo != null) { tokenInfo.setStatus(LoginTokenStatusEnum.SESSION_OFFLINE.getStatus()); - LoginTokenIdentityDTO override = new LoginTokenIdentityDTO(); - override.setLoginTime(System.currentTimeMillis()); - // 设置请求信息 - Requests.fillIdentity(override); - tokenInfo.setOverride(override); + // 设置留痕信息 + tokenInfo.setOverride(Requests.getIdentity()); // 更新 token RedisStrings.setJson(tokenKey, UserCacheKeyDefine.LOGIN_TOKEN, tokenInfo); }