feat: 用户会话后端代码.

This commit is contained in:
lijiahangmax
2023-11-01 01:48:07 +08:00
parent 0dfddf68ca
commit cfcb5cb7a8
19 changed files with 292 additions and 50 deletions

View File

@@ -1,7 +1,8 @@
{
"local": {
"baseUrl": "http://127.0.0.1:9200/orion-api",
"token": "Bearer YQJ3IpwJJv5HujIWY6ZTNDgUxXRY6aDt"
"token": "Bearer YQJ3IpwJJv5HujIWY6ZTNDgUxXRY6aDt",
"timestamp": 1689577685914
},
"gateway": {
"baseUrl": "http://127.0.0.1:9200/orion-api",

View File

@@ -46,11 +46,11 @@ public enum ErrorCode implements CodeInfo {
// -------------------- 自定义 - 业务 --------------------
OTHER_DEVICE_LOGIN(700, "该账号于 {} 已在其他设备登录 {}({})"),
USER_DISABLED(700, "当前用户已禁用"),
USER_DISABLED(701, "当前用户已禁用"),
USER_LOCKED(701, "当前用户已被锁定"),
USER_LOCKED(702, "当前用户已被锁定"),
OTHER_DEVICE_LOGIN(702, "该账号于 {} 已在其他设备登录 {}({})"),
// -------------------- 自定义 - 通用 --------------------

View File

@@ -31,6 +31,9 @@ public class LoginUser {
@Schema(description = "头像地址")
private String avatar;
@Schema(description = "登录时间戳")
private Long timestamp;
@Schema(description = "角色")
private List<String> roles;

View File

@@ -7,6 +7,7 @@ import com.orion.lang.define.cache.CacheKeyDefine;
import com.orion.lang.utils.Strings;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.function.Consumer;
import java.util.stream.Collectors;
@@ -83,7 +84,7 @@ public class RedisStrings extends RedisUtils {
* @param keys keys
* @return cache
*/
public static List<JSONObject> getJsonList(List<String> keys) {
public static List<JSONObject> getJsonList(Collection<String> keys) {
List<String> values = redisTemplate.opsForValue().multiGet(keys);
if (values == null) {
return new ArrayList<>();
@@ -101,7 +102,7 @@ public class RedisStrings extends RedisUtils {
* @param <T> T
* @return cache
*/
public static <T> List<T> getJsonList(List<String> keys, CacheKeyDefine define) {
public static <T> List<T> getJsonList(Collection<String> keys, CacheKeyDefine define) {
return getJsonList(keys, (Class<T>) define.getType());
}
@@ -113,7 +114,7 @@ public class RedisStrings extends RedisUtils {
* @param <T> T
* @return cache
*/
public static <T> List<T> getJsonList(List<String> keys, Class<T> type) {
public static <T> List<T> getJsonList(Collection<String> keys, Class<T> type) {
List<String> values = redisTemplate.opsForValue().multiGet(keys);
if (values == null) {
return new ArrayList<>();
@@ -182,7 +183,7 @@ public class RedisStrings extends RedisUtils {
* @param keys keys
* @return cache
*/
public static List<JSONArray> getJsonArrayList(List<String> keys) {
public static List<JSONArray> getJsonArrayList(Collection<String> keys) {
List<String> values = redisTemplate.opsForValue().multiGet(keys);
if (values == null) {
return new ArrayList<>();
@@ -200,7 +201,7 @@ public class RedisStrings extends RedisUtils {
* @param <T> T
* @return cache
*/
public static <T> List<List<T>> getJsonArrayList(List<String> keys, CacheKeyDefine define) {
public static <T> List<List<T>> getJsonArrayList(Collection<String> keys, CacheKeyDefine define) {
return getJsonArrayList(keys, (Class<T>) define.getType());
}
@@ -212,7 +213,7 @@ public class RedisStrings extends RedisUtils {
* @param <T> T
* @return cache
*/
public static <T> List<List<T>> getJsonArrayList(List<String> keys, Class<T> type) {
public static <T> List<List<T>> getJsonArrayList(Collection<String> keys, Class<T> type) {
List<String> values = redisTemplate.opsForValue().multiGet(keys);
if (values == null) {
return new ArrayList<>();

View File

@@ -88,6 +88,16 @@ public class SecurityUtils {
return loginUser != null ? loginUser.getUsername() : null;
}
/**
* 获取当前 timestamp
*
* @return timestamp
*/
public static Long getLoginTimestamp() {
LoginUser loginUser = getLoginUser();
return loginUser != null ? loginUser.getTimestamp() : null;
}
/**
* 设置当前用户
*

View File

@@ -30,4 +30,19 @@ Authorization: {{token}}
GET {{baseUrl}}/infra/mine/login-history
Authorization: {{token}}
### 获取当前用户会话列表
GET {{baseUrl}}/infra/mine/user-session
Authorization: {{token}}
### 下线当前用户会话
PUT {{baseUrl}}/infra/mine/offline-session
Content-Type: application/json
Authorization: {{token}}
{
"timestamp": 1698774195296
}
###

View File

@@ -6,10 +6,12 @@ import com.orion.ops.framework.log.core.annotation.IgnoreLog;
import com.orion.ops.framework.log.core.enums.IgnoreLogMode;
import com.orion.ops.framework.web.core.annotation.RestWrapper;
import com.orion.ops.module.infra.define.operator.AuthenticationOperatorType;
import com.orion.ops.module.infra.entity.request.user.OfflineUserSessionRequest;
import com.orion.ops.module.infra.entity.request.user.SystemUserUpdateRequest;
import com.orion.ops.module.infra.entity.request.user.UserUpdatePasswordRequest;
import com.orion.ops.module.infra.entity.vo.LoginHistoryVO;
import com.orion.ops.module.infra.entity.vo.SystemUserVO;
import com.orion.ops.module.infra.entity.vo.UserSessionVO;
import com.orion.ops.module.infra.service.MineService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
@@ -67,4 +69,22 @@ public class MineController {
return mineService.getCurrentLoginHistory();
}
@IgnoreLog(IgnoreLogMode.RET)
@GetMapping("/user-session")
@Operation(summary = "获取当前用户会话列表")
public List<UserSessionVO> getCurrentUserSessionList() {
return mineService.getCurrentUserSessionList();
}
@IgnoreLog(IgnoreLogMode.RET)
@PutMapping("/offline-session")
@Operation(summary = "下线当前用户会话")
public HttpWrapper<?> offlineCurrentUserSession(@Validated @RequestBody OfflineUserSessionRequest request) {
mineService.offlineCurrentUserSession(request);
return HttpWrapper.ok();
}
// fixme 全部用户接口进行 设置缓存
// fixme 操作日志
}

View File

@@ -11,6 +11,7 @@ import com.orion.ops.framework.web.core.annotation.RestWrapper;
import com.orion.ops.module.infra.define.operator.SystemUserOperatorType;
import com.orion.ops.module.infra.entity.request.user.*;
import com.orion.ops.module.infra.entity.vo.SystemUserVO;
import com.orion.ops.module.infra.entity.vo.UserSessionVO;
import com.orion.ops.module.infra.service.SystemUserRoleService;
import com.orion.ops.module.infra.service.SystemUserService;
import io.swagger.v3.oas.annotations.Operation;
@@ -137,5 +138,22 @@ public class SystemUserController {
return systemUserService.deleteSystemUserById(id);
}
// fixme 权限配置
@IgnoreLog(IgnoreLogMode.RET)
@GetMapping("/user-session")
@Operation(summary = "获取用户会话列表")
public List<UserSessionVO> getUserSessionList(@RequestParam("id") Long id) {
return systemUserService.getUserSessionList(id);
}
// fixme 权限配置
@IgnoreLog(IgnoreLogMode.RET)
@PutMapping("/offline-session")
@Operation(summary = "下线用户会话")
public HttpWrapper<?> offlineUserSession(@Validated @RequestBody OfflineUserSessionRequest request) {
systemUserService.offlineUserSession(request);
return HttpWrapper.ok();
}
}

View File

@@ -38,41 +38,11 @@ public class LoginTokenDTO {
/**
* 原始登录身份
*/
private Identity origin;
private LoginTokenIdentityDTO origin;
/**
* 覆盖登录身份
*/
private Identity override;
/**
* 身份信息
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public static class Identity {
/**
* 原始登录时间
*/
private Long loginTime;
/**
* 当前设备登录地址
*/
private String address;
/**
* 当前设备登录地址
*/
private String location;
/**
* 当前设备 userAgent
*/
private String userAgent;
}
private LoginTokenIdentityDTO override;
}

View File

@@ -0,0 +1,39 @@
package com.orion.ops.module.infra.entity.dto;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* 身份信息
*
* @author Jiahang Li
* @version 1.0.0
* @since 2023/11/1 1:01
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public class LoginTokenIdentityDTO {
/**
* 原始登录时间
*/
private Long loginTime;
/**
* 当前设备登录地址
*/
private String address;
/**
* 当前设备登录地址
*/
private String location;
/**
* 当前设备 userAgent
*/
private String userAgent;
}

View File

@@ -0,0 +1,26 @@
package com.orion.ops.module.infra.entity.request.user;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import javax.validation.constraints.NotNull;
/**
* 用户下线请求
*
* @author Jiahang Li
* @version 1.0.0
* @since 2023/7/17 12:19
*/
@Data
@Schema(name = "OfflineUserSessionRequest", description = "用户下线请求")
public class OfflineUserSessionRequest {
@Schema(description = "userId")
private Long userId;
@NotNull
@Schema(description = "时间戳")
private Long timestamp;
}

View File

@@ -7,7 +7,6 @@ import lombok.Data;
import lombok.NoArgsConstructor;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Size;
import java.io.Serializable;
@@ -25,7 +24,6 @@ import java.io.Serializable;
@Schema(name = "SystemUserUpdateRequest", description = "用户 更新请求对象")
public class SystemUserUpdateRequest implements Serializable {
@NotNull
@Schema(description = "id")
private Long id;

View File

@@ -0,0 +1,43 @@
package com.orion.ops.module.infra.entity.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serializable;
import java.util.Date;
/**
* 用户 会话响应对象
*
* @author Jiahang Li
* @version 1.0.0
* @since 2023-7-13 18:42
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Schema(name = "UserSessionVO", description = "用户 会话响应对象")
public class UserSessionVO implements Serializable {
private static final long serialVersionUID = 1L;
@Schema(description = "是否为当前会话")
private Boolean current;
@Schema(description = "请求ip")
private String address;
@Schema(description = "请求地址")
private String location;
@Schema(description = "userAgent")
private String userAgent;
@Schema(description = "登录时间")
private Date loginTime;
}

View File

@@ -3,8 +3,11 @@ package com.orion.ops.module.infra.framework.service.impl;
import com.orion.lang.utils.time.Dates;
import com.orion.ops.framework.common.constant.ErrorCode;
import com.orion.ops.framework.common.security.LoginUser;
import com.orion.ops.framework.redis.core.utils.RedisUtils;
import com.orion.ops.framework.security.core.service.SecurityFrameworkService;
import com.orion.ops.module.infra.define.cache.UserCacheKeyDefine;
import com.orion.ops.module.infra.entity.dto.LoginTokenDTO;
import com.orion.ops.module.infra.entity.dto.LoginTokenIdentityDTO;
import com.orion.ops.module.infra.enums.LoginTokenStatusEnum;
import com.orion.ops.module.infra.enums.UserStatusEnum;
import com.orion.ops.module.infra.service.AuthenticationService;
@@ -55,12 +58,21 @@ public class SecurityFrameworkServiceImpl implements SecurityFrameworkService {
if (tokenInfo == null) {
return null;
}
// 检查 token 状态
this.checkTokenStatus(tokenInfo);
try {
// 检查 token 状态
this.checkTokenStatus(tokenInfo);
} catch (Exception e) {
// token 失效则删除
// fixme test
RedisUtils.delete(UserCacheKeyDefine.LOGIN_TOKEN.format(tokenInfo.getId(), tokenInfo.getOrigin().getLoginTime()));
throw e;
}
// 获取登录信息
LoginUser user = authenticationService.getLoginUser(tokenInfo.getId());
// 检查用户状态
UserStatusEnum.checkUserStatus(user.getStatus());
// 设置登录时间戳
user.setTimestamp(tokenInfo.getOrigin().getLoginTime());
return user;
}
@@ -77,7 +89,7 @@ public class SecurityFrameworkServiceImpl implements SecurityFrameworkService {
}
// 其他设备登录
if (LoginTokenStatusEnum.OTHER_DEVICE.getStatus().equals(tokenStatus)) {
LoginTokenDTO.Identity override = loginToken.getOverride();
LoginTokenIdentityDTO override = loginToken.getOverride();
throw ErrorCode.OTHER_DEVICE_LOGIN.exception(
Dates.format(new Date(override.getLoginTime()), Dates.MD_HM),
override.getAddress(),

View File

@@ -1,9 +1,11 @@
package com.orion.ops.module.infra.service;
import com.orion.ops.module.infra.entity.request.user.OfflineUserSessionRequest;
import com.orion.ops.module.infra.entity.request.user.SystemUserUpdateRequest;
import com.orion.ops.module.infra.entity.request.user.UserUpdatePasswordRequest;
import com.orion.ops.module.infra.entity.vo.LoginHistoryVO;
import com.orion.ops.module.infra.entity.vo.SystemUserVO;
import com.orion.ops.module.infra.entity.vo.UserSessionVO;
import java.util.List;
@@ -45,4 +47,18 @@ public interface MineService {
*/
List<LoginHistoryVO> getCurrentLoginHistory();
/**
* 获取当前用户会话列表
*
* @return 回话列表
*/
List<UserSessionVO> getCurrentUserSessionList();
/**
* 下线当前用户会话
*
* @param request request
*/
void offlineCurrentUserSession(OfflineUserSessionRequest request);
}

View File

@@ -3,6 +3,7 @@ package com.orion.ops.module.infra.service;
import com.orion.lang.define.wrapper.DataGrid;
import com.orion.ops.module.infra.entity.request.user.*;
import com.orion.ops.module.infra.entity.vo.SystemUserVO;
import com.orion.ops.module.infra.entity.vo.UserSessionVO;
import java.util.List;
@@ -84,4 +85,19 @@ public interface SystemUserService {
*/
void resetPassword(UserResetPasswordRequest request);
/**
* 获取用户会话列表
*
* @param userId userId
* @return 回话列表
*/
List<UserSessionVO> getUserSessionList(Long userId);
/**
* 下线用户会话
*
* @param request request
*/
void offlineUserSession(OfflineUserSessionRequest request);
}

View File

@@ -23,13 +23,13 @@ import com.orion.ops.module.infra.define.cache.UserCacheKeyDefine;
import com.orion.ops.module.infra.entity.domain.SystemRoleDO;
import com.orion.ops.module.infra.entity.domain.SystemUserDO;
import com.orion.ops.module.infra.entity.dto.LoginTokenDTO;
import com.orion.ops.module.infra.entity.dto.LoginTokenIdentityDTO;
import com.orion.ops.module.infra.entity.request.user.UserLoginRequest;
import com.orion.ops.module.infra.entity.vo.UserLoginVO;
import com.orion.ops.module.infra.enums.LoginTokenStatusEnum;
import com.orion.ops.module.infra.enums.UserStatusEnum;
import com.orion.ops.module.infra.service.AuthenticationService;
import com.orion.ops.module.infra.service.PermissionService;
import com.orion.ops.module.infra.service.SystemUserService;
import com.orion.web.servlet.web.Servlets;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
@@ -331,7 +331,7 @@ public class AuthenticationServiceImpl implements AuthenticationService {
for (LoginTokenDTO loginTokenInfo : loginTokenInfoList) {
String deviceLoginKey = UserCacheKeyDefine.LOGIN_TOKEN.format(id, loginTokenInfo.getOrigin().getLoginTime());
loginTokenInfo.setStatus(LoginTokenStatusEnum.OTHER_DEVICE.getStatus());
loginTokenInfo.setOverride(new LoginTokenDTO.Identity(loginTime, remoteAddr, location, userAgent));
loginTokenInfo.setOverride(new LoginTokenIdentityDTO(loginTime, remoteAddr, location, userAgent));
RedisStrings.setJson(deviceLoginKey, UserCacheKeyDefine.LOGIN_TOKEN, loginTokenInfo);
}
}
@@ -364,7 +364,7 @@ public class AuthenticationServiceImpl implements AuthenticationService {
.id(id)
.status(LoginTokenStatusEnum.OK.getStatus())
.refreshCount(0)
.origin(new LoginTokenDTO.Identity(loginTime, remoteAddr, location, userAgent))
.origin(new LoginTokenIdentityDTO(loginTime, remoteAddr, location, userAgent))
.build();
RedisStrings.setJson(loginKey, UserCacheKeyDefine.LOGIN_TOKEN, loginValue);
// 生成 refreshToken

View File

@@ -6,11 +6,13 @@ import com.orion.ops.framework.common.utils.Valid;
import com.orion.ops.framework.security.core.utils.SecurityUtils;
import com.orion.ops.module.infra.dao.SystemUserDAO;
import com.orion.ops.module.infra.entity.domain.SystemUserDO;
import com.orion.ops.module.infra.entity.request.user.OfflineUserSessionRequest;
import com.orion.ops.module.infra.entity.request.user.SystemUserUpdateRequest;
import com.orion.ops.module.infra.entity.request.user.UserResetPasswordRequest;
import com.orion.ops.module.infra.entity.request.user.UserUpdatePasswordRequest;
import com.orion.ops.module.infra.entity.vo.LoginHistoryVO;
import com.orion.ops.module.infra.entity.vo.SystemUserVO;
import com.orion.ops.module.infra.entity.vo.UserSessionVO;
import com.orion.ops.module.infra.service.MineService;
import com.orion.ops.module.infra.service.OperatorLogService;
import com.orion.ops.module.infra.service.SystemUserService;
@@ -73,4 +75,15 @@ public class MineServiceImpl implements MineService {
return operatorLogService.getLoginHistory(username);
}
@Override
public List<UserSessionVO> getCurrentUserSessionList() {
return systemUserService.getUserSessionList(SecurityUtils.getLoginUserId());
}
@Override
public void offlineCurrentUserSession(OfflineUserSessionRequest request) {
request.setUserId(SecurityUtils.getLoginUserId());
systemUserService.offlineUserSession(request);
}
}

View File

@@ -20,8 +20,10 @@ import com.orion.ops.module.infra.dao.SystemUserRoleDAO;
import com.orion.ops.module.infra.define.cache.TipsCacheKeyDefine;
import com.orion.ops.module.infra.define.cache.UserCacheKeyDefine;
import com.orion.ops.module.infra.entity.domain.SystemUserDO;
import com.orion.ops.module.infra.entity.dto.LoginTokenDTO;
import com.orion.ops.module.infra.entity.request.user.*;
import com.orion.ops.module.infra.entity.vo.SystemUserVO;
import com.orion.ops.module.infra.entity.vo.UserSessionVO;
import com.orion.ops.module.infra.enums.UserStatusEnum;
import com.orion.ops.module.infra.service.AuthenticationService;
import com.orion.ops.module.infra.service.FavoriteService;
@@ -34,8 +36,11 @@ import org.springframework.scheduling.annotation.Async;
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.stream.Collectors;
/**
* 用户 服务实现类
@@ -242,6 +247,42 @@ public class SystemUserServiceImpl implements SystemUserService {
}
}
@Override
public List<UserSessionVO> getUserSessionList(Long userId) {
// 扫描缓存
Set<String> keys = RedisStrings.scanKeys(UserCacheKeyDefine.LOGIN_TOKEN.format(userId, "*"));
if (Lists.isEmpty(keys)) {
return Lists.empty();
}
// 查询缓存
List<LoginTokenDTO> tokens = RedisStrings.getJsonList(keys, UserCacheKeyDefine.LOGIN_TOKEN);
if (Lists.isEmpty(tokens)) {
return Lists.empty();
}
// 返回
return tokens.stream()
.map(LoginTokenDTO::getOrigin)
.map(s -> UserSessionVO.builder()
.current(s.getLoginTime().equals(SecurityUtils.getLoginTimestamp()))
.address(s.getAddress())
.location(s.getLocation())
.userAgent(s.getUserAgent())
.loginTime(new Date(s.getLoginTime()))
.build())
.sorted(Comparator.comparing(UserSessionVO::getLoginTime).reversed())
.collect(Collectors.toList());
}
@Override
public void offlineUserSession(OfflineUserSessionRequest request) {
Long userId = Valid.notNull(request.getUserId());
Long timestamp = request.getTimestamp();
RedisStrings.delete(
UserCacheKeyDefine.LOGIN_TOKEN.format(userId, timestamp),
UserCacheKeyDefine.LOGIN_REFRESH.format(userId, request.getTimestamp())
);
}
/**
* 检查用户名否存在
*