🔨 解锁用户.
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.request.user.*;
|
||||||
import org.dromara.visor.module.infra.entity.vo.LoginHistoryVO;
|
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.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.entity.vo.UserSessionVO;
|
||||||
import org.dromara.visor.module.infra.service.OperatorLogService;
|
import org.dromara.visor.module.infra.service.OperatorLogService;
|
||||||
import org.dromara.visor.module.infra.service.SystemUserManagementService;
|
import org.dromara.visor.module.infra.service.SystemUserManagementService;
|
||||||
@@ -190,7 +191,33 @@ public class SystemUserController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@IgnoreLog(IgnoreLogMode.RET)
|
@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 = "获取用户会话列表")
|
@Operation(summary = "获取用户会话列表")
|
||||||
@PreAuthorize("@ss.hasPermission('infra:system-user:query-session')")
|
@PreAuthorize("@ss.hasPermission('infra:system-user:query-session')")
|
||||||
public List<UserSessionVO> getUserSessionList(@RequestParam("id") Long id) {
|
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.CacheKeyDefine;
|
||||||
import cn.orionsec.kit.lang.define.cache.key.struct.RedisCacheStruct;
|
import cn.orionsec.kit.lang.define.cache.key.struct.RedisCacheStruct;
|
||||||
import org.dromara.visor.common.security.LoginUser;
|
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.LoginTokenDTO;
|
||||||
import org.dromara.visor.module.infra.entity.dto.UserInfoDTO;
|
import org.dromara.visor.module.infra.entity.dto.UserInfoDTO;
|
||||||
|
|
||||||
@@ -56,12 +57,13 @@ public interface UserCacheKeyDefine {
|
|||||||
.timeout(8, TimeUnit.HOURS)
|
.timeout(8, TimeUnit.HOURS)
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
CacheKeyDefine LOGIN_FAILED_COUNT = new CacheKeyBuilder()
|
CacheKeyDefine LOGIN_FAILED = new CacheKeyBuilder()
|
||||||
.key("user:login-failed:{}")
|
.key("user:login-failed-info:{}")
|
||||||
.desc("用户登录失败次数 ${username}")
|
.desc("用户登录失败信息 ${username}")
|
||||||
.noPrefix()
|
.noPrefix()
|
||||||
.type(Integer.class)
|
.type(LoginFailedDTO.class)
|
||||||
.struct(RedisCacheStruct.STRING)
|
.struct(RedisCacheStruct.STRING)
|
||||||
|
.timeout(24 * 60, TimeUnit.MINUTES)
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
CacheKeyDefine LOGIN_TOKEN = new CacheKeyBuilder()
|
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 DELETE = "system-user:delete";
|
||||||
|
|
||||||
|
public static final String UNLOCK = "system-user:unlock";
|
||||||
|
|
||||||
public static final String OFFLINE = "system-user:offline";
|
public static final String OFFLINE = "system-user:offline";
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -61,6 +63,7 @@ public class SystemUserOperatorType extends InitializingOperatorTypes {
|
|||||||
new OperatorType(M, GRANT_ROLE, "分配用户角色 <sb>${username}</sb>"),
|
new OperatorType(M, GRANT_ROLE, "分配用户角色 <sb>${username}</sb>"),
|
||||||
new OperatorType(H, RESET_PASSWORD, "重置用户密码 <sb>${username}</sb>"),
|
new OperatorType(H, RESET_PASSWORD, "重置用户密码 <sb>${username}</sb>"),
|
||||||
new OperatorType(H, DELETE, "删除用户 <sb>${username}</sb>"),
|
new OperatorType(H, DELETE, "删除用户 <sb>${username}</sb>"),
|
||||||
|
new OperatorType(M, UNLOCK, "解锁用户 <sb>${username}</sb>"),
|
||||||
new OperatorType(M, OFFLINE, "下线用户会话 <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;
|
private static final long serialVersionUID = 1L;
|
||||||
|
|
||||||
|
@Schema(description = "id")
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
@Schema(description = "用户名")
|
||||||
|
private String username;
|
||||||
|
|
||||||
@Schema(description = "是否为当前会话")
|
@Schema(description = "是否为当前会话")
|
||||||
private Boolean current;
|
private Boolean current;
|
||||||
|
|
||||||
|
|||||||
@@ -22,30 +22,32 @@
|
|||||||
*/
|
*/
|
||||||
package org.dromara.visor.module.infra.service.impl;
|
package org.dromara.visor.module.infra.service.impl;
|
||||||
|
|
||||||
|
import cn.orionsec.kit.lang.utils.Objects1;
|
||||||
import cn.orionsec.kit.lang.utils.collect.Lists;
|
import cn.orionsec.kit.lang.utils.collect.Lists;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.dromara.visor.common.constant.ErrorMessage;
|
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.Assert;
|
||||||
import org.dromara.visor.common.utils.Requests;
|
import org.dromara.visor.common.utils.Requests;
|
||||||
import org.dromara.visor.framework.biz.operator.log.core.utils.OperatorLogs;
|
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.redis.core.utils.RedisStrings;
|
||||||
import org.dromara.visor.framework.security.core.utils.SecurityUtils;
|
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.dao.SystemUserDAO;
|
||||||
import org.dromara.visor.module.infra.define.cache.UserCacheKeyDefine;
|
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.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.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.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.entity.vo.UserSessionVO;
|
||||||
import org.dromara.visor.module.infra.enums.LoginTokenStatusEnum;
|
import org.dromara.visor.module.infra.enums.LoginTokenStatusEnum;
|
||||||
import org.dromara.visor.module.infra.service.SystemUserManagementService;
|
import org.dromara.visor.module.infra.service.SystemUserManagementService;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
import javax.annotation.Resource;
|
import javax.annotation.Resource;
|
||||||
import java.util.Comparator;
|
import java.util.*;
|
||||||
import java.util.Date;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Set;
|
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -59,6 +61,9 @@ import java.util.stream.Collectors;
|
|||||||
@Service
|
@Service
|
||||||
public class SystemUserManagementServiceImpl implements SystemUserManagementService {
|
public class SystemUserManagementServiceImpl implements SystemUserManagementService {
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private AppLoginConfig appLoginConfig;
|
||||||
|
|
||||||
@Resource
|
@Resource
|
||||||
private SystemUserDAO systemUserDAO;
|
private SystemUserDAO systemUserDAO;
|
||||||
|
|
||||||
@@ -69,6 +74,53 @@ public class SystemUserManagementServiceImpl implements SystemUserManagementServ
|
|||||||
return Lists.size(keys);
|
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
|
@Override
|
||||||
public List<UserSessionVO> getUserSessionList(Long userId) {
|
public List<UserSessionVO> getUserSessionList(Long userId) {
|
||||||
// 扫描缓存
|
// 扫描缓存
|
||||||
@@ -76,23 +128,39 @@ public class SystemUserManagementServiceImpl implements SystemUserManagementServ
|
|||||||
if (Lists.isEmpty(keys)) {
|
if (Lists.isEmpty(keys)) {
|
||||||
return Lists.empty();
|
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);
|
List<LoginTokenDTO> tokens = RedisStrings.getJsonList(keys, UserCacheKeyDefine.LOGIN_TOKEN);
|
||||||
if (Lists.isEmpty(tokens)) {
|
if (Lists.isEmpty(tokens)) {
|
||||||
return Lists.empty();
|
return Lists.empty();
|
||||||
}
|
}
|
||||||
final boolean isCurrentUser = userId.equals(SecurityUtils.getLoginUserId());
|
|
||||||
// 返回
|
// 返回
|
||||||
return tokens.stream()
|
return tokens.stream()
|
||||||
|
.filter(Objects::nonNull)
|
||||||
.filter(s -> LoginTokenStatusEnum.OK.getStatus().equals(s.getStatus()))
|
.filter(s -> LoginTokenStatusEnum.OK.getStatus().equals(s.getStatus()))
|
||||||
.map(LoginTokenDTO::getOrigin)
|
.map(s -> {
|
||||||
.map(s -> UserSessionVO.builder()
|
RequestIdentityModel origin = s.getOrigin();
|
||||||
.current(isCurrentUser && s.getLoginTime().equals(SecurityUtils.getLoginTimestamp()))
|
return UserSessionVO.builder()
|
||||||
.address(s.getAddress())
|
.id(s.getId())
|
||||||
.location(s.getLocation())
|
.username(s.getUsername())
|
||||||
.userAgent(s.getUserAgent())
|
.current(Objects1.eq(loginUserId, s.getId()) && origin.getTimestamp().equals(SecurityUtils.getLoginTimestamp()))
|
||||||
.loginTime(new Date(s.getLoginTime()))
|
.address(origin.getAddress())
|
||||||
.build())
|
.location(origin.getLocation())
|
||||||
|
.userAgent(origin.getUserAgent())
|
||||||
|
.loginTime(new Date(origin.getTimestamp()))
|
||||||
|
.build();
|
||||||
|
})
|
||||||
.sorted(Comparator.comparing(UserSessionVO::getCurrent).reversed()
|
.sorted(Comparator.comparing(UserSessionVO::getCurrent).reversed()
|
||||||
.thenComparing(Comparator.comparing(UserSessionVO::getLoginTime).reversed()))
|
.thenComparing(Comparator.comparing(UserSessionVO::getLoginTime).reversed()))
|
||||||
.collect(Collectors.toList());
|
.collect(Collectors.toList());
|
||||||
@@ -115,11 +183,8 @@ public class SystemUserManagementServiceImpl implements SystemUserManagementServ
|
|||||||
LoginTokenDTO tokenInfo = RedisStrings.getJson(tokenKey, UserCacheKeyDefine.LOGIN_TOKEN);
|
LoginTokenDTO tokenInfo = RedisStrings.getJson(tokenKey, UserCacheKeyDefine.LOGIN_TOKEN);
|
||||||
if (tokenInfo != null) {
|
if (tokenInfo != null) {
|
||||||
tokenInfo.setStatus(LoginTokenStatusEnum.SESSION_OFFLINE.getStatus());
|
tokenInfo.setStatus(LoginTokenStatusEnum.SESSION_OFFLINE.getStatus());
|
||||||
LoginTokenIdentityDTO override = new LoginTokenIdentityDTO();
|
// 设置留痕信息
|
||||||
override.setLoginTime(System.currentTimeMillis());
|
tokenInfo.setOverride(Requests.getIdentity());
|
||||||
// 设置请求信息
|
|
||||||
Requests.fillIdentity(override);
|
|
||||||
tokenInfo.setOverride(override);
|
|
||||||
// 更新 token
|
// 更新 token
|
||||||
RedisStrings.setJson(tokenKey, UserCacheKeyDefine.LOGIN_TOKEN, tokenInfo);
|
RedisStrings.setJson(tokenKey, UserCacheKeyDefine.LOGIN_TOKEN, tokenInfo);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user