33 登录日志
系统管理 > 日志管理 > 登录日志
1 应用场景
- 统计用户的活跃度
- 用户错误输入密码多少次
2 后台实现
(1)SysLoginController#login:登录
/*** 登录方法* * @param loginBody 登录信息* @return 结果*/@PostMapping("/login")public AjaxResult login(@RequestBody LoginBody loginBody){AjaxResult ajax = AjaxResult.success();// 生成令牌String token = loginService.login(loginBody.getUsername(), loginBody.getPassword(), loginBody.getCode(),loginBody.getUuid());ajax.put(Constants.TOKEN, token);return ajax;}
(2)SysLoginService#login:登录校验,记录登录日志
/*** 登录验证*/public SysUser login(String loginCode, String code, boolean ifRemember)public String login(String username, String password, String code, String uuid){validateCaptcha(username, code, uuid);loginPreCheck(username, password);Authentication authentication = null;try{UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(username, password);AuthenticationContextHolder.setContext(authenticationToken);authentication = authenticationManager.authenticate(authenticationToken);}catch (Exception e){/*** 验证码错误,记录*/if (e instanceof BadCredentialsException){AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, MessageUtils.message("user.password.not.match")));throw new UserPasswordNotMatchException();}/*** 其它错误,记录*/else{AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, e.getMessage()));throw new ServiceException(e.getMessage());}}finally{AuthenticationContextHolder.clearContext();}AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_SUCCESS, MessageUtils.message("user.login.success")));LoginUser loginUser = (LoginUser) authentication.getPrincipal();recordLoginInfo(loginUser.getUserId());return tokenService.createToken(loginUser);}
(3)AsyncManager#execute:异步任务管理器,定时执行任务。为什么要异常记录登录日志呢?因为如果登录的时候数据库卡了,那么页面就会转圈圈,可能会造成一直等待的情况,特别在大用户的系统中时常会出现。而使用了异步以后,就可以不影响登录的主业务逻辑了。
/*** 操作延迟10毫秒*/private final int OPERATE_DELAY_TIME = 10;/*** 异步操作任务调度线程池*/private ScheduledExecutorService executor = SpringUtils.getBean("scheduledExecutorService");/*** 执行任务*/public void execute(TimerTask task){/*** 调用定时任务*/executor.schedule(task, OPERATE_DELAY_TIME, TimeUnit.MILLISECONDS);}
(4)ThreadPoolConfig#scheduledExecutorService:配置线程池
/*** 线程池配置**/
@Configuration
public class ThreadPoolConfig
{/*** 执行周期性或定时任务*/@Bean(name = "scheduledExecutorService")protected ScheduledExecutorService scheduledExecutorService(){return new ScheduledThreadPoolExecutor(corePoolSize,new BasicThreadFactory.Builder().namingPattern("schedule-pool-%d").daemon(true).build(),new ThreadPoolExecutor.CallerRunsPolicy()){@Overrideprotected void afterExecute(Runnable r, Throwable t){super.afterExecute(r, t);Threads.printException(r, t);}};}
}
(5)AsyncFactory#recordLogininfor:记录登录信息
/*** 记录登录信息* * @param username 用户名 字符器* @param status 状态 常量* @param message 消息 字符串* @param args 列表* @return 任务task*/public static TimerTask recordLogininfor(final String username, final String status, final String message,final Object... args){/*** 通过用户代理userAgent,可以拿到ip、浏览器、操作系统相关一些信息*/final UserAgent userAgent = UserAgent.parseUserAgentString(ServletUtils.getRequest().getHeader("User-Agent"));/*** 这个IP的话必须放在这个线程的外面.*/final String ip = IpUtils.getIpAddr();/*** 定时任务业务逻辑*/return new TimerTask(){@Overridepublic void run(){/*** 机器的ip地址*/String address = AddressUtils.getRealAddressByIP(ip);StringBuilder s = new StringBuilder();s.append(LogUtils.getBlock(ip));s.append(address);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();// 封装对象SysLogininfor logininfor = new SysLogininfor();logininfor.setUserName(username);logininfor.setIpaddr(ip);logininfor.setLoginLocation(address);logininfor.setBrowser(browser);logininfor.setOs(os);logininfor.setMsg(message);// 日志状态if (StringUtils.equalsAny(status, Constants.LOGIN_SUCCESS, Constants.LOGOUT, Constants.REGISTER)){logininfor.setStatus(Constants.SUCCESS);}else if (Constants.LOGIN_FAIL.equals(status)){logininfor.setStatus(Constants.FAIL);}// 插入数据SpringUtils.getBean(ISysLogininforService.class).insertLogininfor(logininfor);}};}
(6)Constants:登录状态常量
/*** 登录成功*/public static final String LOGIN_SUCCESS = "Success";/*** 注销*/public static final String LOGOUT = "Logout";/*** 登录失败*/public static final String LOGIN_FAIL = "Error";
(7)MessageUtils#message:读取i18n资源文件登录信息,如" 登录成功 "、" 登录失败 "
/*** 获取i18n资源文件* * @author ruoyi*/
public class MessageUtils
{/*** 根据消息键和参数 获取消息 委托给spring messageSource** @param code 消息键* @param args 参数* @return 获取国际化翻译值*/public static String message(String code, Object... args){MessageSource messageSource = SpringUtils.getBean(MessageSource.class);return messageSource.getMessage(code, args, LocaleContextHolder.getLocale());}
}
(8)messages.properties:i18n资源文件
#错误消息
not.null=* 必须填写
user.jcaptcha.error=验证码错误
user.jcaptcha.expire=验证码已失效
user.not.exists=用户不存在/密码错误
user.password.not.match=用户不存在/密码错误
user.password.retry.limit.count=密码输入错误{0}次
user.password.retry.limit.exceed=密码输入错误{0}次,帐户锁定{1}分钟
user.password.delete=对不起,您的账号已被删除
user.blocked=用户已封禁,请联系管理员
role.blocked=角色已封禁,请联系管理员
login.blocked=很遗憾,访问IP已被列入系统黑名单
user.logout.success=退出成功length.not.valid=长度必须在{min}到{max}个字符之间user.username.not.valid=* 2到20个汉字、字母、数字或下划线组成,且必须以非数字开头
user.password.not.valid=* 5-50个字符user.email.not.valid=邮箱格式错误
user.mobile.phone.number.not.valid=手机号格式错误
user.login.success=登录成功
user.register.success=注册成功
user.notfound=请重新登录
user.forcelogout=管理员强制退出,请重新登录
user.unknown.error=未知错误,请重新登录##文件上传消息
upload.exceed.maxSize=上传的文件大小超出限制的文件大小!<br/>允许的文件最大大小是:{0}MB!
upload.filename.exceed.length=上传的文件名最长{0}个字符##权限
no.permission=您没有数据的权限,请联系管理员添加权限 [{0}]
no.create.permission=您没有创建数据的权限,请联系管理员添加权限 [{0}]
no.update.permission=您没有修改数据的权限,请联系管理员添加权限 [{0}]
no.delete.permission=您没有删除数据的权限,请联系管理员添加权限 [{0}]
no.export.permission=您没有导出数据的权限,请联系管理员添加权限 [{0}]
no.view.permission=您没有查看数据的权限,请联系管理员添加权限 [{0}]
(9)退出时也记录: LogoutSuccessHandlerImpl#onLogoutSuccess
/*** 自定义退出处理类 返回成功*/
@Configuration
public class LogoutSuccessHandlerImpl implements LogoutSuccessHandler
{@Autowiredprivate TokenService tokenService;/*** 退出处理*/@Overridepublic void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication)throws IOException, ServletException{LoginUser loginUser = tokenService.getLoginUser(request);if (StringUtils.isNotNull(loginUser)){String userName = loginUser.getUsername();// 删除用户缓存记录tokenService.delLoginUser(loginUser.getToken());// 记录用户退出日志AsyncManager.me().execute(AsyncFactory.recordLogininfor(userName, Constants.LOGOUT, "退出成功"));}ServletUtils.renderString(response, JSON.toJSONString(AjaxResult.success("退出成功")));}
}
34 操作日志详解
系统管理 > 日志管理 > 操作日志
1 应用场景
-
某个模块CRUD的情况。比如说,用户模块,对某个用户的CURD。
-
程序员定位问题
2 后台手册 | RuoYi | 系统日志
(1)使用方法
在实际开发中,对于某些关键业务,我们通常需要记录该操作的内容,一个操作调一次记录方法,每次还得去收集参数等等,会造成大量代码重复。 我们希望代码中只有业务相关的操作,在项目中使用注解来完成此项功能。
在需要被记录日志的controller
方法上添加@Log
注解,使用方法如下:
@Log(title = "用户管理", businessType = BusinessType.INSERT)
public AjaxResult addSave(...)
{return success(...);
}
(2)注解参数说明
参数 | 类型 | 默认值 | 描述 |
---|---|---|---|
title | String | 空 | 操作模块 |
businessType | BusinessType | OTHER | 操作功能(OTHER 其他、INSERT 新增、UPDATE 修改、DELETE 删除、GRANT 授权、EXPORT 导出、IMPORT 导入、FORCE 强退、GENCODE 生成代码、CLEAN 清空数据) |
operatorType | OperatorType | MANAGE | 操作人类别(OTHER 其他、MANAGE 后台用户、MOBILE 手机端用户) |
isSaveRequestData | boolean | true | 是否保存请求的参数 |
isSaveResponseData | boolean | true | 是否保存响应的参数 |
excludeParamNames | String[] | {} | 排除指定的请求参 |
(3)自定义操作功能
1、在BusinessType
中新增业务操作类型如:
/*** 测试*/
TEST,
2、在sys_dict_data
字典数据表中初始化操作业务类型
insert into sys_dict_data values(25, 10, '测试', '10', 'sys_oper_type', '', 'primary', 'N', '0', 'admin', '2018-03-16 11-33-00', 'ry', '2018-03-16 11-33-00', '测试操作');
3、在Controller
中使用注解
@Log(title = "测试标题", businessType = BusinessType.TEST)
public AjaxResult test(...)
{return success(...);
}
操作日志记录逻辑实现代码LogAspect.java
登录系统(系统管理-操作日志)可以查询操作日志列表和详细信息。
35 操作日志实现
1 sys_dict_data
字典数据表中初始化操作业务类型
2 Log:自定义操作日志记录注解
/*** 自定义操作日志记录注解**/
@Target({ ElementType.PARAMETER, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Log
{/*** 模块*/public String title() default "";/*** 功能*/public BusinessType businessType() default BusinessType.OTHER;/*** 操作人类别*/public OperatorType operatorType() default OperatorType.MANAGE;/*** 是否保存请求的参数* 方便开发人员对代码进行调试*/public boolean isSaveRequestData() default true;/*** 是否保存响应的参数*/public boolean isSaveResponseData() default true;/*** 排除指定的请求参数*/public String[] excludeParamNames() default {};
}
3 BusinessType:业务操作类型枚举
/*** 业务操作类型*/
public enum BusinessType
{/*** 其它*/OTHER,/*** 新增*/INSERT,/*** 修改*/UPDATE,/*** 删除*/DELETE,/*** 授权*/GRANT,/*** 导出*/EXPORT,/*** 导入*/IMPORT,/*** 强退*/FORCE,/*** 生成代码*/GENCODE,/*** 清空数据*/CLEAN,
}
4 OperatorType:操作人类别枚举类
/*** 操作人类别*/
public enum OperatorType
{/*** 其它*/OTHER,/*** 后台用户*/MANAGE,/*** 手机端用户*/MOBILE
}
5 LogAspect:记录操作日志spring切面
/*** 操作日志记录处理*/
@Aspect // 切面
@Component // 注入
public class LogAspect
{private static final Logger log = LoggerFactory.getLogger(LogAspect.class);/** 排除敏感属性字段 */public static final String[] EXCLUDE_PROPERTIES = { "password", "oldPassword", "newPassword", "confirmPassword" };/** 计算操作消耗时间 */private static final ThreadLocal<Long> TIME_THREADLOCAL = new NamedThreadLocal<Long>("Cost Time");/*** 处理请求前执行*/@Before(value = "@annotation(controllerLog)")public void boBefore(JoinPoint joinPoint, Log controllerLog){TIME_THREADLOCAL.set(System.currentTimeMillis());}/*** 处理完请求后执行** @param joinPoint 切点*/@AfterReturning(pointcut = "@annotation(controllerLog)", returning = "jsonResult")public void doAfterReturning(JoinPoint joinPoint, Log controllerLog, Object jsonResult){handleLog(joinPoint, controllerLog, null, jsonResult);}/*** 拦截异常操作* * @param joinPoint 切点* @param controllerLog 注解@Log* @param e 异常*/@AfterThrowing(value = "@annotation(controllerLog)", throwing = "e")public void doAfterThrowing(JoinPoint joinPoint, Log controllerLog, Exception e){handleLog(joinPoint, controllerLog, e, null);}protected void handleLog(final JoinPoint joinPoint, Log controllerLog, final Exception e, Object jsonResult){try{// 获取当前的用户LoginUser loginUser = SecurityUtils.getLoginUser();// *========数据库日志=========*//SysOperLog operLog = new SysOperLog();operLog.setStatus(BusinessStatus.SUCCESS.ordinal());// 请求的地址String ip = IpUtils.getIpAddr();operLog.setOperIp(ip);operLog.setOperUrl(StringUtils.substring(ServletUtils.getRequest().getRequestURI(), 0, 255));if (loginUser != null){operLog.setOperName(loginUser.getUsername());}/*** 如果有异常的话*/if (e != null){/*** 状态:异常*/operLog.setStatus(BusinessStatus.FAIL.ordinal());/*** 异常信息*/operLog.setErrorMsg(StringUtils.substring(e.getMessage(), 0, 2000));}// 设置方法名称String className = joinPoint.getTarget().getClass().getName();String methodName = joinPoint.getSignature().getName();operLog.setMethod(className + "." + methodName + "()");// 设置请求方式operLog.setRequestMethod(ServletUtils.getRequest().getMethod());// 处理设置注解上的参数getControllerMethodDescription(joinPoint, controllerLog, operLog, jsonResult);// 设置消耗时间operLog.setCostTime(System.currentTimeMillis() - TIME_THREADLOCAL.get());// 保存数据库// 异步AsyncManager.me().execute(AsyncFactory.recordOper(operLog));}catch (Exception exp){// 记录本地异常日志log.error("异常信息:{}", exp.getMessage());exp.printStackTrace();}finally{TIME_THREADLOCAL.remove();}}/*** 获取注解中对方法的描述信息 用于Controller层注解* * @param log 日志* @param operLog 操作日志* @throws Exception*/public void getControllerMethodDescription(JoinPoint joinPoint, Log log, SysOperLog operLog, Object jsonResult) throws Exception{// 设置action动作operLog.setBusinessType(log.businessType().ordinal());// 设置标题operLog.setTitle(log.title());// 设置操作人类别operLog.setOperatorType(log.operatorType().ordinal());// 是否需要保存request,参数和值if (log.isSaveRequestData()){// 获取参数的信息,传入到数据库中。setRequestValue(joinPoint, operLog, log.excludeParamNames());}// 是否需要保存response,参数和值if (log.isSaveResponseData() && StringUtils.isNotNull(jsonResult)){operLog.setJsonResult(StringUtils.substring(JSON.toJSONString(jsonResult), 0, 2000));}}/*** 获取请求的参数,放到log中* * @param operLog 操作日志* @throws Exception 异常*/private void setRequestValue(JoinPoint joinPoint, SysOperLog operLog, String[] excludeParamNames) throws Exception{Map<?, ?> paramsMap = ServletUtils.getParamMap(ServletUtils.getRequest());String requestMethod = operLog.getRequestMethod();if (StringUtils.isEmpty(paramsMap)&& (HttpMethod.PUT.name().equals(requestMethod) || HttpMethod.POST.name().equals(requestMethod))){String params = argsArrayToString(joinPoint.getArgs(), excludeParamNames);operLog.setOperParam(StringUtils.substring(params, 0, 2000));}else{operLog.setOperParam(StringUtils.substring(JSON.toJSONString(paramsMap, excludePropertyPreFilter(excludeParamNames)), 0, 2000));}}/*** 参数拼装*/private String argsArrayToString(Object[] paramsArray, String[] excludeParamNames){String params = "";if (paramsArray != null && paramsArray.length > 0){for (Object o : paramsArray){if (StringUtils.isNotNull(o) && !isFilterObject(o)){try{String jsonObj = JSON.toJSONString(o, excludePropertyPreFilter(excludeParamNames));params += jsonObj.toString() + " ";}catch (Exception e){}}}}return params.trim();}/*** 忽略敏感属性*/public PropertyPreExcludeFilter excludePropertyPreFilter(String[] excludeParamNames){return new PropertyPreExcludeFilter().addExcludes(ArrayUtils.addAll(EXCLUDE_PROPERTIES, excludeParamNames));}/*** 判断是否需要过滤的对象。* * @param o 对象信息。* @return 如果是需要过滤的对象,则返回true;否则返回false。*/@SuppressWarnings("rawtypes")public boolean isFilterObject(final Object o){Class<?> clazz = o.getClass();if (clazz.isArray()){return clazz.getComponentType().isAssignableFrom(MultipartFile.class);}else if (Collection.class.isAssignableFrom(clazz)){Collection collection = (Collection) o;for (Object value : collection){return value instanceof MultipartFile;}}else if (Map.class.isAssignableFrom(clazz)){Map map = (Map) o;for (Object value : map.entrySet()){Map.Entry entry = (Map.Entry) value;return entry.getValue() instanceof MultipartFile;}}return o instanceof MultipartFile || o instanceof HttpServletRequest || o instanceof HttpServletResponse|| o instanceof BindingResult;}
}