first commit
This commit is contained in:
@@ -0,0 +1,112 @@
|
||||
package top.ysoft.admin.auth;
|
||||
|
||||
import cn.dev33.satoken.stp.SaLoginModel;
|
||||
import cn.dev33.satoken.stp.StpUtil;
|
||||
import cn.hutool.core.bean.BeanUtil;
|
||||
import jakarta.annotation.Resource;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
|
||||
import org.springframework.stereotype.Component;
|
||||
import top.ysoft.admin.auth.model.req.LoginReq;
|
||||
import top.ysoft.admin.common.context.RoleContext;
|
||||
import top.ysoft.admin.common.context.UserContext;
|
||||
import top.ysoft.admin.common.context.UserContextHolder;
|
||||
import top.ysoft.admin.common.context.UserExtraContext;
|
||||
import top.ysoft.admin.common.enums.DisEnableStatusEnum;
|
||||
import top.ysoft.admin.system.model.entity.DeptDO;
|
||||
import top.ysoft.admin.system.model.entity.UserDO;
|
||||
import top.ysoft.admin.system.model.resp.ClientResp;
|
||||
import top.ysoft.admin.system.service.DeptService;
|
||||
import top.ysoft.admin.system.service.OptionService;
|
||||
import top.ysoft.admin.system.service.RoleService;
|
||||
import top.ysoft.admin.system.service.UserService;
|
||||
import top.continew.starter.core.validation.CheckUtils;
|
||||
import top.continew.starter.core.validation.Validator;
|
||||
import top.continew.starter.web.util.SpringWebUtils;
|
||||
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
|
||||
import static top.ysoft.admin.system.enums.PasswordPolicyEnum.PASSWORD_EXPIRATION_DAYS;
|
||||
|
||||
/**
|
||||
* 登录处理器基类
|
||||
*
|
||||
* @author KAI
|
||||
* @author Charles7c
|
||||
* @since 2024/12/22 14:52
|
||||
*/
|
||||
@Component
|
||||
public abstract class AbstractLoginHandler<T extends LoginReq> implements LoginHandler<T> {
|
||||
|
||||
@Resource
|
||||
protected OptionService optionService;
|
||||
@Resource
|
||||
protected UserService userService;
|
||||
@Resource
|
||||
protected RoleService roleService;
|
||||
@Resource
|
||||
private DeptService deptService;
|
||||
@Resource
|
||||
private ThreadPoolTaskExecutor threadPoolTaskExecutor;
|
||||
|
||||
protected static final String CAPTCHA_EXPIRED = "验证码已失效";
|
||||
protected static final String CAPTCHA_ERROR = "验证码错误";
|
||||
protected static final String CLIENT_ID = "clientId";
|
||||
|
||||
@Override
|
||||
public void preLogin(T req, ClientResp client, HttpServletRequest request) {
|
||||
// 参数校验
|
||||
Validator.validate(req);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void postLogin(T req, ClientResp client, HttpServletRequest request) {
|
||||
}
|
||||
|
||||
/**
|
||||
* 认证
|
||||
*
|
||||
* @param user 用户信息
|
||||
* @param client 终端信息
|
||||
* @return token 令牌信息
|
||||
*/
|
||||
protected String authenticate(UserDO user, ClientResp client) {
|
||||
// 获取权限、角色、密码过期天数
|
||||
Long userId = user.getId();
|
||||
CompletableFuture<Set<String>> permissionFuture = CompletableFuture.supplyAsync(() -> roleService
|
||||
.listPermissionByUserId(userId), threadPoolTaskExecutor);
|
||||
CompletableFuture<Set<RoleContext>> roleFuture = CompletableFuture.supplyAsync(() -> roleService
|
||||
.listByUserId(userId), threadPoolTaskExecutor);
|
||||
CompletableFuture<Integer> passwordExpirationDaysFuture = CompletableFuture.supplyAsync(() -> optionService
|
||||
.getValueByCode2Int(PASSWORD_EXPIRATION_DAYS.name()));
|
||||
CompletableFuture.allOf(permissionFuture, roleFuture, passwordExpirationDaysFuture);
|
||||
UserContext userContext = new UserContext(permissionFuture.join(), roleFuture
|
||||
.join(), passwordExpirationDaysFuture.join());
|
||||
BeanUtil.copyProperties(user, userContext);
|
||||
// 设置登录配置参数
|
||||
SaLoginModel model = new SaLoginModel();
|
||||
model.setActiveTimeout(client.getActiveTimeout());
|
||||
model.setTimeout(client.getTimeout());
|
||||
model.setDevice(client.getClientType());
|
||||
userContext.setClientType(client.getClientType());
|
||||
model.setExtra(CLIENT_ID, client.getClientId());
|
||||
userContext.setClientId(client.getClientId());
|
||||
// 登录并缓存用户信息
|
||||
StpUtil.login(userContext.getId(), model.setExtraData(BeanUtil.beanToMap(new UserExtraContext(SpringWebUtils
|
||||
.getRequest()))));
|
||||
UserContextHolder.setContext(userContext);
|
||||
return StpUtil.getTokenValue();
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查用户状态
|
||||
*
|
||||
* @param user 用户信息
|
||||
*/
|
||||
protected void checkUserStatus(UserDO user) {
|
||||
CheckUtils.throwIfEqual(DisEnableStatusEnum.DISABLE, user.getStatus(), "此账号已被禁用,如有疑问,请联系管理员");
|
||||
DeptDO dept = deptService.getById(user.getDeptId());
|
||||
CheckUtils.throwIfEqual(DisEnableStatusEnum.DISABLE, dept.getStatus(), "此账号所属部门已被禁用,如有疑问,请联系管理员");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
package top.ysoft.admin.auth;
|
||||
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import top.ysoft.admin.auth.enums.AuthTypeEnum;
|
||||
import top.ysoft.admin.auth.model.req.LoginReq;
|
||||
import top.ysoft.admin.auth.model.resp.LoginResp;
|
||||
import top.ysoft.admin.system.model.resp.ClientResp;
|
||||
|
||||
/**
|
||||
* 登录处理器
|
||||
*
|
||||
* @author KAI
|
||||
* @author Charles7c
|
||||
* @since 2024/12/22 14:52
|
||||
*/
|
||||
public interface LoginHandler<T extends LoginReq> {
|
||||
|
||||
/**
|
||||
* 登录
|
||||
*
|
||||
* @param req 登录请求参数
|
||||
* @param client 终端信息
|
||||
* @param request 请求对象
|
||||
* @return 登录响应参数
|
||||
*/
|
||||
LoginResp login(T req, ClientResp client, HttpServletRequest request);
|
||||
|
||||
/**
|
||||
* 登录前置处理
|
||||
*
|
||||
* @param req 登录请求参数
|
||||
* @param client 终端信息
|
||||
* @param request 请求对象
|
||||
*/
|
||||
void preLogin(T req, ClientResp client, HttpServletRequest request);
|
||||
|
||||
/**
|
||||
* 登录后置处理
|
||||
*
|
||||
* @param req 登录请求参数
|
||||
* @param client 终端信息
|
||||
* @param request 请求对象
|
||||
*/
|
||||
void postLogin(T req, ClientResp client, HttpServletRequest request);
|
||||
|
||||
/**
|
||||
* 获取认证类型
|
||||
*
|
||||
* @return 认证类型
|
||||
*/
|
||||
AuthTypeEnum getAuthType();
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
package top.ysoft.admin.auth;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Component;
|
||||
import top.ysoft.admin.auth.enums.AuthTypeEnum;
|
||||
import top.ysoft.admin.auth.model.req.LoginReq;
|
||||
|
||||
import java.util.EnumMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 登录处理器工厂
|
||||
*
|
||||
* @author KAI
|
||||
* @author Charles7c
|
||||
* @since 2024/12/20 15:16
|
||||
*/
|
||||
@Component
|
||||
public class LoginHandlerFactory {
|
||||
|
||||
private final Map<AuthTypeEnum, LoginHandler<? extends LoginReq>> handlerMap = new EnumMap<>(AuthTypeEnum.class);
|
||||
|
||||
@Autowired
|
||||
public LoginHandlerFactory(List<LoginHandler<? extends LoginReq>> handlers) {
|
||||
for (LoginHandler<? extends LoginReq> handler : handlers) {
|
||||
handlerMap.put(handler.getAuthType(), handler);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据认证类型获取
|
||||
*
|
||||
* @param authType 认证类型
|
||||
* @return 认证处理器
|
||||
*/
|
||||
public LoginHandler<LoginReq> getHandler(AuthTypeEnum authType) {
|
||||
return (LoginHandler<LoginReq>)handlerMap.get(authType);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
package top.ysoft.admin.auth.enums;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import top.ysoft.admin.common.constant.UiConstants;
|
||||
import top.continew.starter.core.enums.BaseEnum;
|
||||
|
||||
/**
|
||||
* 认证类型枚举
|
||||
*
|
||||
* @author KAI
|
||||
* @author Charles7c
|
||||
* @since 2024/12/22 14:52
|
||||
*/
|
||||
@Getter
|
||||
@RequiredArgsConstructor
|
||||
public enum AuthTypeEnum implements BaseEnum<String> {
|
||||
|
||||
/**
|
||||
* 账号
|
||||
*/
|
||||
ACCOUNT("ACCOUNT", "账号", UiConstants.COLOR_SUCCESS),
|
||||
|
||||
/**
|
||||
* 邮箱
|
||||
*/
|
||||
EMAIL("EMAIL", "邮箱", UiConstants.COLOR_PRIMARY),
|
||||
|
||||
/**
|
||||
* 手机号
|
||||
*/
|
||||
PHONE("PHONE", "手机号", UiConstants.COLOR_PRIMARY),
|
||||
|
||||
/**
|
||||
* 第三方账号
|
||||
*/
|
||||
SOCIAL("SOCIAL", "第三方账号", UiConstants.COLOR_ERROR);
|
||||
|
||||
private final String value;
|
||||
private final String description;
|
||||
private final String color;
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
package top.ysoft.admin.auth.handler;
|
||||
|
||||
import cn.hutool.core.util.ObjectUtil;
|
||||
import cn.hutool.extra.servlet.JakartaServletUtil;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||
import org.springframework.stereotype.Component;
|
||||
import top.ysoft.admin.auth.AbstractLoginHandler;
|
||||
import top.ysoft.admin.auth.enums.AuthTypeEnum;
|
||||
import top.ysoft.admin.auth.model.req.AccountLoginReq;
|
||||
import top.ysoft.admin.auth.model.resp.LoginResp;
|
||||
import top.ysoft.admin.common.constant.CacheConstants;
|
||||
import top.ysoft.admin.common.constant.SysConstants;
|
||||
import top.ysoft.admin.common.util.SecureUtils;
|
||||
import top.ysoft.admin.system.enums.PasswordPolicyEnum;
|
||||
import top.ysoft.admin.system.model.entity.UserDO;
|
||||
import top.ysoft.admin.system.model.resp.ClientResp;
|
||||
import top.continew.starter.cache.redisson.util.RedisUtils;
|
||||
import top.continew.starter.core.util.ExceptionUtils;
|
||||
import top.continew.starter.core.validation.CheckUtils;
|
||||
import top.continew.starter.core.validation.ValidationUtils;
|
||||
|
||||
import java.time.Duration;
|
||||
|
||||
/**
|
||||
* 账号登录处理器
|
||||
*
|
||||
* @author KAI
|
||||
* @author Charles7c
|
||||
* @since 2024/12/22 14:58
|
||||
*/
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class AccountLoginHandler extends AbstractLoginHandler<AccountLoginReq> {
|
||||
|
||||
private final PasswordEncoder passwordEncoder;
|
||||
|
||||
@Override
|
||||
public LoginResp login(AccountLoginReq req, ClientResp client, HttpServletRequest request) {
|
||||
// 解密密码
|
||||
String rawPassword = ExceptionUtils.exToNull(() -> SecureUtils.decryptByRsaPrivateKey(req.getPassword()));
|
||||
ValidationUtils.throwIfBlank(rawPassword, "密码解密失败");
|
||||
// 验证用户名密码
|
||||
String username = req.getUsername();
|
||||
UserDO user = userService.getByUsername(username);
|
||||
boolean isError = ObjectUtil.isNull(user) || !passwordEncoder.matches(rawPassword, user.getPassword());
|
||||
// 检查账号锁定状态
|
||||
this.checkUserLocked(req.getUsername(), request, isError);
|
||||
ValidationUtils.throwIf(isError, "用户名或密码错误");
|
||||
// 检查用户状态
|
||||
super.checkUserStatus(user);
|
||||
// 执行认证
|
||||
String token = this.authenticate(user, client);
|
||||
return LoginResp.builder().token(token).build();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void preLogin(AccountLoginReq req, ClientResp client, HttpServletRequest request) {
|
||||
super.preLogin(req, client, request);
|
||||
// 校验验证码
|
||||
int loginCaptchaEnabled = optionService.getValueByCode2Int("LOGIN_CAPTCHA_ENABLED");
|
||||
if (SysConstants.YES.equals(loginCaptchaEnabled)) {
|
||||
ValidationUtils.throwIfBlank(req.getCaptcha(), "验证码不能为空");
|
||||
ValidationUtils.throwIfBlank(req.getUuid(), "验证码标识不能为空");
|
||||
String captchaKey = CacheConstants.CAPTCHA_KEY_PREFIX + req.getUuid();
|
||||
String captcha = RedisUtils.get(captchaKey);
|
||||
ValidationUtils.throwIfBlank(captcha, CAPTCHA_EXPIRED);
|
||||
RedisUtils.delete(captchaKey);
|
||||
ValidationUtils.throwIfNotEqualIgnoreCase(req.getCaptcha(), captcha, CAPTCHA_ERROR);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public AuthTypeEnum getAuthType() {
|
||||
return AuthTypeEnum.ACCOUNT;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检测用户是否已被锁定
|
||||
*
|
||||
* @param username 用户名
|
||||
* @param request 请求对象
|
||||
* @param isError 是否登录错误
|
||||
*/
|
||||
private void checkUserLocked(String username, HttpServletRequest request, boolean isError) {
|
||||
// 不锁定
|
||||
int maxErrorCount = optionService.getValueByCode2Int(PasswordPolicyEnum.PASSWORD_ERROR_LOCK_COUNT.name());
|
||||
if (maxErrorCount <= SysConstants.NO) {
|
||||
return;
|
||||
}
|
||||
// 检测是否已被锁定
|
||||
String key = CacheConstants.USER_PASSWORD_ERROR_KEY_PREFIX + RedisUtils.formatKey(username, JakartaServletUtil
|
||||
.getClientIP(request));
|
||||
int lockMinutes = optionService.getValueByCode2Int(PasswordPolicyEnum.PASSWORD_ERROR_LOCK_MINUTES.name());
|
||||
Integer currentErrorCount = ObjectUtil.defaultIfNull(RedisUtils.get(key), 0);
|
||||
CheckUtils.throwIf(currentErrorCount >= maxErrorCount, PasswordPolicyEnum.PASSWORD_ERROR_LOCK_MINUTES.getMsg()
|
||||
.formatted(lockMinutes));
|
||||
// 登录成功清除计数
|
||||
if (!isError) {
|
||||
RedisUtils.delete(key);
|
||||
return;
|
||||
}
|
||||
// 登录失败递增计数
|
||||
currentErrorCount++;
|
||||
RedisUtils.set(key, currentErrorCount, Duration.ofMinutes(lockMinutes));
|
||||
CheckUtils.throwIf(currentErrorCount >= maxErrorCount, PasswordPolicyEnum.PASSWORD_ERROR_LOCK_COUNT.getMsg()
|
||||
.formatted(maxErrorCount, lockMinutes));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
package top.ysoft.admin.auth.handler;
|
||||
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import org.springframework.stereotype.Component;
|
||||
import top.ysoft.admin.auth.AbstractLoginHandler;
|
||||
import top.ysoft.admin.auth.enums.AuthTypeEnum;
|
||||
import top.ysoft.admin.auth.model.req.EmailLoginReq;
|
||||
import top.ysoft.admin.auth.model.resp.LoginResp;
|
||||
import top.ysoft.admin.common.constant.CacheConstants;
|
||||
import top.ysoft.admin.system.model.entity.UserDO;
|
||||
import top.ysoft.admin.system.model.resp.ClientResp;
|
||||
import top.continew.starter.cache.redisson.util.RedisUtils;
|
||||
import top.continew.starter.core.validation.ValidationUtils;
|
||||
|
||||
/**
|
||||
* 邮箱登录处理器
|
||||
*
|
||||
* @author KAI
|
||||
* @author Charles7c
|
||||
* @since 2024/12/22 14:58
|
||||
*/
|
||||
@Component
|
||||
public class EmailLoginHandler extends AbstractLoginHandler<EmailLoginReq> {
|
||||
|
||||
@Override
|
||||
public LoginResp login(EmailLoginReq req, ClientResp client, HttpServletRequest request) {
|
||||
// 验证邮箱
|
||||
UserDO user = userService.getByEmail(req.getEmail());
|
||||
ValidationUtils.throwIfNull(user, "此邮箱未绑定本系统账号");
|
||||
// 检查用户状态
|
||||
super.checkUserStatus(user);
|
||||
// 执行认证
|
||||
String token = super.authenticate(user, client);
|
||||
return LoginResp.builder().token(token).build();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void preLogin(EmailLoginReq req, ClientResp client, HttpServletRequest request) {
|
||||
String email = req.getEmail();
|
||||
String captchaKey = CacheConstants.CAPTCHA_KEY_PREFIX + email;
|
||||
String captcha = RedisUtils.get(captchaKey);
|
||||
ValidationUtils.throwIfBlank(captcha, CAPTCHA_EXPIRED);
|
||||
ValidationUtils.throwIfNotEqualIgnoreCase(req.getCaptcha(), captcha, CAPTCHA_ERROR);
|
||||
RedisUtils.delete(captchaKey);
|
||||
}
|
||||
|
||||
@Override
|
||||
public AuthTypeEnum getAuthType() {
|
||||
return AuthTypeEnum.EMAIL;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
package top.ysoft.admin.auth.handler;
|
||||
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import org.springframework.stereotype.Component;
|
||||
import top.ysoft.admin.auth.AbstractLoginHandler;
|
||||
import top.ysoft.admin.auth.enums.AuthTypeEnum;
|
||||
import top.ysoft.admin.auth.model.req.PhoneLoginReq;
|
||||
import top.ysoft.admin.auth.model.resp.LoginResp;
|
||||
import top.ysoft.admin.common.constant.CacheConstants;
|
||||
import top.ysoft.admin.system.model.entity.UserDO;
|
||||
import top.ysoft.admin.system.model.resp.ClientResp;
|
||||
import top.continew.starter.cache.redisson.util.RedisUtils;
|
||||
import top.continew.starter.core.validation.ValidationUtils;
|
||||
|
||||
/**
|
||||
* 手机号登录处理器
|
||||
*
|
||||
* @author KAI
|
||||
* @author Charles7c
|
||||
* @since 2024/12/22 14:59
|
||||
*/
|
||||
@Component
|
||||
public class PhoneLoginHandler extends AbstractLoginHandler<PhoneLoginReq> {
|
||||
|
||||
@Override
|
||||
public LoginResp login(PhoneLoginReq req, ClientResp client, HttpServletRequest request) {
|
||||
// 验证手机号
|
||||
UserDO user = userService.getByPhone(req.getPhone());
|
||||
ValidationUtils.throwIfNull(user, "此手机号未绑定本系统账号");
|
||||
// 检查用户状态
|
||||
super.checkUserStatus(user);
|
||||
// 执行认证
|
||||
String token = super.authenticate(user, client);
|
||||
return LoginResp.builder().token(token).build();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void preLogin(PhoneLoginReq req, ClientResp client, HttpServletRequest request) {
|
||||
String phone = req.getPhone();
|
||||
String captchaKey = CacheConstants.CAPTCHA_KEY_PREFIX + phone;
|
||||
String captcha = RedisUtils.get(captchaKey);
|
||||
ValidationUtils.throwIfBlank(captcha, CAPTCHA_EXPIRED);
|
||||
ValidationUtils.throwIfNotEqualIgnoreCase(req.getCaptcha(), captcha, CAPTCHA_ERROR);
|
||||
RedisUtils.delete(captchaKey);
|
||||
}
|
||||
|
||||
@Override
|
||||
public AuthTypeEnum getAuthType() {
|
||||
return AuthTypeEnum.PHONE;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,161 @@
|
||||
package top.ysoft.admin.auth.handler;
|
||||
|
||||
import cn.dev33.satoken.stp.StpUtil;
|
||||
import cn.hutool.core.bean.BeanUtil;
|
||||
import cn.hutool.core.collection.CollUtil;
|
||||
import cn.hutool.core.util.IdUtil;
|
||||
import cn.hutool.core.util.RandomUtil;
|
||||
import cn.hutool.core.util.ReUtil;
|
||||
import cn.hutool.json.JSONUtil;
|
||||
import com.xkcoding.justauth.AuthRequestFactory;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import me.zhyd.oauth.model.AuthCallback;
|
||||
import me.zhyd.oauth.model.AuthResponse;
|
||||
import me.zhyd.oauth.model.AuthUser;
|
||||
import me.zhyd.oauth.request.AuthRequest;
|
||||
import org.springframework.stereotype.Component;
|
||||
import top.ysoft.admin.auth.AbstractLoginHandler;
|
||||
import top.ysoft.admin.auth.enums.AuthTypeEnum;
|
||||
import top.ysoft.admin.auth.model.req.SocialLoginReq;
|
||||
import top.ysoft.admin.auth.model.resp.LoginResp;
|
||||
import top.ysoft.admin.common.constant.RegexConstants;
|
||||
import top.ysoft.admin.common.constant.SysConstants;
|
||||
import top.ysoft.admin.common.enums.DisEnableStatusEnum;
|
||||
import top.ysoft.admin.common.enums.GenderEnum;
|
||||
import top.ysoft.admin.system.enums.MessageTemplateEnum;
|
||||
import top.ysoft.admin.system.enums.MessageTypeEnum;
|
||||
import top.ysoft.admin.system.model.entity.RoleDO;
|
||||
import top.ysoft.admin.system.model.entity.UserDO;
|
||||
import top.ysoft.admin.system.model.entity.UserSocialDO;
|
||||
import top.ysoft.admin.system.model.req.MessageReq;
|
||||
import top.ysoft.admin.system.model.resp.ClientResp;
|
||||
import top.ysoft.admin.system.service.MessageService;
|
||||
import top.ysoft.admin.system.service.UserRoleService;
|
||||
import top.ysoft.admin.system.service.UserSocialService;
|
||||
import top.continew.starter.core.autoconfigure.project.ProjectProperties;
|
||||
import top.continew.starter.core.exception.BadRequestException;
|
||||
import top.continew.starter.core.validation.ValidationUtils;
|
||||
import top.continew.starter.messaging.websocket.util.WebSocketUtils;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 第三方账号登录处理器
|
||||
*
|
||||
* @author KAI
|
||||
* @author Charles7c
|
||||
* @since 2024/12/25 14:21
|
||||
*/
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class SocialLoginHandler extends AbstractLoginHandler<SocialLoginReq> {
|
||||
|
||||
private final AuthRequestFactory authRequestFactory;
|
||||
private final UserSocialService userSocialService;
|
||||
private final UserRoleService userRoleService;
|
||||
private final MessageService messageService;
|
||||
private final ProjectProperties projectProperties;
|
||||
|
||||
@Override
|
||||
public LoginResp login(SocialLoginReq req, ClientResp client, HttpServletRequest request) {
|
||||
// 获取第三方登录信息
|
||||
AuthRequest authRequest = this.getAuthRequest(req.getSource());
|
||||
AuthCallback callback = new AuthCallback();
|
||||
callback.setCode(req.getCode());
|
||||
callback.setState(req.getState());
|
||||
AuthResponse<AuthUser> response = authRequest.login(callback);
|
||||
ValidationUtils.throwIf(!response.ok(), response.getMsg());
|
||||
AuthUser authUser = response.getData();
|
||||
// 如未绑定则自动注册新用户,保存或更新关联信息
|
||||
String source = authUser.getSource();
|
||||
String openId = authUser.getUuid();
|
||||
UserSocialDO userSocial = userSocialService.getBySourceAndOpenId(source, openId);
|
||||
UserDO user;
|
||||
if (null == userSocial) {
|
||||
String username = authUser.getUsername();
|
||||
String nickname = authUser.getNickname();
|
||||
UserDO existsUser = userService.getByUsername(username);
|
||||
String randomStr = RandomUtil.randomString(RandomUtil.BASE_CHAR, 5);
|
||||
if (null != existsUser || !ReUtil.isMatch(RegexConstants.USERNAME, username)) {
|
||||
username = randomStr + IdUtil.fastSimpleUUID();
|
||||
}
|
||||
if (!ReUtil.isMatch(RegexConstants.GENERAL_NAME, nickname)) {
|
||||
nickname = source.toLowerCase() + randomStr;
|
||||
}
|
||||
user = new UserDO();
|
||||
user.setUsername(username);
|
||||
user.setNickname(nickname);
|
||||
user.setGender(GenderEnum.valueOf(authUser.getGender().name()));
|
||||
user.setAvatar(authUser.getAvatar());
|
||||
user.setDeptId(SysConstants.SUPER_DEPT_ID);
|
||||
user.setStatus(DisEnableStatusEnum.ENABLE);
|
||||
userService.save(user);
|
||||
Long userId = user.getId();
|
||||
RoleDO role = roleService.getByCode(SysConstants.SUPER_ROLE_CODE);
|
||||
userRoleService.assignRolesToUser(Collections.singletonList(role.getId()), userId);
|
||||
userSocial = new UserSocialDO();
|
||||
userSocial.setUserId(userId);
|
||||
userSocial.setSource(source);
|
||||
userSocial.setOpenId(openId);
|
||||
this.sendSecurityMsg(user);
|
||||
} else {
|
||||
user = BeanUtil.copyProperties(userService.getById(userSocial.getUserId()), UserDO.class);
|
||||
}
|
||||
// 检查用户状态
|
||||
super.checkUserStatus(user);
|
||||
userSocial.setMetaJson(JSONUtil.toJsonStr(authUser));
|
||||
userSocial.setLastLoginTime(LocalDateTime.now());
|
||||
userSocialService.saveOrUpdate(userSocial);
|
||||
// 执行认证
|
||||
String token = super.authenticate(user, client);
|
||||
return LoginResp.builder().token(token).build();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void preLogin(SocialLoginReq req, ClientResp client, HttpServletRequest request) {
|
||||
super.preLogin(req, client, request);
|
||||
if (StpUtil.isLogin()) {
|
||||
StpUtil.logout();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public AuthTypeEnum getAuthType() {
|
||||
return AuthTypeEnum.SOCIAL;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 AuthRequest
|
||||
*
|
||||
* @param source 平台名称
|
||||
* @return AuthRequest
|
||||
*/
|
||||
private AuthRequest getAuthRequest(String source) {
|
||||
try {
|
||||
return authRequestFactory.get(source);
|
||||
} catch (Exception e) {
|
||||
throw new BadRequestException("暂不支持 [%s] 平台账号登录".formatted(source));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送安全消息
|
||||
*
|
||||
* @param user 用户信息
|
||||
*/
|
||||
private void sendSecurityMsg(UserDO user) {
|
||||
MessageReq req = new MessageReq();
|
||||
MessageTemplateEnum socialRegister = MessageTemplateEnum.SOCIAL_REGISTER;
|
||||
req.setTitle(socialRegister.getTitle().formatted(projectProperties.getName()));
|
||||
req.setContent(socialRegister.getContent().formatted(user.getNickname()));
|
||||
req.setType(MessageTypeEnum.SECURITY);
|
||||
messageService.add(req, CollUtil.toList(user.getId()));
|
||||
List<String> tokenList = StpUtil.getTokenValueListByLoginId(user.getId());
|
||||
for (String token : tokenList) {
|
||||
WebSocketUtils.sendMessage(token, "1");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
package top.ysoft.admin.auth.model.query;
|
||||
|
||||
import cn.hutool.core.date.DatePattern;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
import org.springframework.format.annotation.DateTimeFormat;
|
||||
|
||||
import java.io.Serial;
|
||||
import java.io.Serializable;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 在线用户查询条件
|
||||
*
|
||||
* @author Charles7c
|
||||
* @since 2023/1/20 23:07
|
||||
*/
|
||||
@Data
|
||||
@Schema(description = "在线用户查询条件")
|
||||
public class OnlineUserQuery implements Serializable {
|
||||
|
||||
@Serial
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/**
|
||||
* 用户昵称
|
||||
*/
|
||||
@Schema(description = "用户昵称", example = "张三")
|
||||
private String nickname;
|
||||
|
||||
/**
|
||||
* 终端 ID
|
||||
*/
|
||||
@Schema(description = "终端 ID", example = "ef51c9a3e9046c4f2ea45142c8a8344a")
|
||||
private String clientId;
|
||||
|
||||
/**
|
||||
* 登录时间
|
||||
*/
|
||||
@Schema(description = "登录时间", example = "2023-08-08 00:00:00,2023-08-08 23:59:59")
|
||||
@DateTimeFormat(pattern = DatePattern.NORM_DATETIME_PATTERN)
|
||||
private List<Date> loginTime;
|
||||
|
||||
/**
|
||||
* 用户 ID
|
||||
*/
|
||||
@Schema(hidden = true)
|
||||
private Long userId;
|
||||
|
||||
/**
|
||||
* 角色 ID
|
||||
*/
|
||||
@Schema(hidden = true)
|
||||
private Long roleId;
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
package top.ysoft.admin.auth.model.req;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serial;
|
||||
|
||||
/**
|
||||
* 账号登录参数
|
||||
*
|
||||
* @author Charles7c
|
||||
* @since 2022/12/21 20:43
|
||||
*/
|
||||
@Data
|
||||
@Schema(description = "账号登录参数")
|
||||
public class AccountLoginReq extends LoginReq {
|
||||
|
||||
@Serial
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/**
|
||||
* 用户名
|
||||
*/
|
||||
@Schema(description = "用户名", example = "zhangsan")
|
||||
@NotBlank(message = "用户名不能为空")
|
||||
private String username;
|
||||
|
||||
/**
|
||||
* 密码(加密)
|
||||
*/
|
||||
@Schema(description = "密码(加密)", example = "HHwZoiBwCfh0xLdWOAd0bHOkEZlIMMOQKJyeFUw9T3ArrhL57od2i42s1o0sSXKkeHPJXvQsninhPFH2lArDDQ==")
|
||||
@NotBlank(message = "密码不能为空")
|
||||
private String password;
|
||||
|
||||
/**
|
||||
* 验证码
|
||||
*/
|
||||
@Schema(description = "验证码", example = "ABCD")
|
||||
private String captcha;
|
||||
|
||||
/**
|
||||
* 验证码标识
|
||||
*/
|
||||
@Schema(description = "验证码标识", example = "090b9a2c-1691-4fca-99db-e4ed0cff362f")
|
||||
private String uuid;
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
package top.ysoft.admin.auth.model.req;
|
||||
|
||||
import cn.hutool.core.lang.RegexPool;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.Pattern;
|
||||
import lombok.Data;
|
||||
import org.hibernate.validator.constraints.Length;
|
||||
|
||||
import java.io.Serial;
|
||||
|
||||
/**
|
||||
* 邮箱登录参数
|
||||
*
|
||||
* @author Charles7c
|
||||
* @since 2023/10/23 20:15
|
||||
*/
|
||||
@Data
|
||||
@Schema(description = "邮箱登录参数")
|
||||
public class EmailLoginReq extends LoginReq {
|
||||
|
||||
@Serial
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/**
|
||||
* 邮箱
|
||||
*/
|
||||
@Schema(description = "邮箱", example = "123456789@qq.com")
|
||||
@NotBlank(message = "邮箱不能为空")
|
||||
@Pattern(regexp = RegexPool.EMAIL, message = "邮箱格式错误")
|
||||
private String email;
|
||||
|
||||
/**
|
||||
* 验证码
|
||||
*/
|
||||
@Schema(description = "验证码", example = "888888")
|
||||
@NotBlank(message = "验证码不能为空")
|
||||
@Length(max = 6, message = "验证码非法")
|
||||
private String captcha;
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
package top.ysoft.admin.auth.model.req;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonSubTypes;
|
||||
import com.fasterxml.jackson.annotation.JsonTypeInfo;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import lombok.Data;
|
||||
import top.ysoft.admin.auth.enums.AuthTypeEnum;
|
||||
|
||||
import java.io.Serial;
|
||||
import java.io.Serializable;
|
||||
|
||||
/**
|
||||
* 登录参数基类
|
||||
*
|
||||
* @author KAI
|
||||
* @author Charles7c
|
||||
* @since 2024/12/22 15:16
|
||||
*/
|
||||
@Data
|
||||
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.EXISTING_PROPERTY, property = "authType", visible = true)
|
||||
@JsonSubTypes({@JsonSubTypes.Type(value = AccountLoginReq.class, name = "ACCOUNT"),
|
||||
@JsonSubTypes.Type(value = EmailLoginReq.class, name = "EMAIL"),
|
||||
@JsonSubTypes.Type(value = PhoneLoginReq.class, name = "PHONE"),
|
||||
@JsonSubTypes.Type(value = SocialLoginReq.class, name = "SOCIAL")})
|
||||
@Schema(description = "基础登录参数")
|
||||
public class LoginReq implements Serializable {
|
||||
|
||||
@Serial
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/**
|
||||
* 终端 ID
|
||||
*/
|
||||
@Schema(description = "终端 ID", example = "ef51c9a3e9046c4f2ea45142c8a8344a")
|
||||
@NotBlank(message = "终端ID不能为空")
|
||||
private String clientId;
|
||||
|
||||
/**
|
||||
* 认证类型
|
||||
*/
|
||||
@Schema(description = "认证类型", example = "ACCOUNT")
|
||||
@NotNull(message = "认证类型非法")
|
||||
private AuthTypeEnum authType;
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
package top.ysoft.admin.auth.model.req;
|
||||
|
||||
import cn.hutool.core.lang.RegexPool;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.Pattern;
|
||||
import lombok.Data;
|
||||
import org.hibernate.validator.constraints.Length;
|
||||
|
||||
import java.io.Serial;
|
||||
|
||||
/**
|
||||
* 手机号登录参数
|
||||
*
|
||||
* @author Charles7c
|
||||
* @since 2023/10/26 22:37
|
||||
*/
|
||||
@Data
|
||||
@Schema(description = "手机号登录参数")
|
||||
public class PhoneLoginReq extends LoginReq {
|
||||
|
||||
@Serial
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/**
|
||||
* 手机号
|
||||
*/
|
||||
@Schema(description = "手机号", example = "13811111111")
|
||||
@NotBlank(message = "手机号不能为空")
|
||||
@Pattern(regexp = RegexPool.MOBILE, message = "手机号格式错误")
|
||||
private String phone;
|
||||
|
||||
/**
|
||||
* 验证码
|
||||
*/
|
||||
@Schema(description = "验证码", example = "8888")
|
||||
@NotBlank(message = "验证码不能为空")
|
||||
@Length(max = 4, message = "验证码非法")
|
||||
private String captcha;
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
package top.ysoft.admin.auth.model.req;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serial;
|
||||
|
||||
/**
|
||||
* 第三方账号登录参数
|
||||
*
|
||||
* @author KAI
|
||||
* @author Charles7c
|
||||
* @since 2024/12/25 15:43
|
||||
*/
|
||||
@Data
|
||||
@Schema(description = "第三方账号登录参数")
|
||||
public class SocialLoginReq extends LoginReq {
|
||||
|
||||
@Serial
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/**
|
||||
* 第三方登录平台
|
||||
*/
|
||||
@Schema(description = "第三方登录平台", example = "gitee")
|
||||
@NotBlank(message = "第三方登录平台不能为空")
|
||||
private String source;
|
||||
|
||||
/**
|
||||
* 授权码
|
||||
*/
|
||||
@Schema(description = "授权码", example = "a08d33e9e577fb339de027499784ed4e871d6f62ae65b459153e906ab546bd56")
|
||||
@NotBlank(message = "授权码不能为空")
|
||||
private String code;
|
||||
|
||||
/**
|
||||
* 状态码
|
||||
*/
|
||||
@Schema(description = "状态码", example = "2ca8d8baf437eb374efaa1191a3d")
|
||||
@NotBlank(message = "状态码不能为空")
|
||||
private String state;
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
package top.ysoft.admin.auth.model.resp;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serial;
|
||||
import java.io.Serializable;
|
||||
|
||||
/**
|
||||
* 验证码信息
|
||||
*
|
||||
* @author Charles7c
|
||||
* @since 2022/12/11 13:55
|
||||
*/
|
||||
@Data
|
||||
@Builder
|
||||
@Schema(description = "验证码信息")
|
||||
public class CaptchaResp implements Serializable {
|
||||
|
||||
@Serial
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/**
|
||||
* 验证码标识
|
||||
*/
|
||||
@Schema(description = "验证码标识", example = "090b9a2c-1691-4fca-99db-e4ed0cff362f")
|
||||
private String uuid;
|
||||
|
||||
/**
|
||||
* 验证码图片(Base64编码,带图片格式:data:image/gif;base64)
|
||||
*/
|
||||
@Schema(description = "验证码图片(Base64编码,带图片格式:data:image/gif;base64)", example = "data:image/png;base64,iVBORw0KGgoAAAAN...")
|
||||
private String img;
|
||||
|
||||
/**
|
||||
* 过期时间戳
|
||||
*/
|
||||
@Schema(description = "过期时间戳", example = "1714376969409")
|
||||
private Long expireTime;
|
||||
|
||||
/**
|
||||
* 是否启用
|
||||
*/
|
||||
@Schema(description = "是否启用", example = "true")
|
||||
private Boolean isEnabled;
|
||||
|
||||
/**
|
||||
* 构建验证码信息
|
||||
*
|
||||
* @param uuid 验证码标识
|
||||
* @param img 验证码图片(Base64编码,带图片格式:data:image/gif;base64)
|
||||
* @param expireTime 过期时间戳
|
||||
* @return 验证码信息
|
||||
*/
|
||||
public static CaptchaResp of(String uuid, String img, Long expireTime) {
|
||||
return CaptchaResp.builder().uuid(uuid).img(img).expireTime(expireTime).isEnabled(true).build();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package top.ysoft.admin.auth.model.resp;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serial;
|
||||
import java.io.Serializable;
|
||||
|
||||
/**
|
||||
* 登录响应参数
|
||||
*
|
||||
* @author Charles7c
|
||||
* @since 2022/12/21 20:42
|
||||
*/
|
||||
@Data
|
||||
@Builder
|
||||
@Schema(description = "登录响应参数")
|
||||
public class LoginResp implements Serializable {
|
||||
|
||||
@Serial
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/**
|
||||
* 令牌
|
||||
*/
|
||||
@Schema(description = "令牌", example = "eyJ0eXAiOiJlV1QiLCJhbGciqiJIUzI1NiJ9.eyJsb2dpblR5cGUiOiJsb29pbiIsImxvZ2luSWQiOjEsInJuU3RyIjoiSjd4SUljYnU5cmNwU09vQ3Uyc1ND1BYYTYycFRjcjAifQ.KUPOYm-2wfuLUSfEEAbpGE527fzmkAJG7sMNcQ0pUZ8")
|
||||
private String token;
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
package top.ysoft.admin.auth.model.resp;
|
||||
|
||||
import cn.crane4j.annotation.Assemble;
|
||||
import cn.crane4j.annotation.AssembleMethod;
|
||||
import cn.crane4j.annotation.ContainerMethod;
|
||||
import cn.crane4j.annotation.MappingType;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
import top.ysoft.admin.auth.service.OnlineUserService;
|
||||
import top.ysoft.admin.common.constant.ContainerConstants;
|
||||
|
||||
import java.io.Serial;
|
||||
import java.io.Serializable;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* 在线用户信息
|
||||
*
|
||||
* @author Charles7c
|
||||
* @since 2023/1/20 21:54
|
||||
*/
|
||||
@Data
|
||||
@Schema(description = "在线用户信息")
|
||||
public class OnlineUserResp implements Serializable {
|
||||
|
||||
@Serial
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/**
|
||||
* ID
|
||||
*/
|
||||
@Schema(description = "ID", example = "1")
|
||||
@Assemble(prop = ":nickname", container = ContainerConstants.USER_NICKNAME)
|
||||
private Long id;
|
||||
|
||||
/**
|
||||
* 令牌
|
||||
*/
|
||||
@Schema(description = "令牌", example = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJsb2dpblR5cGUiOiJsb2dpbiIsImxvZ2luSWQiOjEsInJuU3RyIjoiTUd6djdyOVFoeHEwdVFqdFAzV3M5YjVJRzh4YjZPSEUifQ.7q7U3ouoN7WPhH2kUEM7vPe5KF3G_qavSG-vRgIxKvE")
|
||||
@AssembleMethod(prop = ":lastActiveTime", targetType = OnlineUserService.class, method = @ContainerMethod(bindMethod = "getLastActiveTime", type = MappingType.ORDER_OF_KEYS))
|
||||
private String token;
|
||||
|
||||
/**
|
||||
* 用户名
|
||||
*/
|
||||
@Schema(description = "用户名", example = "zhangsan")
|
||||
private String username;
|
||||
|
||||
/**
|
||||
* 昵称
|
||||
*/
|
||||
@Schema(description = "昵称", example = "张三")
|
||||
private String nickname;
|
||||
|
||||
/**
|
||||
* 终端类型
|
||||
*/
|
||||
@Schema(description = "终端类型", example = "PC")
|
||||
private String clientType;
|
||||
|
||||
/**
|
||||
* 终端 ID
|
||||
*/
|
||||
@Schema(description = "终端 ID", example = "ef51c9a3e9046c4f2ea45142c8a8344a")
|
||||
private String clientId;
|
||||
|
||||
/**
|
||||
* 登录 IP
|
||||
*/
|
||||
@Schema(description = "登录 IP", example = "")
|
||||
private String ip;
|
||||
|
||||
/**
|
||||
* 登录地点
|
||||
*/
|
||||
@Schema(description = "登录地点", example = "中国北京北京市")
|
||||
private String address;
|
||||
|
||||
/**
|
||||
* 浏览器
|
||||
*/
|
||||
@Schema(description = "浏览器", example = "Chrome 115.0.0.0")
|
||||
private String browser;
|
||||
|
||||
/**
|
||||
* 操作系统
|
||||
*/
|
||||
@Schema(description = "操作系统", example = "Windows 10")
|
||||
private String os;
|
||||
|
||||
/**
|
||||
* 登录时间
|
||||
*/
|
||||
@Schema(description = "登录时间", example = "2023-08-08 08:08:08", type = "string")
|
||||
private LocalDateTime loginTime;
|
||||
|
||||
/**
|
||||
* 最后活跃时间
|
||||
*/
|
||||
@Schema(description = "最后活跃时间", example = "2023-08-08 08:08:08", type = "string")
|
||||
private LocalDateTime lastActiveTime;
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
package top.ysoft.admin.auth.model.resp;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serial;
|
||||
import java.io.Serializable;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 路由信息
|
||||
*
|
||||
* @author Charles7c
|
||||
* @since 2023/2/26 22:51
|
||||
*/
|
||||
@Data
|
||||
@Schema(description = "路由信息")
|
||||
@JsonInclude(JsonInclude.Include.NON_EMPTY)
|
||||
public class RouteResp implements Serializable {
|
||||
|
||||
@Serial
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/**
|
||||
* ID
|
||||
*/
|
||||
@Schema(description = "ID", example = "1010")
|
||||
private Long id;
|
||||
|
||||
/**
|
||||
* 上级菜单 ID
|
||||
*/
|
||||
@Schema(description = "上级菜单ID", example = "1000")
|
||||
private Long parentId;
|
||||
|
||||
/**
|
||||
* 标题
|
||||
*/
|
||||
@Schema(description = "标题", example = "用户管理")
|
||||
private String title;
|
||||
|
||||
/**
|
||||
* 类型
|
||||
*/
|
||||
@Schema(description = "类型", example = "2")
|
||||
private Integer type;
|
||||
|
||||
/**
|
||||
* 路由地址
|
||||
*/
|
||||
@Schema(description = "路由地址", example = "/system/user")
|
||||
private String path;
|
||||
|
||||
/**
|
||||
* 组件名称
|
||||
*/
|
||||
@Schema(description = "组件名称", example = "User")
|
||||
private String name;
|
||||
|
||||
/**
|
||||
* 组件路径
|
||||
*/
|
||||
@Schema(description = "组件路径", example = "/system/user/index")
|
||||
private String component;
|
||||
|
||||
/**
|
||||
* 重定向地址
|
||||
*/
|
||||
@Schema(description = "重定向地址")
|
||||
private String redirect;
|
||||
|
||||
/**
|
||||
* 图标
|
||||
*/
|
||||
@Schema(description = "图标", example = "user")
|
||||
private String icon;
|
||||
|
||||
/**
|
||||
* 是否外链
|
||||
*/
|
||||
@Schema(description = "是否外链", example = "false")
|
||||
private Boolean isExternal;
|
||||
|
||||
/**
|
||||
* 是否缓存
|
||||
*/
|
||||
@Schema(description = "是否缓存", example = "false")
|
||||
private Boolean isCache;
|
||||
|
||||
/**
|
||||
* 是否隐藏
|
||||
*/
|
||||
@Schema(description = "是否隐藏", example = "false")
|
||||
private Boolean isHidden;
|
||||
|
||||
/**
|
||||
* 权限标识
|
||||
*/
|
||||
@Schema(description = "权限标识", example = "system:user:list")
|
||||
private String permission;
|
||||
|
||||
/**
|
||||
* 排序
|
||||
*/
|
||||
@Schema(description = "排序", example = "1")
|
||||
private Integer sort;
|
||||
|
||||
/**
|
||||
* 子路由列表
|
||||
*/
|
||||
@Schema(description = "子路由列表")
|
||||
private List<RouteResp> children;
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package top.ysoft.admin.auth.model.resp;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serial;
|
||||
import java.io.Serializable;
|
||||
|
||||
/**
|
||||
* 三方账号授权认证响应信息
|
||||
*
|
||||
* @author Charles7c
|
||||
* @since 2024/3/6 22:26
|
||||
*/
|
||||
@Data
|
||||
@Builder
|
||||
@Schema(description = "三方账号授权认证响应信息")
|
||||
public class SocialAuthAuthorizeResp implements Serializable {
|
||||
|
||||
@Serial
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/**
|
||||
* 授权 URL
|
||||
*/
|
||||
@Schema(description = "授权 URL", example = "https://gitee.com/oauth/authorize?response_type=code&client_id=5d271b7f638941812aaf8bfc2e2f08f06d6235ef934e0e39537e2364eb8452c4&redirect_uri=http://localhost:6609/social/callback?source=gitee&state=d4ea7129e2531050210e9c918cc007d7&scope=user_info")
|
||||
private String authorizeUrl;
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
package top.ysoft.admin.auth.model.resp;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
import top.ysoft.admin.common.enums.GenderEnum;
|
||||
import top.continew.starter.security.mask.annotation.JsonMask;
|
||||
import top.continew.starter.security.mask.enums.MaskType;
|
||||
|
||||
import java.io.Serial;
|
||||
import java.io.Serializable;
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* 用户信息
|
||||
*
|
||||
* @author Charles7c
|
||||
* @since 2022/12/29 20:15
|
||||
*/
|
||||
@Data
|
||||
@Schema(description = "用户信息")
|
||||
public class UserInfoResp implements Serializable {
|
||||
|
||||
@Serial
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/**
|
||||
* ID
|
||||
*/
|
||||
@Schema(description = "ID", example = "1")
|
||||
private Long id;
|
||||
|
||||
/**
|
||||
* 用户名
|
||||
*/
|
||||
@Schema(description = "用户名", example = "zhangsan")
|
||||
private String username;
|
||||
|
||||
/**
|
||||
* 昵称
|
||||
*/
|
||||
@Schema(description = "昵称", example = "张三")
|
||||
private String nickname;
|
||||
|
||||
/**
|
||||
* 性别
|
||||
*/
|
||||
@Schema(description = "性别", example = "1")
|
||||
private GenderEnum gender;
|
||||
|
||||
/**
|
||||
* 邮箱
|
||||
*/
|
||||
@Schema(description = "邮箱", example = "c*******@126.com")
|
||||
@JsonMask(MaskType.EMAIL)
|
||||
private String email;
|
||||
|
||||
/**
|
||||
* 手机号码
|
||||
*/
|
||||
@Schema(description = "手机号码", example = "188****8888")
|
||||
@JsonMask(MaskType.MOBILE_PHONE)
|
||||
private String phone;
|
||||
|
||||
/**
|
||||
* 头像地址
|
||||
*/
|
||||
@Schema(description = "头像地址", example = "https://himg.bdimg.com/sys/portrait/item/public.1.81ac9a9e.rf1ix17UfughLQjNo7XQ_w.jpg")
|
||||
private String avatar;
|
||||
|
||||
/**
|
||||
* 描述
|
||||
*/
|
||||
@Schema(description = "描述", example = "张三描述信息")
|
||||
private String description;
|
||||
|
||||
/**
|
||||
* 最后一次修改密码时间
|
||||
*/
|
||||
@Schema(description = "最后一次修改密码时间", example = "2023-08-08 08:08:08", type = "string")
|
||||
private LocalDateTime pwdResetTime;
|
||||
|
||||
/**
|
||||
* 密码是否已过期
|
||||
*/
|
||||
@Schema(description = "密码是否已过期", example = "true")
|
||||
private Boolean pwdExpired;
|
||||
|
||||
/**
|
||||
* 创建时间
|
||||
*/
|
||||
@JsonIgnore
|
||||
private LocalDateTime createTime;
|
||||
|
||||
/**
|
||||
* 注册日期
|
||||
*/
|
||||
@Schema(description = "注册日期", example = "2023-08-08")
|
||||
private LocalDate registrationDate;
|
||||
|
||||
/**
|
||||
* 部门 ID
|
||||
*/
|
||||
@Schema(description = "部门 ID", example = "1")
|
||||
private Long deptId;
|
||||
|
||||
/**
|
||||
* 所属部门
|
||||
*/
|
||||
@Schema(description = "所属部门", example = "测试部")
|
||||
private String deptName;
|
||||
|
||||
/**
|
||||
* 权限码集合
|
||||
*/
|
||||
@Schema(description = "权限码集合", example = "[\"system:user:list\",\"system:user:add\"]")
|
||||
private Set<String> permissions;
|
||||
|
||||
/**
|
||||
* 角色编码集合
|
||||
*/
|
||||
@Schema(description = "角色编码集合", example = "[\"test\"]")
|
||||
private Set<String> roles;
|
||||
|
||||
public LocalDate getRegistrationDate() {
|
||||
return createTime.toLocalDate();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
package top.ysoft.admin.auth.service;
|
||||
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import top.ysoft.admin.auth.model.req.LoginReq;
|
||||
import top.ysoft.admin.auth.model.resp.LoginResp;
|
||||
import top.ysoft.admin.auth.model.resp.RouteResp;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 认证业务接口
|
||||
*
|
||||
* @author Charles7c
|
||||
* @since 2022/12/21 21:48
|
||||
*/
|
||||
public interface AuthService {
|
||||
|
||||
/**
|
||||
* 登录
|
||||
*
|
||||
* @param req 登录请求参数
|
||||
* @param request 请求对象
|
||||
* @return 登录响应参数
|
||||
*/
|
||||
LoginResp login(LoginReq req, HttpServletRequest request);
|
||||
|
||||
/**
|
||||
* 构建路由树
|
||||
*
|
||||
* @param userId 用户 ID
|
||||
* @return 路由树
|
||||
*/
|
||||
List<RouteResp> buildRouteTree(Long userId);
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
package top.ysoft.admin.auth.service;
|
||||
|
||||
import top.ysoft.admin.auth.model.query.OnlineUserQuery;
|
||||
import top.ysoft.admin.auth.model.resp.OnlineUserResp;
|
||||
import top.continew.starter.extension.crud.model.query.PageQuery;
|
||||
import top.continew.starter.extension.crud.model.resp.PageResp;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 在线用户业务接口
|
||||
*
|
||||
* @author Charles7c
|
||||
* @since 2023/3/25 22:48
|
||||
*/
|
||||
public interface OnlineUserService {
|
||||
|
||||
/**
|
||||
* 分页查询列表
|
||||
*
|
||||
* @param query 查询条件
|
||||
* @param pageQuery 分页查询条件
|
||||
* @return 分页列表信息
|
||||
*/
|
||||
PageResp<OnlineUserResp> page(OnlineUserQuery query, PageQuery pageQuery);
|
||||
|
||||
/**
|
||||
* 查询列表
|
||||
*
|
||||
* @param query 查询条件
|
||||
* @return 列表信息
|
||||
*/
|
||||
List<OnlineUserResp> list(OnlineUserQuery query);
|
||||
|
||||
/**
|
||||
* 查询 Token 最后活跃时间
|
||||
*
|
||||
* @param token Token
|
||||
* @return 最后活跃时间
|
||||
*/
|
||||
LocalDateTime getLastActiveTime(String token);
|
||||
|
||||
/**
|
||||
* 踢出用户
|
||||
*
|
||||
* @param userId 用户 ID
|
||||
*/
|
||||
void kickOut(Long userId);
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
package top.ysoft.admin.auth.service.impl;
|
||||
|
||||
import cn.hutool.core.bean.BeanUtil;
|
||||
import cn.hutool.core.collection.CollUtil;
|
||||
import cn.hutool.core.lang.tree.Tree;
|
||||
import cn.hutool.core.lang.tree.TreeNodeConfig;
|
||||
import cn.hutool.core.lang.tree.TreeUtil;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.stereotype.Service;
|
||||
import top.ysoft.admin.auth.LoginHandler;
|
||||
import top.ysoft.admin.auth.LoginHandlerFactory;
|
||||
import top.ysoft.admin.auth.enums.AuthTypeEnum;
|
||||
import top.ysoft.admin.auth.model.req.LoginReq;
|
||||
import top.ysoft.admin.auth.model.resp.LoginResp;
|
||||
import top.ysoft.admin.auth.model.resp.RouteResp;
|
||||
import top.ysoft.admin.auth.service.AuthService;
|
||||
import top.ysoft.admin.common.constant.SysConstants;
|
||||
import top.ysoft.admin.common.context.RoleContext;
|
||||
import top.ysoft.admin.common.enums.DisEnableStatusEnum;
|
||||
import top.ysoft.admin.system.enums.MenuTypeEnum;
|
||||
import top.ysoft.admin.system.model.resp.ClientResp;
|
||||
import top.ysoft.admin.system.model.resp.MenuResp;
|
||||
import top.ysoft.admin.system.service.ClientService;
|
||||
import top.ysoft.admin.system.service.MenuService;
|
||||
import top.ysoft.admin.system.service.RoleService;
|
||||
import top.continew.starter.core.validation.ValidationUtils;
|
||||
import top.continew.starter.extension.crud.annotation.TreeField;
|
||||
import top.continew.starter.extension.crud.autoconfigure.CrudProperties;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* 认证业务实现
|
||||
*
|
||||
* @author Charles7c
|
||||
* @since 2022/12/21 21:49
|
||||
*/
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class AuthServiceImpl implements AuthService {
|
||||
|
||||
private final LoginHandlerFactory loginHandlerFactory;
|
||||
private final ClientService clientService;
|
||||
private final RoleService roleService;
|
||||
private final MenuService menuService;
|
||||
private final CrudProperties crudProperties;
|
||||
|
||||
@Override
|
||||
public LoginResp login(LoginReq req, HttpServletRequest request) {
|
||||
AuthTypeEnum authType = req.getAuthType();
|
||||
// 校验终端
|
||||
ClientResp client = clientService.getByClientId(req.getClientId());
|
||||
ValidationUtils.throwIfNull(client, "终端不存在");
|
||||
ValidationUtils.throwIf(DisEnableStatusEnum.DISABLE.equals(client.getStatus()), "终端已禁用");
|
||||
ValidationUtils.throwIf(!client.getAuthType().contains(authType.getValue()), "该终端暂未授权 [{}] 认证", authType
|
||||
.getDescription());
|
||||
// 获取处理器
|
||||
LoginHandler<LoginReq> loginHandler = loginHandlerFactory.getHandler(authType);
|
||||
// 登录前置处理
|
||||
loginHandler.preLogin(req, client, request);
|
||||
// 登录
|
||||
LoginResp loginResp = loginHandler.login(req, client, request);
|
||||
// 登录后置处理
|
||||
loginHandler.postLogin(req, client, request);
|
||||
return loginResp;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<RouteResp> buildRouteTree(Long userId) {
|
||||
Set<RoleContext> roleSet = roleService.listByUserId(userId);
|
||||
if (CollUtil.isEmpty(roleSet)) {
|
||||
return new ArrayList<>(0);
|
||||
}
|
||||
// 查询菜单列表
|
||||
Set<MenuResp> menuSet = new LinkedHashSet<>();
|
||||
if (roleSet.stream().anyMatch(r -> SysConstants.SUPER_ROLE_ID.equals(r.getId()))) {
|
||||
menuSet.addAll(menuService.listByRoleId(SysConstants.SUPER_ROLE_ID));
|
||||
} else {
|
||||
roleSet.forEach(r -> menuSet.addAll(menuService.listByRoleId(r.getId())));
|
||||
}
|
||||
List<MenuResp> menuList = menuSet.stream().filter(m -> !MenuTypeEnum.BUTTON.equals(m.getType())).toList();
|
||||
if (CollUtil.isEmpty(menuList)) {
|
||||
return new ArrayList<>(0);
|
||||
}
|
||||
// 构建路由树
|
||||
TreeField treeField = MenuResp.class.getDeclaredAnnotation(TreeField.class);
|
||||
TreeNodeConfig treeNodeConfig = crudProperties.getTree().genTreeNodeConfig(treeField);
|
||||
List<Tree<Long>> treeList = TreeUtil.build(menuList, treeField.rootId(), treeNodeConfig, (m, tree) -> {
|
||||
tree.setId(m.getId());
|
||||
tree.setParentId(m.getParentId());
|
||||
tree.setName(m.getTitle());
|
||||
tree.setWeight(m.getSort());
|
||||
tree.putExtra("type", m.getType().getValue());
|
||||
tree.putExtra("path", m.getPath());
|
||||
tree.putExtra("name", m.getName());
|
||||
tree.putExtra("component", m.getComponent());
|
||||
tree.putExtra("redirect", m.getRedirect());
|
||||
tree.putExtra("icon", m.getIcon());
|
||||
tree.putExtra("isExternal", m.getIsExternal());
|
||||
tree.putExtra("isCache", m.getIsCache());
|
||||
tree.putExtra("isHidden", m.getIsHidden());
|
||||
tree.putExtra("permission", m.getPermission());
|
||||
});
|
||||
return BeanUtil.copyToList(treeList, RouteResp.class);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,137 @@
|
||||
package top.ysoft.admin.auth.service.impl;
|
||||
|
||||
import cn.crane4j.annotation.AutoOperate;
|
||||
import cn.dev33.satoken.dao.SaTokenDao;
|
||||
import cn.dev33.satoken.stp.StpUtil;
|
||||
import cn.hutool.core.bean.BeanUtil;
|
||||
import cn.hutool.core.collection.CollUtil;
|
||||
import cn.hutool.core.convert.Convert;
|
||||
import cn.hutool.core.date.DateUtil;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.stereotype.Service;
|
||||
import top.ysoft.admin.auth.model.query.OnlineUserQuery;
|
||||
import top.ysoft.admin.auth.model.resp.OnlineUserResp;
|
||||
import top.ysoft.admin.auth.service.OnlineUserService;
|
||||
import top.ysoft.admin.common.context.UserContext;
|
||||
import top.ysoft.admin.common.context.UserContextHolder;
|
||||
import top.ysoft.admin.common.context.UserExtraContext;
|
||||
import top.continew.starter.core.constant.StringConstants;
|
||||
import top.continew.starter.extension.crud.model.query.PageQuery;
|
||||
import top.continew.starter.extension.crud.model.resp.PageResp;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* 在线用户业务实现
|
||||
*
|
||||
* @author Charles7c
|
||||
* @since 2023/3/25 22:49
|
||||
*/
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class OnlineUserServiceImpl implements OnlineUserService {
|
||||
|
||||
@Override
|
||||
@AutoOperate(type = OnlineUserResp.class, on = "list")
|
||||
public PageResp<OnlineUserResp> page(OnlineUserQuery query, PageQuery pageQuery) {
|
||||
List<OnlineUserResp> list = this.list(query);
|
||||
return PageResp.build(pageQuery.getPage(), pageQuery.getSize(), list);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<OnlineUserResp> list(OnlineUserQuery query) {
|
||||
List<OnlineUserResp> list = new ArrayList<>();
|
||||
// 查询所有在线 Token
|
||||
List<String> tokenKeyList = StpUtil.searchTokenValue(StringConstants.EMPTY, 0, -1, false);
|
||||
Map<Long, List<String>> tokenMap = tokenKeyList.stream().filter(tokenKey -> {
|
||||
String token = StrUtil.subAfter(tokenKey, StringConstants.COLON, true);
|
||||
// 忽略已过期或失效 Token
|
||||
return StpUtil.getStpLogic().getTokenActiveTimeoutByToken(token) >= SaTokenDao.NEVER_EXPIRE;
|
||||
})
|
||||
.map(tokenKey -> StrUtil.subAfter(tokenKey, StringConstants.COLON, true))
|
||||
.collect(Collectors.groupingBy(token -> Convert.toLong(StpUtil.getLoginIdByToken(token))));
|
||||
// 筛选数据
|
||||
for (Map.Entry<Long, List<String>> entry : tokenMap.entrySet()) {
|
||||
Long userId = entry.getKey();
|
||||
UserContext userContext = UserContextHolder.getContext(userId);
|
||||
if (null == userContext || !this.isMatchNickname(query.getNickname(), userContext) || !this
|
||||
.isMatchClientId(query.getClientId(), userContext)) {
|
||||
continue;
|
||||
}
|
||||
List<Date> loginTimeList = query.getLoginTime();
|
||||
entry.getValue().parallelStream().forEach(token -> {
|
||||
UserExtraContext extraContext = UserContextHolder.getExtraContext(token);
|
||||
if (!this.isMatchLoginTime(loginTimeList, extraContext.getLoginTime())) {
|
||||
return;
|
||||
}
|
||||
OnlineUserResp resp = BeanUtil.copyProperties(userContext, OnlineUserResp.class);
|
||||
BeanUtil.copyProperties(extraContext, resp);
|
||||
resp.setToken(token);
|
||||
list.add(resp);
|
||||
});
|
||||
}
|
||||
// 设置排序
|
||||
CollUtil.sort(list, Comparator.comparing(OnlineUserResp::getLoginTime).reversed());
|
||||
return list;
|
||||
}
|
||||
|
||||
@Override
|
||||
public LocalDateTime getLastActiveTime(String token) {
|
||||
long lastActiveTime = StpUtil.getStpLogic().getTokenLastActiveTime(token);
|
||||
return lastActiveTime == SaTokenDao.NOT_VALUE_EXPIRE ? null : DateUtil.date(lastActiveTime).toLocalDateTime();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void kickOut(Long userId) {
|
||||
if (!StpUtil.isLogin(userId)) {
|
||||
return;
|
||||
}
|
||||
StpUtil.logout(userId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否匹配昵称
|
||||
*
|
||||
* @param nickname 昵称
|
||||
* @param userContext 用户上下文信息
|
||||
* @return 是否匹配昵称
|
||||
*/
|
||||
private boolean isMatchNickname(String nickname, UserContext userContext) {
|
||||
if (StrUtil.isBlank(nickname)) {
|
||||
return true;
|
||||
}
|
||||
return StrUtil.contains(userContext.getUsername(), nickname) || StrUtil.contains(UserContextHolder
|
||||
.getNickname(userContext.getId()), nickname);
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否匹配终端 ID
|
||||
*
|
||||
* @param clientId 终端 ID
|
||||
* @param userContext 用户上下文信息
|
||||
* @return 是否匹配终端 ID
|
||||
*/
|
||||
private boolean isMatchClientId(String clientId, UserContext userContext) {
|
||||
if (StrUtil.isBlank(clientId)) {
|
||||
return true;
|
||||
}
|
||||
return Objects.equals(userContext.getClientId(), clientId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否匹配登录时间
|
||||
*
|
||||
* @param loginTimeList 登录时间列表
|
||||
* @param loginTime 登录时间
|
||||
* @return 是否匹配登录时间
|
||||
*/
|
||||
private boolean isMatchLoginTime(List<Date> loginTimeList, LocalDateTime loginTime) {
|
||||
if (CollUtil.isEmpty(loginTimeList)) {
|
||||
return true;
|
||||
}
|
||||
return DateUtil.isIn(DateUtil.date(loginTime).toJdkDate(), loginTimeList.get(0), loginTimeList.get(1));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user