🔨 解锁用户.
This commit is contained in:
@@ -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) {
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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>"),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user