mirror of https://gitee.com/y_project/RuoYi.git
1,增加异步管理和工厂两类。
2,修改常规操作为异步同步到数据库。 3,修改登陆日志为异步同步到数据库。 4,修改session为异步同步到数据库。 fix issue:登陆/操作等操作响应时间长。pull/23/MERGE
parent
fd75ee49d6
commit
f9c82e1f14
|
@ -2,11 +2,13 @@ package com.ruoyi.common.utils;
|
||||||
|
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
import com.ruoyi.common.constant.Constants;
|
import com.ruoyi.common.constant.Constants;
|
||||||
import com.ruoyi.common.utils.security.ShiroUtils;
|
import com.ruoyi.common.utils.security.ShiroUtils;
|
||||||
import com.ruoyi.common.utils.spring.SpringUtils;
|
import com.ruoyi.common.utils.spring.SpringUtils;
|
||||||
import com.ruoyi.project.monitor.logininfor.domain.Logininfor;
|
import com.ruoyi.project.monitor.logininfor.domain.Logininfor;
|
||||||
import com.ruoyi.project.monitor.logininfor.service.LogininforServiceImpl;
|
import com.ruoyi.project.monitor.logininfor.service.LogininforServiceImpl;
|
||||||
|
|
||||||
import eu.bitwalker.useragentutils.UserAgent;
|
import eu.bitwalker.useragentutils.UserAgent;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -14,58 +16,54 @@ import eu.bitwalker.useragentutils.UserAgent;
|
||||||
*
|
*
|
||||||
* @author ruoyi
|
* @author ruoyi
|
||||||
*/
|
*/
|
||||||
public class SystemLogUtils
|
@Deprecated // 加入异步功能之后,该类已无意义
|
||||||
{
|
public class SystemLogUtils {
|
||||||
|
|
||||||
private static final Logger sys_user_logger = LoggerFactory.getLogger("sys-user");
|
private static final Logger sys_user_logger = LoggerFactory.getLogger("sys-user");
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 记录格式 [ip][用户名][操作][错误消息]
|
* 记录格式 [ip][用户名][操作][错误消息]
|
||||||
* <p/>
|
* <p/>
|
||||||
* 注意操作如下: loginError 登录失败 loginSuccess 登录成功 passwordError 密码错误 changePassword 修改密码 changeStatus 修改状态
|
* 注意操作如下: loginError 登录失败 loginSuccess 登录成功 passwordError 密码错误
|
||||||
*
|
* changePassword 修改密码 changeStatus 修改状态
|
||||||
* @param username
|
*
|
||||||
* @param op
|
* @param username
|
||||||
* @param msg
|
* @param op
|
||||||
* @param args
|
* @param msg
|
||||||
*/
|
* @param args
|
||||||
public static void log(String username, String status, String msg, Object... args)
|
*/
|
||||||
{
|
public static void log(String username, String status, String msg, Object... args) {
|
||||||
StringBuilder s = new StringBuilder();
|
StringBuilder s = new StringBuilder();
|
||||||
s.append(LogUtils.getBlock(ShiroUtils.getIp()));
|
s.append(LogUtils.getBlock(ShiroUtils.getIp()));
|
||||||
s.append(AddressUtils.getRealAddressByIP(ShiroUtils.getIp()));
|
s.append(AddressUtils.getRealAddressByIP(ShiroUtils.getIp()));
|
||||||
s.append(LogUtils.getBlock(username));
|
s.append(LogUtils.getBlock(username));
|
||||||
s.append(LogUtils.getBlock(status));
|
s.append(LogUtils.getBlock(status));
|
||||||
s.append(LogUtils.getBlock(msg));
|
s.append(LogUtils.getBlock(msg));
|
||||||
|
|
||||||
sys_user_logger.info(s.toString(), args);
|
sys_user_logger.info(s.toString(), args);
|
||||||
|
|
||||||
if (Constants.LOGIN_SUCCESS.equals(status) || Constants.LOGOUT.equals(status))
|
if (Constants.LOGIN_SUCCESS.equals(status) || Constants.LOGOUT.equals(status)) {
|
||||||
{
|
saveOpLog(username, msg, Constants.SUCCESS);
|
||||||
saveOpLog(username, msg, Constants.SUCCESS);
|
} else if (Constants.LOGIN_FAIL.equals(status)) {
|
||||||
}
|
saveOpLog(username, msg, Constants.FAIL);
|
||||||
else if (Constants.LOGIN_FAIL.equals(status))
|
}
|
||||||
{
|
}
|
||||||
saveOpLog(username, msg, Constants.FAIL);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void saveOpLog(String username, String message, String status)
|
public static void saveOpLog(String username, String message, String status) {
|
||||||
{
|
UserAgent userAgent = UserAgent.parseUserAgentString(ServletUtils.getRequest().getHeader("User-Agent"));
|
||||||
UserAgent userAgent = UserAgent.parseUserAgentString(ServletUtils.getRequest().getHeader("User-Agent"));
|
// 获取客户端操作系统
|
||||||
// 获取客户端操作系统
|
String os = userAgent.getOperatingSystem().getName();
|
||||||
String os = userAgent.getOperatingSystem().getName();
|
// 获取客户端浏览器
|
||||||
// 获取客户端浏览器
|
String browser = userAgent.getBrowser().getName();
|
||||||
String browser = userAgent.getBrowser().getName();
|
LogininforServiceImpl logininforService = SpringUtils.getBean(LogininforServiceImpl.class);
|
||||||
LogininforServiceImpl logininforService = SpringUtils.getBean(LogininforServiceImpl.class);
|
Logininfor logininfor = new Logininfor();
|
||||||
Logininfor logininfor = new Logininfor();
|
logininfor.setLoginName(username);
|
||||||
logininfor.setLoginName(username);
|
logininfor.setStatus(status);
|
||||||
logininfor.setStatus(status);
|
logininfor.setIpaddr(ShiroUtils.getIp());
|
||||||
logininfor.setIpaddr(ShiroUtils.getIp());
|
logininfor.setLoginLocation(AddressUtils.getRealAddressByIP(ShiroUtils.getIp()));
|
||||||
logininfor.setLoginLocation(AddressUtils.getRealAddressByIP(ShiroUtils.getIp()));
|
logininfor.setBrowser(browser);
|
||||||
logininfor.setBrowser(browser);
|
logininfor.setOs(os);
|
||||||
logininfor.setOs(os);
|
logininfor.setMsg(message);
|
||||||
logininfor.setMsg(message);
|
logininforService.insertLogininfor(logininfor);
|
||||||
logininforService.insertLogininfor(logininfor);
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,7 +2,7 @@ package com.ruoyi.framework.aspectj;
|
||||||
|
|
||||||
import java.lang.reflect.Method;
|
import java.lang.reflect.Method;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import com.ruoyi.common.utils.AddressUtils;
|
|
||||||
import org.aspectj.lang.JoinPoint;
|
import org.aspectj.lang.JoinPoint;
|
||||||
import org.aspectj.lang.Signature;
|
import org.aspectj.lang.Signature;
|
||||||
import org.aspectj.lang.annotation.AfterReturning;
|
import org.aspectj.lang.annotation.AfterReturning;
|
||||||
|
@ -12,18 +12,19 @@ import org.aspectj.lang.annotation.Pointcut;
|
||||||
import org.aspectj.lang.reflect.MethodSignature;
|
import org.aspectj.lang.reflect.MethodSignature;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
|
||||||
import org.springframework.scheduling.annotation.Async;
|
import org.springframework.scheduling.annotation.Async;
|
||||||
import org.springframework.scheduling.annotation.EnableAsync;
|
import org.springframework.scheduling.annotation.EnableAsync;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
import com.alibaba.fastjson.JSONObject;
|
import com.alibaba.fastjson.JSONObject;
|
||||||
import com.ruoyi.common.utils.ServletUtils;
|
import com.ruoyi.common.utils.ServletUtils;
|
||||||
import com.ruoyi.common.utils.StringUtils;
|
import com.ruoyi.common.utils.StringUtils;
|
||||||
import com.ruoyi.common.utils.security.ShiroUtils;
|
import com.ruoyi.common.utils.security.ShiroUtils;
|
||||||
import com.ruoyi.framework.aspectj.lang.annotation.Log;
|
import com.ruoyi.framework.aspectj.lang.annotation.Log;
|
||||||
import com.ruoyi.framework.aspectj.lang.constant.BusinessStatus;
|
import com.ruoyi.framework.aspectj.lang.constant.BusinessStatus;
|
||||||
|
import com.ruoyi.framework.manager.AsyncManager;
|
||||||
|
import com.ruoyi.framework.manager.factory.AsyncFactory;
|
||||||
import com.ruoyi.project.monitor.operlog.domain.OperLog;
|
import com.ruoyi.project.monitor.operlog.domain.OperLog;
|
||||||
import com.ruoyi.project.monitor.operlog.service.IOperLogService;
|
|
||||||
import com.ruoyi.project.system.user.domain.User;
|
import com.ruoyi.project.system.user.domain.User;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -34,149 +35,128 @@ import com.ruoyi.project.system.user.domain.User;
|
||||||
@Aspect
|
@Aspect
|
||||||
@Component
|
@Component
|
||||||
@EnableAsync
|
@EnableAsync
|
||||||
public class LogAspect
|
public class LogAspect {
|
||||||
{
|
private static final Logger log = LoggerFactory.getLogger(LogAspect.class);
|
||||||
private static final Logger log = LoggerFactory.getLogger(LogAspect.class);
|
|
||||||
|
|
||||||
@Autowired
|
// 配置织入点
|
||||||
private IOperLogService operLogService;
|
@Pointcut("@annotation(com.ruoyi.framework.aspectj.lang.annotation.Log)")
|
||||||
|
public void logPointCut() {
|
||||||
|
}
|
||||||
|
|
||||||
// 配置织入点
|
/**
|
||||||
@Pointcut("@annotation(com.ruoyi.framework.aspectj.lang.annotation.Log)")
|
* 前置通知 用于拦截操作
|
||||||
public void logPointCut()
|
*
|
||||||
{
|
* @param joinPoint
|
||||||
}
|
* 切点
|
||||||
|
*/
|
||||||
|
@AfterReturning(pointcut = "logPointCut()")
|
||||||
|
public void doBefore(JoinPoint joinPoint) {
|
||||||
|
handleLog(joinPoint, null);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 前置通知 用于拦截操作
|
* 拦截异常操作
|
||||||
*
|
*
|
||||||
* @param joinPoint 切点
|
* @param joinPoint
|
||||||
*/
|
* @param e
|
||||||
@AfterReturning(pointcut = "logPointCut()")
|
*/
|
||||||
public void doBefore(JoinPoint joinPoint)
|
@AfterThrowing(value = "logPointCut()", throwing = "e")
|
||||||
{
|
public void doAfter(JoinPoint joinPoint, Exception e) {
|
||||||
handleLog(joinPoint, null);
|
handleLog(joinPoint, e);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
@Async
|
||||||
* 拦截异常操作
|
protected void handleLog(final JoinPoint joinPoint, final Exception e) {
|
||||||
*
|
try {
|
||||||
* @param joinPoint
|
// 获得注解
|
||||||
* @param e
|
Log controllerLog = getAnnotationLog(joinPoint);
|
||||||
*/
|
if (controllerLog == null) {
|
||||||
@AfterThrowing(value = "logPointCut()", throwing = "e")
|
return;
|
||||||
public void doAfter(JoinPoint joinPoint, Exception e)
|
}
|
||||||
{
|
|
||||||
handleLog(joinPoint, e);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Async
|
// 获取当前的用户
|
||||||
protected void handleLog(final JoinPoint joinPoint, final Exception e)
|
User currentUser = ShiroUtils.getUser();
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
// 获得注解
|
|
||||||
Log controllerLog = getAnnotationLog(joinPoint);
|
|
||||||
if (controllerLog == null)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 获取当前的用户
|
// *========数据库日志=========*//
|
||||||
User currentUser = ShiroUtils.getUser();
|
OperLog operLog = new OperLog();
|
||||||
|
operLog.setStatus(BusinessStatus.SUCCESS);
|
||||||
|
// 请求的地址
|
||||||
|
String ip = ShiroUtils.getIp();
|
||||||
|
operLog.setOperIp(ip);
|
||||||
|
|
||||||
// *========数据库日志=========*//
|
operLog.setOperUrl(ServletUtils.getRequest().getRequestURI());
|
||||||
OperLog operLog = new OperLog();
|
if (currentUser != null) {
|
||||||
operLog.setStatus(BusinessStatus.SUCCESS);
|
operLog.setOperName(currentUser.getLoginName());
|
||||||
// 请求的地址
|
if (StringUtils.isNotNull(currentUser.getDept()) && StringUtils.isNotEmpty(currentUser.getDept().getDeptName())) {
|
||||||
String ip = ShiroUtils.getIp();
|
operLog.setDeptName(currentUser.getDept().getDeptName());
|
||||||
operLog.setOperIp(ip);
|
}
|
||||||
// 操作地点
|
}
|
||||||
operLog.setOperLocation(AddressUtils.getRealAddressByIP(ip));
|
|
||||||
|
|
||||||
operLog.setOperUrl(ServletUtils.getRequest().getRequestURI());
|
if (e != null) {
|
||||||
if (currentUser != null)
|
operLog.setStatus(BusinessStatus.FAIL);
|
||||||
{
|
operLog.setErrorMsg(StringUtils.substring(e.getMessage(), 0, 2000));
|
||||||
operLog.setOperName(currentUser.getLoginName());
|
}
|
||||||
if (StringUtils.isNotNull(currentUser.getDept())
|
// 设置方法名称
|
||||||
&& StringUtils.isNotEmpty(currentUser.getDept().getDeptName()))
|
String className = joinPoint.getTarget().getClass().getName();
|
||||||
{
|
String methodName = joinPoint.getSignature().getName();
|
||||||
operLog.setDeptName(currentUser.getDept().getDeptName());
|
operLog.setMethod(className + "." + methodName + "()");
|
||||||
}
|
// 处理设置注解上的参数
|
||||||
}
|
getControllerMethodDescription(controllerLog, operLog);
|
||||||
|
// 保存数据库
|
||||||
|
AsyncManager.me().execute(AsyncFactory.recordOper(operLog));
|
||||||
|
} catch (Exception exp) {
|
||||||
|
// 记录本地异常日志
|
||||||
|
log.error("==前置通知异常==");
|
||||||
|
log.error("异常信息:{}", exp.getMessage());
|
||||||
|
exp.printStackTrace();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (e != null)
|
/**
|
||||||
{
|
* 获取注解中对方法的描述信息 用于Controller层注解
|
||||||
operLog.setStatus(BusinessStatus.FAIL);
|
*
|
||||||
operLog.setErrorMsg(StringUtils.substring(e.getMessage(), 0, 2000));
|
* @param joinPoint
|
||||||
}
|
* 切点
|
||||||
// 设置方法名称
|
* @return 方法描述
|
||||||
String className = joinPoint.getTarget().getClass().getName();
|
* @throws Exception
|
||||||
String methodName = joinPoint.getSignature().getName();
|
*/
|
||||||
operLog.setMethod(className + "." + methodName + "()");
|
public void getControllerMethodDescription(Log log, OperLog operLog) throws Exception {
|
||||||
// 处理设置注解上的参数
|
// 设置action动作
|
||||||
getControllerMethodDescription(controllerLog, operLog);
|
operLog.setAction(log.action());
|
||||||
// 保存数据库
|
// 设置标题
|
||||||
operLogService.insertOperlog(operLog);
|
operLog.setTitle(log.title());
|
||||||
}
|
// 设置channel
|
||||||
catch (Exception exp)
|
operLog.setChannel(log.channel());
|
||||||
{
|
// 是否需要保存request,参数和值
|
||||||
// 记录本地异常日志
|
if (log.isSaveRequestData()) {
|
||||||
log.error("==前置通知异常==");
|
// 获取参数的信息,传入到数据库中。
|
||||||
log.error("异常信息:{}", exp.getMessage());
|
setRequestValue(operLog);
|
||||||
exp.printStackTrace();
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取注解中对方法的描述信息 用于Controller层注解
|
* 获取请求的参数,放到log中
|
||||||
*
|
*
|
||||||
* @param joinPoint 切点
|
* @param operLog
|
||||||
* @return 方法描述
|
* @param request
|
||||||
* @throws Exception
|
*/
|
||||||
*/
|
private void setRequestValue(OperLog operLog) {
|
||||||
public void getControllerMethodDescription(Log log, OperLog operLog) throws Exception
|
Map<String, String[]> map = ServletUtils.getRequest().getParameterMap();
|
||||||
{
|
String params = JSONObject.toJSONString(map);
|
||||||
// 设置action动作
|
operLog.setOperParam(StringUtils.substring(params, 0, 255));
|
||||||
operLog.setAction(log.action());
|
}
|
||||||
// 设置标题
|
|
||||||
operLog.setTitle(log.title());
|
|
||||||
// 设置channel
|
|
||||||
operLog.setChannel(log.channel());
|
|
||||||
// 是否需要保存request,参数和值
|
|
||||||
if (log.isSaveRequestData())
|
|
||||||
{
|
|
||||||
// 获取参数的信息,传入到数据库中。
|
|
||||||
setRequestValue(operLog);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取请求的参数,放到log中
|
* 是否存在注解,如果存在就获取
|
||||||
*
|
*/
|
||||||
* @param operLog
|
private Log getAnnotationLog(JoinPoint joinPoint) throws Exception {
|
||||||
* @param request
|
Signature signature = joinPoint.getSignature();
|
||||||
*/
|
MethodSignature methodSignature = (MethodSignature) signature;
|
||||||
private void setRequestValue(OperLog operLog)
|
Method method = methodSignature.getMethod();
|
||||||
{
|
|
||||||
Map<String, String[]> map = ServletUtils.getRequest().getParameterMap();
|
|
||||||
String params = JSONObject.toJSONString(map);
|
|
||||||
operLog.setOperParam(StringUtils.substring(params, 0, 255));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
if (method != null) {
|
||||||
* 是否存在注解,如果存在就获取
|
return method.getAnnotation(Log.class);
|
||||||
*/
|
}
|
||||||
private Log getAnnotationLog(JoinPoint joinPoint) throws Exception
|
return null;
|
||||||
{
|
}
|
||||||
Signature signature = joinPoint.getSignature();
|
|
||||||
MethodSignature methodSignature = (MethodSignature) signature;
|
|
||||||
Method method = methodSignature.getMethod();
|
|
||||||
|
|
||||||
if (method != null)
|
|
||||||
{
|
|
||||||
return method.getAnnotation(Log.class);
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,26 @@
|
||||||
|
package com.ruoyi.framework.manager;
|
||||||
|
|
||||||
|
import java.util.TimerTask;
|
||||||
|
import java.util.concurrent.ScheduledThreadPoolExecutor;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 异步任务管理器
|
||||||
|
*
|
||||||
|
* @author liuhulu
|
||||||
|
*/
|
||||||
|
public class AsyncManager {
|
||||||
|
// 操作延迟
|
||||||
|
private final int OPERATE_DELAY_TIME = 10;
|
||||||
|
// 异步操作此案城池
|
||||||
|
private ScheduledThreadPoolExecutor executor = new ScheduledThreadPoolExecutor(5);
|
||||||
|
// 单例
|
||||||
|
private static AsyncManager me = new AsyncManager();
|
||||||
|
public static AsyncManager me() {
|
||||||
|
return me;
|
||||||
|
}
|
||||||
|
// 执行任务
|
||||||
|
public void execute(TimerTask task) {
|
||||||
|
executor.schedule(task, OPERATE_DELAY_TIME, TimeUnit.MILLISECONDS);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,125 @@
|
||||||
|
package com.ruoyi.framework.manager.factory;
|
||||||
|
|
||||||
|
import java.util.TimerTask;
|
||||||
|
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
import com.ruoyi.common.constant.Constants;
|
||||||
|
import com.ruoyi.common.utils.AddressUtils;
|
||||||
|
import com.ruoyi.common.utils.LogUtils;
|
||||||
|
import com.ruoyi.common.utils.ServletUtils;
|
||||||
|
import com.ruoyi.common.utils.security.ShiroUtils;
|
||||||
|
import com.ruoyi.common.utils.spring.SpringUtils;
|
||||||
|
import com.ruoyi.project.monitor.logininfor.domain.Logininfor;
|
||||||
|
import com.ruoyi.project.monitor.logininfor.service.LogininforServiceImpl;
|
||||||
|
import com.ruoyi.project.monitor.online.domain.OnlineSession;
|
||||||
|
import com.ruoyi.project.monitor.online.domain.UserOnline;
|
||||||
|
import com.ruoyi.project.monitor.online.service.IUserOnlineService;
|
||||||
|
import com.ruoyi.project.monitor.operlog.domain.OperLog;
|
||||||
|
import com.ruoyi.project.monitor.operlog.service.IOperLogService;
|
||||||
|
|
||||||
|
import eu.bitwalker.useragentutils.UserAgent;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 异步工厂(产生任务用)
|
||||||
|
*
|
||||||
|
* @author liuhulu
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
public class AsyncFactory {
|
||||||
|
private static final Logger sys_user_logger = LoggerFactory.getLogger("sys-user");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 同步session到数据库
|
||||||
|
*
|
||||||
|
* @param operLog
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
public static TimerTask syncSessionToDb(final OnlineSession session) {
|
||||||
|
return new TimerTask() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
UserOnline online = new UserOnline();
|
||||||
|
online.setSessionId(String.valueOf(session.getId()));
|
||||||
|
online.setDeptName(session.getDeptName());
|
||||||
|
online.setLoginName(session.getLoginName());
|
||||||
|
online.setStartTimestamp(session.getStartTimestamp());
|
||||||
|
online.setLastAccessTime(session.getLastAccessTime());
|
||||||
|
online.setExpireTime(session.getTimeout());
|
||||||
|
online.setIpaddr(session.getHost());
|
||||||
|
online.setLonginLocation(AddressUtils.getRealAddressByIP(session.getHost()));
|
||||||
|
online.setBrowser(session.getBrowser());
|
||||||
|
online.setOs(session.getOs());
|
||||||
|
online.setStatus(session.getStatus());
|
||||||
|
online.setSession(session);
|
||||||
|
SpringUtils.getBean(IUserOnlineService.class).saveOnline(online);
|
||||||
|
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* 记录 操作log
|
||||||
|
*
|
||||||
|
* @param rc
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
public static TimerTask recordOper(final OperLog operLog) {
|
||||||
|
return new TimerTask() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
// 远程查询操作地点
|
||||||
|
operLog.setOperLocation(AddressUtils.getRealAddressByIP(operLog.getOperIp()));
|
||||||
|
SpringUtils.getBean(IOperLogService.class).insertOperlog(operLog);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 记录登陆信息
|
||||||
|
*
|
||||||
|
* @param username
|
||||||
|
* @param status
|
||||||
|
* @param message
|
||||||
|
* @param userAgent
|
||||||
|
* @param args
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
public static TimerTask recordLogininfor(final String username, final String status, final String message, final Object... args) {
|
||||||
|
final UserAgent userAgent = UserAgent.parseUserAgentString(ServletUtils.getRequest().getHeader("User-Agent"));
|
||||||
|
return new TimerTask() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
StringBuilder s = new StringBuilder();
|
||||||
|
s.append(LogUtils.getBlock(ShiroUtils.getIp()));
|
||||||
|
s.append(AddressUtils.getRealAddressByIP(ShiroUtils.getIp()));
|
||||||
|
s.append(LogUtils.getBlock(username));
|
||||||
|
s.append(LogUtils.getBlock(status));
|
||||||
|
s.append(LogUtils.getBlock(message));
|
||||||
|
// 打印信息到日志
|
||||||
|
sys_user_logger.info(s.toString(), args);
|
||||||
|
// 获取客户端操作系统
|
||||||
|
String os = userAgent.getOperatingSystem().getName();
|
||||||
|
// 获取客户端浏览器
|
||||||
|
String browser = userAgent.getBrowser().getName();
|
||||||
|
// 封装对象
|
||||||
|
Logininfor logininfor = new Logininfor();
|
||||||
|
logininfor.setLoginName(username);
|
||||||
|
logininfor.setIpaddr(ShiroUtils.getIp());
|
||||||
|
logininfor.setLoginLocation(AddressUtils.getRealAddressByIP(ShiroUtils.getIp()));
|
||||||
|
logininfor.setBrowser(browser);
|
||||||
|
logininfor.setOs(os);
|
||||||
|
logininfor.setMsg(message);
|
||||||
|
// 日志状态
|
||||||
|
if (Constants.LOGIN_SUCCESS.equals(status) || Constants.LOGOUT.equals(status)) {
|
||||||
|
logininfor.setStatus(Constants.SUCCESS);
|
||||||
|
} else if (Constants.LOGIN_FAIL.equals(status)) {
|
||||||
|
logininfor.setStatus(Constants.FAIL);
|
||||||
|
}
|
||||||
|
// 插入数据
|
||||||
|
SpringUtils.getBean(LogininforServiceImpl.class).insertLogininfor(logininfor);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -3,6 +3,7 @@ package com.ruoyi.framework.shiro.service;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
import org.springframework.util.StringUtils;
|
import org.springframework.util.StringUtils;
|
||||||
|
|
||||||
import com.ruoyi.common.constant.Constants;
|
import com.ruoyi.common.constant.Constants;
|
||||||
import com.ruoyi.common.constant.ShiroConstants;
|
import com.ruoyi.common.constant.ShiroConstants;
|
||||||
import com.ruoyi.common.constant.UserConstants;
|
import com.ruoyi.common.constant.UserConstants;
|
||||||
|
@ -13,8 +14,9 @@ import com.ruoyi.common.exception.user.UserPasswordNotMatchException;
|
||||||
import com.ruoyi.common.utils.DateUtils;
|
import com.ruoyi.common.utils.DateUtils;
|
||||||
import com.ruoyi.common.utils.MessageUtils;
|
import com.ruoyi.common.utils.MessageUtils;
|
||||||
import com.ruoyi.common.utils.ServletUtils;
|
import com.ruoyi.common.utils.ServletUtils;
|
||||||
import com.ruoyi.common.utils.SystemLogUtils;
|
|
||||||
import com.ruoyi.common.utils.security.ShiroUtils;
|
import com.ruoyi.common.utils.security.ShiroUtils;
|
||||||
|
import com.ruoyi.framework.manager.AsyncManager;
|
||||||
|
import com.ruoyi.framework.manager.factory.AsyncFactory;
|
||||||
import com.ruoyi.project.system.user.domain.User;
|
import com.ruoyi.project.system.user.domain.User;
|
||||||
import com.ruoyi.project.system.user.domain.UserStatus;
|
import com.ruoyi.project.system.user.domain.UserStatus;
|
||||||
import com.ruoyi.project.system.user.service.IUserService;
|
import com.ruoyi.project.system.user.service.IUserService;
|
||||||
|
@ -25,104 +27,101 @@ import com.ruoyi.project.system.user.service.IUserService;
|
||||||
* @author ruoyi
|
* @author ruoyi
|
||||||
*/
|
*/
|
||||||
@Component
|
@Component
|
||||||
public class LoginService
|
public class LoginService {
|
||||||
{
|
@Autowired
|
||||||
@Autowired
|
private PasswordService passwordService;
|
||||||
private PasswordService passwordService;
|
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
private IUserService userService;
|
private IUserService userService;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 登录
|
* 登录
|
||||||
*/
|
*/
|
||||||
public User login(String username, String password)
|
public User login(String username, String password) {
|
||||||
{
|
// 验证码校验
|
||||||
// 验证码校验
|
if (!StringUtils.isEmpty(ServletUtils.getRequest().getAttribute(ShiroConstants.CURRENT_CAPTCHA))) {
|
||||||
if (!StringUtils.isEmpty(ServletUtils.getRequest().getAttribute(ShiroConstants.CURRENT_CAPTCHA)))
|
AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, MessageUtils.message("user.jcaptcha.error")));
|
||||||
{
|
// SystemLogUtils.log(username, Constants.LOGIN_FAIL,
|
||||||
SystemLogUtils.log(username, Constants.LOGIN_FAIL, MessageUtils.message("user.jcaptcha.error"));
|
// MessageUtils.message("user.jcaptcha.error"));
|
||||||
throw new CaptchaException();
|
throw new CaptchaException();
|
||||||
}
|
}
|
||||||
// 用户名或密码为空 错误
|
// 用户名或密码为空 错误
|
||||||
if (StringUtils.isEmpty(username) || StringUtils.isEmpty(password))
|
if (StringUtils.isEmpty(username) || StringUtils.isEmpty(password)) {
|
||||||
{
|
AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, MessageUtils.message("not.null")));
|
||||||
SystemLogUtils.log(username, Constants.LOGIN_FAIL, MessageUtils.message("not.null"));
|
// SystemLogUtils.log(username, Constants.LOGIN_FAIL,
|
||||||
throw new UserNotExistsException();
|
// MessageUtils.message("not.null"));
|
||||||
}
|
throw new UserNotExistsException();
|
||||||
// 密码如果不在指定范围内 错误
|
}
|
||||||
if (password.length() < UserConstants.PASSWORD_MIN_LENGTH
|
// 密码如果不在指定范围内 错误
|
||||||
|| password.length() > UserConstants.PASSWORD_MAX_LENGTH)
|
if (password.length() < UserConstants.PASSWORD_MIN_LENGTH || password.length() > UserConstants.PASSWORD_MAX_LENGTH) {
|
||||||
{
|
AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, MessageUtils.message("user.password.not.match")));
|
||||||
SystemLogUtils.log(username, Constants.LOGIN_FAIL, MessageUtils.message("user.password.not.match"));
|
// SystemLogUtils.log(username, Constants.LOGIN_FAIL,
|
||||||
throw new UserPasswordNotMatchException();
|
// MessageUtils.message("user.password.not.match"));
|
||||||
}
|
throw new UserPasswordNotMatchException();
|
||||||
|
}
|
||||||
|
|
||||||
// 用户名不在指定范围内 错误
|
// 用户名不在指定范围内 错误
|
||||||
if (username.length() < UserConstants.USERNAME_MIN_LENGTH
|
if (username.length() < UserConstants.USERNAME_MIN_LENGTH || username.length() > UserConstants.USERNAME_MAX_LENGTH) {
|
||||||
|| username.length() > UserConstants.USERNAME_MAX_LENGTH)
|
AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, MessageUtils.message("user.password.not.match")));
|
||||||
{
|
// SystemLogUtils.log(username, Constants.LOGIN_FAIL,
|
||||||
SystemLogUtils.log(username, Constants.LOGIN_FAIL, MessageUtils.message("user.password.not.match"));
|
// MessageUtils.message("user.password.not.match"));
|
||||||
throw new UserPasswordNotMatchException();
|
throw new UserPasswordNotMatchException();
|
||||||
}
|
}
|
||||||
|
|
||||||
// 查询用户信息
|
// 查询用户信息
|
||||||
User user = userService.selectUserByLoginName(username);
|
User user = userService.selectUserByLoginName(username);
|
||||||
|
|
||||||
if (user == null && maybeMobilePhoneNumber(username))
|
if (user == null && maybeMobilePhoneNumber(username)) {
|
||||||
{
|
user = userService.selectUserByPhoneNumber(username);
|
||||||
user = userService.selectUserByPhoneNumber(username);
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if (user == null && maybeEmail(username))
|
if (user == null && maybeEmail(username)) {
|
||||||
{
|
user = userService.selectUserByEmail(username);
|
||||||
user = userService.selectUserByEmail(username);
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if (user == null || UserStatus.DELETED.getCode().equals(user.getDelFlag()))
|
if (user == null || UserStatus.DELETED.getCode().equals(user.getDelFlag())) {
|
||||||
{
|
AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, MessageUtils.message("user.not.exists")));
|
||||||
SystemLogUtils.log(username, Constants.LOGIN_FAIL, MessageUtils.message("user.not.exists"));
|
// SystemLogUtils.log(username, Constants.LOGIN_FAIL,
|
||||||
throw new UserNotExistsException();
|
// MessageUtils.message("user.not.exists"));
|
||||||
}
|
throw new UserNotExistsException();
|
||||||
|
}
|
||||||
|
|
||||||
passwordService.validate(user, password);
|
passwordService.validate(user, password);
|
||||||
|
|
||||||
if (UserStatus.DISABLE.getCode().equals(user.getStatus()))
|
if (UserStatus.DISABLE.getCode().equals(user.getStatus())) {
|
||||||
{
|
AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, MessageUtils.message("user.blocked", user.getRemark())));
|
||||||
SystemLogUtils.log(username, Constants.LOGIN_FAIL, MessageUtils.message("user.blocked", user.getRemark()));
|
// SystemLogUtils.log(username, Constants.LOGIN_FAIL,
|
||||||
throw new UserBlockedException(user.getRemark());
|
// MessageUtils.message("user.blocked", user.getRemark()));
|
||||||
}
|
throw new UserBlockedException(user.getRemark());
|
||||||
SystemLogUtils.log(username, Constants.LOGIN_SUCCESS, MessageUtils.message("user.login.success"));
|
}
|
||||||
recordLoginInfo(user);
|
AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_SUCCESS, MessageUtils.message("user.login.success")));
|
||||||
return user;
|
// SystemLogUtils.log(username, Constants.LOGIN_SUCCESS,
|
||||||
}
|
// MessageUtils.message("user.login.success"));
|
||||||
|
recordLoginInfo(user);
|
||||||
|
return user;
|
||||||
|
}
|
||||||
|
|
||||||
private boolean maybeEmail(String username)
|
private boolean maybeEmail(String username) {
|
||||||
{
|
if (!username.matches(UserConstants.EMAIL_PATTERN)) {
|
||||||
if (!username.matches(UserConstants.EMAIL_PATTERN))
|
return false;
|
||||||
{
|
}
|
||||||
return false;
|
return true;
|
||||||
}
|
}
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean maybeMobilePhoneNumber(String username)
|
private boolean maybeMobilePhoneNumber(String username) {
|
||||||
{
|
if (!username.matches(UserConstants.MOBILE_PHONE_NUMBER_PATTERN)) {
|
||||||
if (!username.matches(UserConstants.MOBILE_PHONE_NUMBER_PATTERN))
|
return false;
|
||||||
{
|
}
|
||||||
return false;
|
return true;
|
||||||
}
|
}
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 记录登录信息
|
* 记录登录信息
|
||||||
*/
|
*/
|
||||||
public void recordLoginInfo(User user)
|
public void recordLoginInfo(User user) {
|
||||||
{
|
user.setLoginIp(ShiroUtils.getIp());
|
||||||
user.setLoginIp(ShiroUtils.getIp());
|
user.setLoginDate(DateUtils.getNowDate());
|
||||||
user.setLoginDate(DateUtils.getNowDate());
|
userService.updateUserInfo(user);
|
||||||
userService.updateUserInfo(user);
|
}
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,18 +1,22 @@
|
||||||
package com.ruoyi.framework.shiro.service;
|
package com.ruoyi.framework.shiro.service;
|
||||||
|
|
||||||
import java.util.concurrent.atomic.AtomicInteger;
|
import java.util.concurrent.atomic.AtomicInteger;
|
||||||
|
|
||||||
import javax.annotation.PostConstruct;
|
import javax.annotation.PostConstruct;
|
||||||
|
|
||||||
import org.apache.shiro.cache.Cache;
|
import org.apache.shiro.cache.Cache;
|
||||||
import org.apache.shiro.cache.CacheManager;
|
import org.apache.shiro.cache.CacheManager;
|
||||||
import org.apache.shiro.crypto.hash.Md5Hash;
|
import org.apache.shiro.crypto.hash.Md5Hash;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.beans.factory.annotation.Value;
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
import com.ruoyi.common.constant.Constants;
|
import com.ruoyi.common.constant.Constants;
|
||||||
import com.ruoyi.common.exception.user.UserPasswordNotMatchException;
|
import com.ruoyi.common.exception.user.UserPasswordNotMatchException;
|
||||||
import com.ruoyi.common.exception.user.UserPasswordRetryLimitExceedException;
|
import com.ruoyi.common.exception.user.UserPasswordRetryLimitExceedException;
|
||||||
import com.ruoyi.common.utils.MessageUtils;
|
import com.ruoyi.common.utils.MessageUtils;
|
||||||
import com.ruoyi.common.utils.SystemLogUtils;
|
import com.ruoyi.framework.manager.AsyncManager;
|
||||||
|
import com.ruoyi.framework.manager.factory.AsyncFactory;
|
||||||
import com.ruoyi.project.system.user.domain.User;
|
import com.ruoyi.project.system.user.domain.User;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -21,70 +25,66 @@ import com.ruoyi.project.system.user.domain.User;
|
||||||
* @author ruoyi
|
* @author ruoyi
|
||||||
*/
|
*/
|
||||||
@Component
|
@Component
|
||||||
public class PasswordService
|
public class PasswordService {
|
||||||
{
|
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
private CacheManager cacheManager;
|
private CacheManager cacheManager;
|
||||||
|
|
||||||
private Cache<String, AtomicInteger> loginRecordCache;
|
private Cache<String, AtomicInteger> loginRecordCache;
|
||||||
|
|
||||||
@Value(value = "${user.password.maxRetryCount}")
|
@Value(value = "${user.password.maxRetryCount}")
|
||||||
private String maxRetryCount;
|
private String maxRetryCount;
|
||||||
|
|
||||||
@PostConstruct
|
@PostConstruct
|
||||||
public void init()
|
public void init() {
|
||||||
{
|
loginRecordCache = cacheManager.getCache("loginRecordCache");
|
||||||
loginRecordCache = cacheManager.getCache("loginRecordCache");
|
}
|
||||||
}
|
|
||||||
|
|
||||||
public void validate(User user, String password)
|
public void validate(User user, String password) {
|
||||||
{
|
String loginName = user.getLoginName();
|
||||||
String loginName = user.getLoginName();
|
|
||||||
|
|
||||||
AtomicInteger retryCount = loginRecordCache.get(loginName);
|
AtomicInteger retryCount = loginRecordCache.get(loginName);
|
||||||
|
|
||||||
if (retryCount == null)
|
if (retryCount == null) {
|
||||||
{
|
retryCount = new AtomicInteger(0);
|
||||||
retryCount = new AtomicInteger(0);
|
loginRecordCache.put(loginName, retryCount);
|
||||||
loginRecordCache.put(loginName, retryCount);
|
}
|
||||||
}
|
if (retryCount.incrementAndGet() > Integer.valueOf(maxRetryCount).intValue()) {
|
||||||
if (retryCount.incrementAndGet() > Integer.valueOf(maxRetryCount).intValue())
|
AsyncManager.me().execute(AsyncFactory.recordLogininfor(loginName, Constants.LOGIN_FAIL, MessageUtils.message("user.password.retry.limit.exceed"), maxRetryCount));
|
||||||
{
|
// SystemLogUtils.log(loginName, Constants.LOGIN_FAIL,
|
||||||
SystemLogUtils.log(loginName, Constants.LOGIN_FAIL, MessageUtils.message("user.password.retry.limit.exceed", maxRetryCount));
|
// MessageUtils.message("user.password.retry.limit.exceed",
|
||||||
throw new UserPasswordRetryLimitExceedException(Integer.valueOf(maxRetryCount).intValue());
|
// maxRetryCount));
|
||||||
}
|
throw new UserPasswordRetryLimitExceedException(Integer.valueOf(maxRetryCount).intValue());
|
||||||
|
}
|
||||||
|
|
||||||
if (!matches(user, password))
|
if (!matches(user, password)) {
|
||||||
{
|
AsyncManager.me().execute(AsyncFactory.recordLogininfor(loginName, Constants.LOGIN_FAIL, MessageUtils.message("user.password.retry.limit.count"), retryCount, password));
|
||||||
SystemLogUtils.log(loginName, Constants.LOGIN_FAIL, MessageUtils.message("user.password.retry.limit.count", retryCount, password));
|
// SystemLogUtils.log(loginName, Constants.LOGIN_FAIL,
|
||||||
loginRecordCache.put(loginName, retryCount);
|
// MessageUtils.message("user.password.retry.limit.count",
|
||||||
throw new UserPasswordNotMatchException();
|
// retryCount, password));
|
||||||
}
|
loginRecordCache.put(loginName, retryCount);
|
||||||
else
|
throw new UserPasswordNotMatchException();
|
||||||
{
|
} else {
|
||||||
clearLoginRecordCache(loginName);
|
clearLoginRecordCache(loginName);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean matches(User user, String newPassword)
|
public boolean matches(User user, String newPassword) {
|
||||||
{
|
return user.getPassword().equals(encryptPassword(user.getLoginName(), newPassword, user.getSalt()));
|
||||||
return user.getPassword().equals(encryptPassword(user.getLoginName(), newPassword, user.getSalt()));
|
}
|
||||||
}
|
|
||||||
|
|
||||||
public void clearLoginRecordCache(String username)
|
public void clearLoginRecordCache(String username) {
|
||||||
{
|
loginRecordCache.remove(username);
|
||||||
loginRecordCache.remove(username);
|
}
|
||||||
}
|
|
||||||
|
|
||||||
public String encryptPassword(String username, String password, String salt)
|
public String encryptPassword(String username, String password, String salt) {
|
||||||
{
|
return new Md5Hash(username + password + salt).toHex().toString();
|
||||||
return new Md5Hash(username + password + salt).toHex().toString();
|
}
|
||||||
}
|
|
||||||
|
|
||||||
public static void main(String[] args)
|
public static void main(String[] args) {
|
||||||
{
|
// System.out.println(new PasswordService().encryptPassword("admin",
|
||||||
//System.out.println(new PasswordService().encryptPassword("admin", "admin123", "111111"));
|
// "admin123", "111111"));
|
||||||
//System.out.println(new PasswordService().encryptPassword("ry", "admin123", "222222"));
|
// System.out.println(new PasswordService().encryptPassword("ry",
|
||||||
}
|
// "admin123", "222222"));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,10 +2,14 @@ package com.ruoyi.framework.shiro.session;
|
||||||
|
|
||||||
import java.io.Serializable;
|
import java.io.Serializable;
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
|
|
||||||
import org.apache.shiro.session.Session;
|
import org.apache.shiro.session.Session;
|
||||||
import org.apache.shiro.session.mgt.eis.EnterpriseCacheSessionDAO;
|
import org.apache.shiro.session.mgt.eis.EnterpriseCacheSessionDAO;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.beans.factory.annotation.Value;
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
|
|
||||||
|
import com.ruoyi.framework.manager.AsyncManager;
|
||||||
|
import com.ruoyi.framework.manager.factory.AsyncFactory;
|
||||||
import com.ruoyi.project.monitor.online.domain.OnlineSession;
|
import com.ruoyi.project.monitor.online.domain.OnlineSession;
|
||||||
import com.ruoyi.project.monitor.online.domain.UserOnline;
|
import com.ruoyi.project.monitor.online.domain.UserOnline;
|
||||||
import com.ruoyi.project.monitor.online.service.IUserOnlineService;
|
import com.ruoyi.project.monitor.online.service.IUserOnlineService;
|
||||||
|
@ -15,101 +19,90 @@ import com.ruoyi.project.monitor.online.service.IUserOnlineService;
|
||||||
*
|
*
|
||||||
* @author ruoyi
|
* @author ruoyi
|
||||||
*/
|
*/
|
||||||
public class OnlineSessionDAO extends EnterpriseCacheSessionDAO
|
public class OnlineSessionDAO extends EnterpriseCacheSessionDAO {
|
||||||
{
|
/**
|
||||||
/**
|
* 同步session到数据库的周期 单位为毫秒(默认1分钟)
|
||||||
* 同步session到数据库的周期 单位为毫秒(默认1分钟)
|
*/
|
||||||
*/
|
@Value("${shiro.session.dbSyncPeriod}")
|
||||||
@Value("${shiro.session.dbSyncPeriod}")
|
private int dbSyncPeriod;
|
||||||
private int dbSyncPeriod;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 上次同步数据库的时间戳
|
* 上次同步数据库的时间戳
|
||||||
*/
|
*/
|
||||||
private static final String LAST_SYNC_DB_TIMESTAMP = OnlineSessionDAO.class.getName() + "LAST_SYNC_DB_TIMESTAMP";
|
private static final String LAST_SYNC_DB_TIMESTAMP = OnlineSessionDAO.class.getName() + "LAST_SYNC_DB_TIMESTAMP";
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
private IUserOnlineService onlineService;
|
private IUserOnlineService onlineService;
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
private OnlineSessionFactory onlineSessionFactory;
|
private OnlineSessionFactory onlineSessionFactory;
|
||||||
|
|
||||||
public OnlineSessionDAO()
|
public OnlineSessionDAO() {
|
||||||
{
|
super();
|
||||||
super();
|
}
|
||||||
}
|
|
||||||
|
|
||||||
public OnlineSessionDAO(long expireTime)
|
public OnlineSessionDAO(long expireTime) {
|
||||||
{
|
super();
|
||||||
super();
|
}
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 根据会话ID获取会话
|
* 根据会话ID获取会话
|
||||||
*
|
*
|
||||||
* @param sessionId 会话ID
|
* @param sessionId
|
||||||
* @return ShiroSession
|
* 会话ID
|
||||||
*/
|
* @return ShiroSession
|
||||||
@Override
|
*/
|
||||||
protected Session doReadSession(Serializable sessionId)
|
@Override
|
||||||
{
|
protected Session doReadSession(Serializable sessionId) {
|
||||||
UserOnline userOnline = onlineService.selectOnlineById(String.valueOf(sessionId));
|
UserOnline userOnline = onlineService.selectOnlineById(String.valueOf(sessionId));
|
||||||
if (userOnline == null)
|
if (userOnline == null) {
|
||||||
{
|
return null;
|
||||||
return null;
|
}
|
||||||
}
|
return onlineSessionFactory.createSession(userOnline);
|
||||||
return onlineSessionFactory.createSession(userOnline);
|
}
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 更新会话;如更新会话最后访问时间/停止会话/设置超时时间/设置移除属性等会调用
|
* 更新会话;如更新会话最后访问时间/停止会话/设置超时时间/设置移除属性等会调用
|
||||||
*/
|
*/
|
||||||
public void syncToDb(OnlineSession onlineSession)
|
public void syncToDb(OnlineSession onlineSession) {
|
||||||
{
|
Date lastSyncTimestamp = (Date) onlineSession.getAttribute(LAST_SYNC_DB_TIMESTAMP);
|
||||||
Date lastSyncTimestamp = (Date) onlineSession.getAttribute(LAST_SYNC_DB_TIMESTAMP);
|
if (lastSyncTimestamp != null) {
|
||||||
if (lastSyncTimestamp != null)
|
boolean needSync = true;
|
||||||
{
|
long deltaTime = onlineSession.getLastAccessTime().getTime() - lastSyncTimestamp.getTime();
|
||||||
boolean needSync = true;
|
if (deltaTime < dbSyncPeriod * 60 * 1000) {
|
||||||
long deltaTime = onlineSession.getLastAccessTime().getTime() - lastSyncTimestamp.getTime();
|
// 时间差不足 无需同步
|
||||||
if (deltaTime < dbSyncPeriod * 60 * 1000)
|
needSync = false;
|
||||||
{
|
}
|
||||||
// 时间差不足 无需同步
|
boolean isGuest = onlineSession.getUserId() == null || onlineSession.getUserId() == 0L;
|
||||||
needSync = false;
|
|
||||||
}
|
|
||||||
boolean isGuest = onlineSession.getUserId() == null || onlineSession.getUserId() == 0L;
|
|
||||||
|
|
||||||
// session 数据变更了 同步
|
// session 数据变更了 同步
|
||||||
if (isGuest == false && onlineSession.isAttributeChanged())
|
if (isGuest == false && onlineSession.isAttributeChanged()) {
|
||||||
{
|
needSync = true;
|
||||||
needSync = true;
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if (needSync == false)
|
if (needSync == false) {
|
||||||
{
|
return;
|
||||||
return;
|
}
|
||||||
}
|
}
|
||||||
}
|
onlineSession.setAttribute(LAST_SYNC_DB_TIMESTAMP, onlineSession.getLastAccessTime());
|
||||||
onlineSession.setAttribute(LAST_SYNC_DB_TIMESTAMP, onlineSession.getLastAccessTime());
|
// 更新完后 重置标识
|
||||||
// 更新完后 重置标识
|
if (onlineSession.isAttributeChanged()) {
|
||||||
if (onlineSession.isAttributeChanged())
|
onlineSession.resetAttributeChanged();
|
||||||
{
|
}
|
||||||
onlineSession.resetAttributeChanged();
|
// onlineService.saveOnline(UserOnline.fromOnlineSession(onlineSession));
|
||||||
}
|
AsyncManager.me().execute(AsyncFactory.syncSessionToDb(onlineSession));
|
||||||
onlineService.saveOnline(UserOnline.fromOnlineSession(onlineSession));
|
}
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 当会话过期/停止(如用户退出时)属性等会调用
|
* 当会话过期/停止(如用户退出时)属性等会调用
|
||||||
*/
|
*/
|
||||||
@Override
|
@Override
|
||||||
protected void doDelete(Session session)
|
protected void doDelete(Session session) {
|
||||||
{
|
OnlineSession onlineSession = (OnlineSession) session;
|
||||||
OnlineSession onlineSession = (OnlineSession) session;
|
if (null == onlineSession) {
|
||||||
if (null == onlineSession)
|
return;
|
||||||
{
|
}
|
||||||
return;
|
onlineSession.setStatus(OnlineSession.OnlineStatus.off_line);
|
||||||
}
|
onlineService.deleteOnlineById(String.valueOf(onlineSession.getId()));
|
||||||
onlineSession.setStatus(OnlineSession.OnlineStatus.off_line);
|
}
|
||||||
onlineService.deleteOnlineById(String.valueOf(onlineSession.getId()));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,15 +2,18 @@ package com.ruoyi.framework.shiro.web.filter;
|
||||||
|
|
||||||
import javax.servlet.ServletRequest;
|
import javax.servlet.ServletRequest;
|
||||||
import javax.servlet.ServletResponse;
|
import javax.servlet.ServletResponse;
|
||||||
|
|
||||||
import org.apache.shiro.session.SessionException;
|
import org.apache.shiro.session.SessionException;
|
||||||
import org.apache.shiro.subject.Subject;
|
import org.apache.shiro.subject.Subject;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
import com.ruoyi.common.constant.Constants;
|
import com.ruoyi.common.constant.Constants;
|
||||||
import com.ruoyi.common.utils.MessageUtils;
|
import com.ruoyi.common.utils.MessageUtils;
|
||||||
import com.ruoyi.common.utils.StringUtils;
|
import com.ruoyi.common.utils.StringUtils;
|
||||||
import com.ruoyi.common.utils.SystemLogUtils;
|
|
||||||
import com.ruoyi.common.utils.security.ShiroUtils;
|
import com.ruoyi.common.utils.security.ShiroUtils;
|
||||||
|
import com.ruoyi.framework.manager.AsyncManager;
|
||||||
|
import com.ruoyi.framework.manager.factory.AsyncFactory;
|
||||||
import com.ruoyi.project.system.user.domain.User;
|
import com.ruoyi.project.system.user.domain.User;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -18,69 +21,58 @@ import com.ruoyi.project.system.user.domain.User;
|
||||||
*
|
*
|
||||||
* @author ruoyi
|
* @author ruoyi
|
||||||
*/
|
*/
|
||||||
public class LogoutFilter extends org.apache.shiro.web.filter.authc.LogoutFilter
|
public class LogoutFilter extends org.apache.shiro.web.filter.authc.LogoutFilter {
|
||||||
{
|
private static final Logger log = LoggerFactory.getLogger(LogoutFilter.class);
|
||||||
private static final Logger log = LoggerFactory.getLogger(LogoutFilter.class);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 退出后重定向的地址
|
|
||||||
*/
|
|
||||||
private String loginUrl;
|
|
||||||
|
|
||||||
public String getLoginUrl()
|
/**
|
||||||
{
|
* 退出后重定向的地址
|
||||||
return loginUrl;
|
*/
|
||||||
}
|
private String loginUrl;
|
||||||
|
|
||||||
public void setLoginUrl(String loginUrl)
|
public String getLoginUrl() {
|
||||||
{
|
return loginUrl;
|
||||||
this.loginUrl = loginUrl;
|
}
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
public void setLoginUrl(String loginUrl) {
|
||||||
protected boolean preHandle(ServletRequest request, ServletResponse response) throws Exception
|
this.loginUrl = loginUrl;
|
||||||
{
|
}
|
||||||
try
|
|
||||||
{
|
|
||||||
Subject subject = getSubject(request, response);
|
|
||||||
String redirectUrl = getRedirectUrl(request, response, subject);
|
|
||||||
try
|
|
||||||
{
|
|
||||||
User user = ShiroUtils.getUser();
|
|
||||||
if (StringUtils.isNotNull(user))
|
|
||||||
{
|
|
||||||
String loginName = user.getLoginName();
|
|
||||||
// 记录用户退出日志
|
|
||||||
SystemLogUtils.log(loginName, Constants.LOGOUT, MessageUtils.message("user.logout.success"));
|
|
||||||
}
|
|
||||||
// 退出登录
|
|
||||||
subject.logout();
|
|
||||||
}
|
|
||||||
catch (SessionException ise)
|
|
||||||
{
|
|
||||||
log.error("logout fail.", ise);
|
|
||||||
}
|
|
||||||
issueRedirect(request, response, redirectUrl);
|
|
||||||
}
|
|
||||||
catch (Exception e)
|
|
||||||
{
|
|
||||||
log.error("Encountered session exception during logout. This can generally safely be ignored.", e);
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
@Override
|
||||||
* 退出跳转URL
|
protected boolean preHandle(ServletRequest request, ServletResponse response) throws Exception {
|
||||||
*/
|
try {
|
||||||
@Override
|
Subject subject = getSubject(request, response);
|
||||||
protected String getRedirectUrl(ServletRequest request, ServletResponse response, Subject subject)
|
String redirectUrl = getRedirectUrl(request, response, subject);
|
||||||
{
|
try {
|
||||||
String url = getLoginUrl();
|
User user = ShiroUtils.getUser();
|
||||||
if (StringUtils.isNotEmpty(url))
|
if (StringUtils.isNotNull(user)) {
|
||||||
{
|
String loginName = user.getLoginName();
|
||||||
return url;
|
// 记录用户退出日志
|
||||||
}
|
AsyncManager.me().execute(AsyncFactory.recordLogininfor(loginName, Constants.LOGOUT, MessageUtils.message("user.logout.success")));
|
||||||
return super.getRedirectUrl(request, response, subject);
|
// SystemLogUtils.log(loginName, Constants.LOGOUT,
|
||||||
}
|
// MessageUtils.message("user.logout.success"));
|
||||||
|
}
|
||||||
|
// 退出登录
|
||||||
|
subject.logout();
|
||||||
|
} catch (SessionException ise) {
|
||||||
|
log.error("logout fail.", ise);
|
||||||
|
}
|
||||||
|
issueRedirect(request, response, redirectUrl);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("Encountered session exception during logout. This can generally safely be ignored.", e);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 退出跳转URL
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
protected String getRedirectUrl(ServletRequest request, ServletResponse response, Subject subject) {
|
||||||
|
String url = getLoginUrl();
|
||||||
|
if (StringUtils.isNotEmpty(url)) {
|
||||||
|
return url;
|
||||||
|
}
|
||||||
|
return super.getRedirectUrl(request, response, subject);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,202 +1,174 @@
|
||||||
package com.ruoyi.project.monitor.online.domain;
|
package com.ruoyi.project.monitor.online.domain;
|
||||||
|
|
||||||
|
import java.util.Date;
|
||||||
|
|
||||||
import com.ruoyi.common.utils.AddressUtils;
|
import com.ruoyi.common.utils.AddressUtils;
|
||||||
import com.ruoyi.framework.web.domain.BaseEntity;
|
import com.ruoyi.framework.web.domain.BaseEntity;
|
||||||
import com.ruoyi.project.monitor.online.domain.OnlineSession.OnlineStatus;
|
import com.ruoyi.project.monitor.online.domain.OnlineSession.OnlineStatus;
|
||||||
import java.util.Date;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 当前在线会话 sys_user_online
|
* 当前在线会话 sys_user_online
|
||||||
*
|
*
|
||||||
* @author ruoyi
|
* @author ruoyi
|
||||||
*/
|
*/
|
||||||
public class UserOnline extends BaseEntity
|
public class UserOnline extends BaseEntity {
|
||||||
{
|
private static final long serialVersionUID = 1L;
|
||||||
private static final long serialVersionUID = 1L;
|
/** 用户会话id */
|
||||||
/** 用户会话id */
|
private String sessionId;
|
||||||
private String sessionId;
|
|
||||||
|
|
||||||
/** 部门名称 */
|
/** 部门名称 */
|
||||||
private String deptName;
|
private String deptName;
|
||||||
|
|
||||||
/** 登录名称 */
|
/** 登录名称 */
|
||||||
private String loginName;
|
private String loginName;
|
||||||
|
|
||||||
/** 登录IP地址 */
|
/** 登录IP地址 */
|
||||||
private String ipaddr;
|
private String ipaddr;
|
||||||
|
|
||||||
/** 登录地址 */
|
/** 登录地址 */
|
||||||
private String longinLocation;
|
private String longinLocation;
|
||||||
|
|
||||||
/** 浏览器类型 */
|
/** 浏览器类型 */
|
||||||
private String browser;
|
private String browser;
|
||||||
|
|
||||||
/** 操作系统 */
|
/** 操作系统 */
|
||||||
private String os;
|
private String os;
|
||||||
|
|
||||||
/** session创建时间 */
|
/** session创建时间 */
|
||||||
private Date startTimestamp;
|
private Date startTimestamp;
|
||||||
|
|
||||||
/** session最后访问时间 */
|
/** session最后访问时间 */
|
||||||
private Date lastAccessTime;
|
private Date lastAccessTime;
|
||||||
|
|
||||||
/** 超时时间,单位为分钟 */
|
/** 超时时间,单位为分钟 */
|
||||||
private Long expireTime;
|
private Long expireTime;
|
||||||
|
|
||||||
/** 在线状态 */
|
/** 在线状态 */
|
||||||
private OnlineStatus status = OnlineStatus.on_line;
|
private OnlineStatus status = OnlineStatus.on_line;
|
||||||
|
|
||||||
/** 备份的当前用户会话 */
|
/** 备份的当前用户会话 */
|
||||||
private OnlineSession session;
|
private OnlineSession session;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 设置session对象
|
* 设置session对象
|
||||||
*/
|
*/
|
||||||
public static final UserOnline fromOnlineSession(OnlineSession session)
|
@Deprecated
|
||||||
{
|
public static final UserOnline fromOnlineSession(OnlineSession session) {
|
||||||
UserOnline online = new UserOnline();
|
UserOnline online = new UserOnline();
|
||||||
online.setSessionId(String.valueOf(session.getId()));
|
online.setSessionId(String.valueOf(session.getId()));
|
||||||
online.setDeptName(session.getDeptName());
|
online.setDeptName(session.getDeptName());
|
||||||
online.setLoginName(session.getLoginName());
|
online.setLoginName(session.getLoginName());
|
||||||
online.setStartTimestamp(session.getStartTimestamp());
|
online.setStartTimestamp(session.getStartTimestamp());
|
||||||
online.setLastAccessTime(session.getLastAccessTime());
|
online.setLastAccessTime(session.getLastAccessTime());
|
||||||
online.setExpireTime(session.getTimeout());
|
online.setExpireTime(session.getTimeout());
|
||||||
online.setIpaddr(session.getHost());
|
online.setIpaddr(session.getHost());
|
||||||
online.setLonginLocation(AddressUtils.getRealAddressByIP(session.getHost()));
|
online.setLonginLocation(AddressUtils.getRealAddressByIP(session.getHost()));
|
||||||
online.setBrowser(session.getBrowser());
|
online.setBrowser(session.getBrowser());
|
||||||
online.setOs(session.getOs());
|
online.setOs(session.getOs());
|
||||||
online.setStatus(session.getStatus());
|
online.setStatus(session.getStatus());
|
||||||
online.setSession(session);
|
online.setSession(session);
|
||||||
return online;
|
return online;
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getSessionId()
|
public String getSessionId() {
|
||||||
{
|
return sessionId;
|
||||||
return sessionId;
|
}
|
||||||
}
|
|
||||||
|
|
||||||
public void setSessionId(String sessionId)
|
public void setSessionId(String sessionId) {
|
||||||
{
|
this.sessionId = sessionId;
|
||||||
this.sessionId = sessionId;
|
}
|
||||||
}
|
|
||||||
|
|
||||||
public String getDeptName()
|
public String getDeptName() {
|
||||||
{
|
return deptName;
|
||||||
return deptName;
|
}
|
||||||
}
|
|
||||||
|
|
||||||
public void setDeptName(String deptName)
|
public void setDeptName(String deptName) {
|
||||||
{
|
this.deptName = deptName;
|
||||||
this.deptName = deptName;
|
}
|
||||||
}
|
|
||||||
|
|
||||||
public String getLoginName()
|
public String getLoginName() {
|
||||||
{
|
return loginName;
|
||||||
return loginName;
|
}
|
||||||
}
|
|
||||||
|
|
||||||
public void setLoginName(String loginName)
|
public void setLoginName(String loginName) {
|
||||||
{
|
this.loginName = loginName;
|
||||||
this.loginName = loginName;
|
}
|
||||||
}
|
|
||||||
|
|
||||||
public String getIpaddr()
|
public String getIpaddr() {
|
||||||
{
|
return ipaddr;
|
||||||
return ipaddr;
|
}
|
||||||
}
|
|
||||||
|
|
||||||
public void setIpaddr(String ipaddr)
|
public void setIpaddr(String ipaddr) {
|
||||||
{
|
this.ipaddr = ipaddr;
|
||||||
this.ipaddr = ipaddr;
|
}
|
||||||
}
|
|
||||||
|
|
||||||
public String getLonginLocation()
|
public String getLonginLocation() {
|
||||||
{
|
return longinLocation;
|
||||||
return longinLocation;
|
}
|
||||||
}
|
|
||||||
|
|
||||||
public void setLonginLocation(String longinLocation)
|
public void setLonginLocation(String longinLocation) {
|
||||||
{
|
this.longinLocation = longinLocation;
|
||||||
this.longinLocation = longinLocation;
|
}
|
||||||
}
|
|
||||||
|
|
||||||
public String getBrowser()
|
public String getBrowser() {
|
||||||
{
|
return browser;
|
||||||
return browser;
|
}
|
||||||
}
|
|
||||||
|
|
||||||
public void setBrowser(String browser)
|
public void setBrowser(String browser) {
|
||||||
{
|
this.browser = browser;
|
||||||
this.browser = browser;
|
}
|
||||||
}
|
|
||||||
|
|
||||||
public String getOs()
|
public String getOs() {
|
||||||
{
|
return os;
|
||||||
return os;
|
}
|
||||||
}
|
|
||||||
|
|
||||||
public void setOs(String os)
|
public void setOs(String os) {
|
||||||
{
|
this.os = os;
|
||||||
this.os = os;
|
}
|
||||||
}
|
|
||||||
|
|
||||||
public Date getStartTimestamp()
|
public Date getStartTimestamp() {
|
||||||
{
|
return startTimestamp;
|
||||||
return startTimestamp;
|
}
|
||||||
}
|
|
||||||
|
|
||||||
public void setStartTimestamp(Date startTimestamp)
|
public void setStartTimestamp(Date startTimestamp) {
|
||||||
{
|
this.startTimestamp = startTimestamp;
|
||||||
this.startTimestamp = startTimestamp;
|
}
|
||||||
}
|
|
||||||
|
|
||||||
public Date getLastAccessTime()
|
public Date getLastAccessTime() {
|
||||||
{
|
return lastAccessTime;
|
||||||
return lastAccessTime;
|
}
|
||||||
}
|
|
||||||
|
|
||||||
public void setLastAccessTime(Date lastAccessTime)
|
public void setLastAccessTime(Date lastAccessTime) {
|
||||||
{
|
this.lastAccessTime = lastAccessTime;
|
||||||
this.lastAccessTime = lastAccessTime;
|
}
|
||||||
}
|
|
||||||
|
|
||||||
public Long getExpireTime()
|
public Long getExpireTime() {
|
||||||
{
|
return expireTime;
|
||||||
return expireTime;
|
}
|
||||||
}
|
|
||||||
|
|
||||||
public void setExpireTime(Long expireTime)
|
public void setExpireTime(Long expireTime) {
|
||||||
{
|
this.expireTime = expireTime;
|
||||||
this.expireTime = expireTime;
|
}
|
||||||
}
|
|
||||||
|
|
||||||
public OnlineStatus getStatus()
|
public OnlineStatus getStatus() {
|
||||||
{
|
return status;
|
||||||
return status;
|
}
|
||||||
}
|
|
||||||
|
|
||||||
public void setStatus(OnlineStatus status)
|
public void setStatus(OnlineStatus status) {
|
||||||
{
|
this.status = status;
|
||||||
this.status = status;
|
}
|
||||||
}
|
|
||||||
|
|
||||||
public OnlineSession getSession()
|
public OnlineSession getSession() {
|
||||||
{
|
return session;
|
||||||
return session;
|
}
|
||||||
}
|
|
||||||
|
|
||||||
public void setSession(OnlineSession session)
|
public void setSession(OnlineSession session) {
|
||||||
{
|
this.session = session;
|
||||||
this.session = session;
|
}
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String toString()
|
public String toString() {
|
||||||
{
|
return "UserOnline [sessionId=" + sessionId + ", deptName=" + deptName + ", loginName=" + loginName + ", ipaddr=" + ipaddr + ", browser=" + browser + ", os=" + os + ", startTimestamp=" + startTimestamp + ", lastAccessTime=" + lastAccessTime + ", expireTime=" + expireTime + ", status=" + status + ", session=" + session + "]";
|
||||||
return "UserOnline [sessionId=" + sessionId + ", deptName=" + deptName + ", loginName=" + loginName
|
}
|
||||||
+ ", ipaddr=" + ipaddr + ", browser=" + browser + ", os=" + os + ", startTimestamp=" + startTimestamp
|
|
||||||
+ ", lastAccessTime=" + lastAccessTime + ", expireTime=" + expireTime + ", status=" + status
|
|
||||||
+ ", session=" + session + "]";
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue