柏竹 柏竹
首页
后端
前端
  • 应用推荐
关于
友链
  • 分类
  • 标签
  • 归档

柏竹

奋斗柏竹
首页
后端
前端
  • 应用推荐
关于
友链
  • 分类
  • 标签
  • 归档
  • Java基础

  • JavaWeb

  • 拓展技术

  • 框架技术

  • 数据库

  • 数据结构

  • Spring

  • SpringMVC

  • SpringBoot

  • SpringClound

  • Ruoyi-Vue-Plus

    • ruoyi-vue-plus-基础功能
    • ruoyi-vue-plus-权限控制
    • ruoyi-vue-plus-表格操作
    • ruoyi-vue-plus-缓存功能
    • ruoyi-vue-plus-日志功能
      • 登录日志
        • 登录成功
        • 登录失败
        • 登录事件监听
      • 操作日志
        • Log注解
        • LogAspect切面类
        • SysOperLogServiceImpl 操作日志实现类
    • ruoyi-vue-plus-线程相关
    • ruoyi-vue-plus-OSS功能
    • ruoyi-vue-plus-代码生成功能
    • ruoyi-vue-plus-多数据源
    • ruoyi-vue-plus-任务调度
    • ruoyi-vue-plus-监控功能
    • ruoyi-vue-plus-国际化
    • ruoyi-vue-plus-XSS功能
    • ruoyi-vue-plus-防重幂&限流 功能
    • ruoyi-vue-plus-推送功能
    • ruoyi-vue-plus-序列化功能
    • ruoyi-vue-plus-数据加密
    • ruoyi-vue-plus-单元测试
    • ruoyi-vue-plus-前端插件
    • ruoyi-vue-plus-前端工具篇
    • ruoyi-vue-plus-部署篇
    • ruoyi-vue-plus-前端篇
    • ruoyi-vue-plus-后端工具篇
    • ruoyi-vue-plus-框架篇
    • ruoyi-vue-plus-问题解决
  • 后端
  • Ruoyi-Vue-Plus
柏竹
2023-11-14
目录

ruoyi-vue-plus-日志功能

# 登录日志

用户登录可能出现以下情况 , 以及日志记录情况

  • 登录成功 (日志记录)
  • 账号不存在
  • 密码错误 (日志记录)
  • 验证码错误 (日志记录)
  • 超出限制 (日志记录)

根据PC端登录进行分析

点击展开








 






 

 



public String login(String username, String password, String code, String uuid) {
    boolean captchaEnabled = configService.selectCaptchaEnabled();
    // 验证码开关
    if (captchaEnabled) {
        validateCaptcha(username, code, uuid);
    }
    // 框架登录不限制从什么表查询 只要最终构建出 LoginUser 即可
    SysUser user = loadUserByUsername(username);
    checkLogin(LoginType.PASSWORD, username, () -> !BCrypt.checkpw(password, user.getPassword()));
    // 此处可根据登录用户的数据不同 自行创建 loginUser 属性不够用继承扩展就行了
    LoginUser loginUser = buildLoginUser(user);
    // 生成token
    LoginHelper.loginByDevice(loginUser, DeviceType.PC);
	
    // 日志记录
    recordLogininfor(username, Constants.LOGIN_SUCCESS, MessageUtils.message("user.login.success"));
    // 登录用户信息记录
    recordLoginInfo(user.getUserId(), username);
    return StpUtil.getTokenValue();
}

# 登录成功

用户登录成功则进入以下代码段进行日志记录

// 日志记录
recordLogininfor(username, Constants.LOGIN_SUCCESS, MessageUtils.message("user.login.success"));
// 登录用户信息记录
recordLoginInfo(user.getUserId(), username);
  • recordLogininfor() : 记录登录日志信息 (通过事件监听异步记录日志) 点击跳转
  • recordLoginInfo() : 记录用户自身登录信息 (IP , 登录时间等...)
点击展开
private void recordLogininfor(String username, String status, String message) {
    LogininforEvent logininforEvent = new LogininforEvent();
    logininforEvent.setUsername(username);
    logininforEvent.setStatus(status);
    logininforEvent.setMessage(message);
    // 存当前请求信息
    logininforEvent.setRequest(ServletUtils.getRequest());
    SpringUtils.context().publishEvent(logininforEvent);
}

public void recordLoginInfo(Long userId, String username) {
    SysUser sysUser = new SysUser();
    sysUser.setUserId(userId);
    sysUser.setLoginIp(ServletUtils.getClientIP());
    sysUser.setLoginDate(DateUtils.getNowDate());
    sysUser.setUpdateBy(username);
    userMapper.updateById(sysUser);
}

# 登录失败

根据以上状态可将登录不成功的统一归纳为登录失败 , 登录校验主要通过以下

checkLogin(LoginType.PASSWORD, username, () -> !BCrypt.checkpw(password, user.getPassword()));

提示

密码校验采用SaToken的 BCrypt.checkpw() 实现 , 点击文档 (opens new window)

登录校验

点击展开








 










 



 








private void checkLogin(LoginType loginType, String username, Supplier<Boolean> supplier) {
    String errorKey = CacheConstants.PWD_ERR_CNT_KEY + username;
    String loginFail = Constants.LOGIN_FAIL;

    // 获取用户登录错误次数,默认为0 (可自定义限制策略 例如: key + username + ip)
    int errorNumber = ObjectUtil.defaultIfNull(RedisUtils.getCacheObject(errorKey), 0);
    // 锁定时间内登录 则踢出
    if (errorNumber >= maxRetryCount) {
        recordLogininfor(username, loginFail, MessageUtils.message(loginType.getRetryLimitExceed(), maxRetryCount, lockTime));
        throw new UserException(loginType.getRetryLimitExceed(), maxRetryCount, lockTime);
    }

    // 是否登录错误
    if (supplier.get()) {
        // 错误次数递增
        errorNumber++;
        RedisUtils.setCacheObject(errorKey, errorNumber, Duration.ofMinutes(lockTime));
        // 达到规定错误次数 则锁定登录
        if (errorNumber >= maxRetryCount) {
            recordLogininfor(username, loginFail, MessageUtils.message(loginType.getRetryLimitExceed(), maxRetryCount, lockTime));
            throw new UserException(loginType.getRetryLimitExceed(), maxRetryCount, lockTime);
        } else {
            // 未达到规定错误次数
            recordLogininfor(username, loginFail, MessageUtils.message(loginType.getRetryLimitCount(), errorNumber));
            throw new UserException(loginType.getRetryLimitCount(), errorNumber);
        }
    }

    // 登录成功 清空错误次数
    RedisUtils.deleteObject(errorKey);
}

提示

  • loginType 标识登录类型 , 密码登录 / 短信登录 / 邮箱登录 / ...
  • MessageUtils.message() , 国际化消息处理 , 根据消息键 处理

# 登录事件监听

一旦执行了 recordLogininfor()方法 , 会将登录的事件发送并进行日志记录

点击展开
@Async
@EventListener
public void recordLogininfor(LogininforEvent logininforEvent) {
    HttpServletRequest request = logininforEvent.getRequest();
    final UserAgent userAgent = UserAgentUtil.parse(request.getHeader("User-Agent"));
    final String ip = ServletUtils.getClientIP(request);

    String address = AddressUtils.getRealAddressByIP(ip);
    StringBuilder s = new StringBuilder();
    s.append(getBlock(ip));
    s.append(address);
    s.append(getBlock(logininforEvent.getUsername()));
    s.append(getBlock(logininforEvent.getStatus()));
    s.append(getBlock(logininforEvent.getMessage()));
    // 打印信息到日志
    log.info(s.toString(), logininforEvent.getArgs());
    // 获取客户端操作系统
    String os = userAgent.getOs().getName();
    // 获取客户端浏览器
    String browser = userAgent.getBrowser().getName();
    // 封装对象
    SysLogininfor logininfor = new SysLogininfor();
    logininfor.setUserName(logininforEvent.getUsername());
    logininfor.setIpaddr(ip);
    logininfor.setLoginLocation(address);
    logininfor.setBrowser(browser);
    logininfor.setOs(os);
    logininfor.setMsg(logininforEvent.getMessage());
    // 日志状态
    if (StringUtils.equalsAny(logininforEvent.getStatus(), Constants.LOGIN_SUCCESS, Constants.LOGOUT, Constants.REGISTER)) {
        logininfor.setStatus(Constants.SUCCESS);
    } else if (Constants.LOGIN_FAIL.equals(logininforEvent.getStatus())) {
        logininfor.setStatus(Constants.FAIL);
    }
    // 插入数据
    insertLogininfor(logininfor);
}

# 操作日志

操作日志的记录主要通过Spring AOP切面实现 , 根据含有 @Log注解 的方法进行日志记录

运作大体流程

  1. Controller层 @Log注解方法 被 切面类LogAspect拦截

  2. 前置处理 , 方法业务执行前 , 记录当前时间戳

  3. 后置处理 , 方法业务执行后 , 记录方法类型等相关信息

    记录信息 : 租户id , 请求ip , 请求状态 , 请求url , 请求方法 , 接口类方法 , 请求方式 , 请求参数 , ...

  4. 时间戳记录运行过程时长

  5. 发布事件监听 , 异步存储数据库

  6. API获取IP城市地址 , 并封装存储

  7. 写入数据库

涉及信息标识

  • Log 标识日志注解 (Controller层标识接口方法)
  • LogAspect 切面类 (操作日志切面处理)
  • SysOperLogServiceImpl 操作日志实现类 (数据源操作)
  • sys_logininfor 库名 (数据源日志信息)

# Log注解

点击展开
@Target({ElementType.PARAMETER, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Log {
    /**
     * 模块
     */
    String title() default "";

    /**
     * 功能
     */
    BusinessType businessType() default BusinessType.OTHER;

    /**
     * 操作人类别
     */
    OperatorType operatorType() default OperatorType.MANAGE;

    /**
     * 是否保存请求的参数
     */
    boolean isSaveRequestData() default true;

    /**
     * 是否保存响应的参数
     */
    boolean isSaveResponseData() default true;

    /**
     * 排除指定的请求参数
     */
    String[] excludeParamNames() default {};

}
点击展开

# LogAspect切面类

Controller层含有 @Log注解 的方法被执行 , AOP会进行对该方法切面处理 , 分别在 方法调用返回后 或 方法异常 处理

切面不同时段的业务处理 (针对含有@Log注解方法)

  1. @Before前置通知 : 记录当前时间戳
  2. @AfterReturning后置通知 : 记录日志信息
  3. @AfterThrowing异常通知 : 记录异常信息
点击展开
@Slf4j
@Aspect
@Component
public class LogAspect {

    /**
     * 排除敏感属性字段 (自动排除数组字段)
     */
    public static final String[] EXCLUDE_PROPERTIES = { "password", "oldPassword", "newPassword", "confirmPassword" };

    /**
     * 处理完请求后执行
     *
     * @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 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 {

            // *========数据库日志=========*//
            OperLogEvent operLog = new OperLogEvent();
            operLog.setStatus(BusinessStatus.SUCCESS.ordinal());
            // 请求的地址
            String ip = ServletUtils.getClientIP();
            operLog.setOperIp(ip);
            operLog.setOperUrl(StringUtils.substring(ServletUtils.getRequest().getRequestURI(), 0, 255));
            LoginUser loginUser = LoginHelper.getLoginUser();
            operLog.setOperName(loginUser.getUsername());
            operLog.setDeptName(loginUser.getDeptName());

            // 判断异常 , 异常存储信息
            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);
            // 发布事件保存数据库
            SpringUtils.context().publishEvent(operLog);
        } catch (Exception exp) {
            // 记录本地异常日志
            log.error("异常信息:{}", exp.getMessage());
            exp.printStackTrace();
        }
    }

    /**
     * 获取注解中对方法的描述信息 用于Controller层注解
     *
     * @param log     日志
     * @param operLog 操作日志
     * @throws Exception
     */
    public void getControllerMethodDescription(JoinPoint joinPoint, Log log, OperLogEvent 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() && ObjectUtil.isNotNull(jsonResult)) {
            operLog.setJsonResult(StringUtils.substring(JsonUtils.toJsonString(jsonResult), 0, 2000));
        }
    }

    /**
     * 获取请求的参数,放到log中
     *
     * @param operLog 操作日志
     * @throws Exception 异常
     */
    private void setRequestValue(JoinPoint joinPoint, OperLogEvent operLog, String[] excludeParamNames) throws Exception {
        Map<String, String> paramsMap = ServletUtils.getParamMap(ServletUtils.getRequest());
        String requestMethod = operLog.getRequestMethod();
        // 请求参数为空 并且 请求是 PUT 或 POST
        if (MapUtil.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 {
            MapUtil.removeAny(paramsMap, EXCLUDE_PROPERTIES);
            MapUtil.removeAny(paramsMap, excludeParamNames);
            operLog.setOperParam(StringUtils.substring(JsonUtils.toJsonString(paramsMap), 0, 2000));
        }
    }

    /**
     * 参数拼装
     */
    private String argsArrayToString(Object[] paramsArray, String[] excludeParamNames) {
        StringJoiner params = new StringJoiner(" ");
        if (ArrayUtil.isEmpty(paramsArray)) {
            return params.toString();
        }
        for (Object o : paramsArray) {
            if (ObjectUtil.isNotNull(o) && !isFilterObject(o)) {
                String str = JsonUtils.toJsonString(o);
                Dict dict = JsonUtils.parseMap(str);
                if (MapUtil.isNotEmpty(dict)) {
                    MapUtil.removeAny(dict, EXCLUDE_PROPERTIES);
                    MapUtil.removeAny(dict, excludeParamNames);
                    str = JsonUtils.toJsonString(dict);
                }
                params.add(str);
            }
        }
        return params.toString();
    }

    /**
     * 判断是否需要过滤的对象
     *
     * @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.values()) {
                return value instanceof MultipartFile;
            }
        }
        return o instanceof MultipartFile || o instanceof HttpServletRequest || o instanceof HttpServletResponse
            || o instanceof BindingResult;
    }
}

# SysOperLogServiceImpl 操作日志实现类

通过监听实现数据记录

@RequiredArgsConstructor
@Service
public class SysOperLogServiceImpl implements ISysOperLogService {

    private final SysOperLogMapper baseMapper;

    /**
     * 操作日志记录
     *
     * @param operLogEvent 操作日志事件
     */
    @Async
    @EventListener
    public void recordOper(OperLogEvent operLogEvent) {
        SysOperLog operLog = BeanUtil.toBean(operLogEvent, SysOperLog.class);
        // 远程查询操作地点
        operLog.setOperLocation(AddressUtils.getRealAddressByIP(operLog.getOperIp()));
        insertOperlog(operLog);
    }

    /**
     * 新增操作日志
     *
     * @param operLog 操作日志对象
     */
    @Override
    public void insertOperlog(SysOperLog operLog) {
        operLog.setOperTime(new Date());
        baseMapper.insert(operLog);
    }

    // 省略...
    
}
#ruoyi-vue-plus

← ruoyi-vue-plus-缓存功能 ruoyi-vue-plus-线程相关→

最近更新
01
HTTPS自动续签
10-21
02
博客搭建-简化版(脚本)
10-20
03
ruoyi-vue-plus-部署篇
07-13
更多文章>
Theme by Vdoing | Copyright © 2019-2024 | 桂ICP备2022009417号-1
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式