🔨 解锁用户.

This commit is contained in:
lijiahangmax
2025-11-06 09:50:09 +08:00
parent 2012f20a09
commit 6deebedc75
6 changed files with 169 additions and 24 deletions

View File

@@ -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<UserLockedVO> 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<UserSessionVO> getUsersSessionList() {
return systemUserManagementService.getUsersSessionList();
}
@IgnoreLog(IgnoreLogMode.RET)
@GetMapping("/session/user/list")
@Operation(summary = "获取用户会话列表")
@PreAuthorize("@ss.hasPermission('infra:system-user:query-session')")
public List<UserSessionVO> getUserSessionList(@RequestParam("id") Long id) {

View File

@@ -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()

View File

@@ -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, "分配用户角色 <sb>${username}</sb>"),
new OperatorType(H, RESET_PASSWORD, "重置用户密码 <sb>${username}</sb>"),
new OperatorType(H, DELETE, "删除用户 <sb>${username}</sb>"),
new OperatorType(M, UNLOCK, "解锁用户 <sb>${username}</sb>"),
new OperatorType(M, OFFLINE, "下线用户会话 <sb>${username}</sb>"),
};
}

View File

@@ -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;
}

View File

@@ -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;

View File

@@ -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<UserLockedVO> getLockedUserList() {
// 扫描缓存
Set<String> keys = RedisStrings.scanKeys(UserCacheKeyDefine.LOGIN_FAILED.format("*"));
if (Lists.isEmpty(keys)) {
return Lists.empty();
}
// 查询缓存
List<LoginFailedDTO> 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<UserSessionVO> getUsersSessionList() {
// 扫描缓存
Set<String> keys = RedisStrings.scanKeys(UserCacheKeyDefine.LOGIN_TOKEN.format("*", "*"));
if (Lists.isEmpty(keys)) {
return Lists.empty();
}
// 获取用户会话列表
return this.getUserSessionList(keys);
}
@Override
public List<UserSessionVO> 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<UserSessionVO> getUserSessionList(Set<String> keys) {
Long loginUserId = SecurityUtils.getLoginUserId();
// 查询缓存
List<LoginTokenDTO> 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);
}