在项目中,记录用户登录日志是监控用户行为、分析系统安全性和排查问题的重要手段,登录日志通常包括以下关键信息:
1. 用户信息:记录登录用户的用户名或用户 ID。
2. 登录时间:记录用户登录的具体时间。
3. 登录状态:记录登录是否成功。
4. IP 地址:记录用户登录时的 IP 地址。
5. 设备信息:记录用户使用的设备或浏览器信息。
AOP优势
在项目中使用 AOP
(面向切面编程)注解实现登录日志具有以下优势:
1. 代码解耦:将日志记录逻辑与业务逻辑分离,避免代码重复,提升代码可读性和可维护性。
2. 灵活性与扩展性:通过注解可以灵活地控制哪些方法需要记录日志,便于扩展和修改日志记录逻辑。
3. 非侵入性:无需修改现有业务代码,只需在方法上添加注解即可实现日志记录。
4. 集中管理:日志记录逻辑集中在切面中,便于统一管理和维护。
5. 提高开发效率:通过注解方式快速实现日志功能,减少重复代码编写。
定义注解
在 xiaomayi-common/xiaomayi-logger
模块中定义的登录日志的 AOP
切面 LoginLog
文件,内容如下:
js
package com.xiaomayi.logger.annotation;
import com.xiaomayi.logger.enums.LoginType;
import com.xiaomayi.logger.enums.RequestSource;
import java.lang.annotation.*;
/**
* <p>
* 登录日志注解
* </p>
*
* @author 小蚂蚁云团队
* @since 2024-05-30
*/
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
public @interface LoginLog {
/**
* 日志标题
*
* @return 返回结果
*/
String title() default "未知";
/**
* 请求类型
*
* @return 返回结果
*/
LoginType type() default LoginType.LOGIN;
/**
* 请求来源
*
* @return 返回结果
*/
RequestSource source() default RequestSource.SYSTEM;
/**
* 需要排序的敏感字段
*
* @return 返回结果
*/
String[] exclude() default {};
}
注解实现
js
package com.xiaomayi.logger.aspect;
import com.alibaba.fastjson2.JSON;
import com.xiaomayi.core.enums.HttpMethod;
import com.xiaomayi.core.utils.IpAddressUtils;
import com.xiaomayi.core.utils.IpUtils;
import com.xiaomayi.core.utils.ServletUtils;
import com.xiaomayi.core.utils.StringUtils;
import com.xiaomayi.logger.annotation.LoginLog;
import com.xiaomayi.logger.enums.RequestSource;
import com.xiaomayi.logger.enums.RequestStatus;
import com.xiaomayi.logger.filter.ExcludePropertyPreFilter;
import com.xiaomayi.logger.vo.LoginLogVO;
import eu.bitwalker.useragentutils.UserAgent;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.ArrayUtils;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.core.NamedThreadLocal;
import org.springframework.stereotype.Component;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
@Slf4j
@Aspect
@Component
public abstract class LoginAspect {
/**
* 敏感属性过滤
*/
public static final String[] EXCLUDE_PROPERTIES = {"password"};
/**
* 计算耗时线程
*/
ThreadLocal<Long> TIME_THREADLOCAL = new NamedThreadLocal<Long>("LoginTime");
/**
* 扫描切入点注解
*/
@Pointcut("@within(com.xiaomayi.logger.annotation.LoginLog) " +
"|| @annotation(com.xiaomayi.logger.annotation.LoginLog) ")
public void doPointCut() {
}
/**
* 处理请求前执行
*
* @param loginLog 请求日志
*/
@Before("doPointCut() && @annotation(loginLog)")
public void doBefore(LoginLog loginLog) {
log.info("请求AOP日志处理开始");
// 设置请求开始时间
TIME_THREADLOCAL.set(System.currentTimeMillis());
}
/**
* 处理完请求后执行
*/
@After("doPointCut()")
public void doAfter() {
log.info("请求AOP日志处理结束");
}
/**
* 处理完请求后执行
*
* @param point 切点
* @param loginLog 请求日志
* @param jsonResult 响应结果
*/
@AfterReturning(pointcut = "@annotation(loginLog)", returning = "jsonResult")
public void doAfterReturning(JoinPoint point, LoginLog loginLog, Object jsonResult) {
log.info("请求AOP日志结果已响应");
saveLog(point, loginLog, jsonResult, null);
}
/**
* 拦截异常处理
*
* @param point 切点
* @param loginLog 请求日志
* @param exception 异常处理
*/
@AfterThrowing(value = "@annotation(loginLog)", throwing = "exception")
public void doAfterThrowing(JoinPoint point, LoginLog loginLog, Exception exception) {
log.info("请求AOP日志处理异常");
saveLog(point, loginLog, null, exception);
}
/**
* 处理请求日志
*
* @param point 切点
* @param annotation 日志注解
* @param jsonResult 响应结果
* @param exception 异常处理
*/
private void saveLog(JoinPoint point, LoginLog annotation, Object jsonResult, Exception exception) {
try {
// 获取日志标题
String title = annotation.title();
// 实例化请求日志VO
LoginLogVO loginLogVO = new LoginLogVO();
// 日志标题
loginLogVO.setTitle(title);
// 请求类型
loginLogVO.setType(RequestStatus.SUCCESS.ordinal());
// 请求来源
loginLogVO.setSource(RequestSource.SYSTEM.ordinal());
// 客户端IP
String ip = IpUtils.getIpAddr();
loginLogVO.setIp(ip);
// 根据IP获取真实地址
String address = IpAddressUtils.getRealAddress(ip);
loginLogVO.setLocation(address);
// 请求地址
String url = StringUtils.substring(ServletUtils.getRequest().getRequestURI(), 0, 255);
loginLogVO.setUrl(url);
// 浏览器解析
UserAgent userAgent = UserAgent.parseUserAgentString(ServletUtils.getRequest().getHeader("User-Agent"));
// 获取客户端操作系统
String os = userAgent.getOperatingSystem().getName();
loginLogVO.setOs(os);
// 获取客户端浏览器
String browser = userAgent.getBrowser().getName();
loginLogVO.setBrowser(browser);
// 设置方法名称
String className = point.getTarget().getClass().getName();
String methodName = point.getSignature().getName();
// 设置方法名称
loginLogVO.setMethod(className + "." + methodName + "()");
// 设置请求方式
loginLogVO.setRequestMethod(ServletUtils.getRequest().getMethod());
// 处理设置注解上的参数
setRequestParam(point, loginLogVO, annotation.exclude(), jsonResult);
// 设置请求耗时
loginLogVO.setConsumeTime(System.currentTimeMillis() - TIME_THREADLOCAL.get());
// 请求日志状态
if (exception != null) {
// 异常处理
loginLogVO.setStatus(RequestStatus.FAIL.ordinal());
loginLogVO.setError(StringUtils.substring(exception.getMessage(), 0, 2000));
} else {
loginLogVO.setStatus(0);
loginLogVO.setError(null);
}
// 获取登录名称
String username = JSON.parseObject(loginLogVO.getParam()).get("username").toString();
loginLogVO.setUsername(username);
// 调用抽象类保存请求地址
if (loginLog(loginLogVO)) {
log.info("请求AOP日志处理结束");
}
} catch (Exception e) {
log.error("异常信息:{}", e.getMessage());
} finally {
TIME_THREADLOCAL.remove();
}
}
/**
* 设置请求参数
*
* @param point 切点
* @param loginLogVO 请求日志VO
* @param excludeParamNames 敏感属性字段
* @param jsonResult 返回结果
*/
private void setRequestParam(JoinPoint point, LoginLogVO loginLogVO, String[] excludeParamNames, Object jsonResult) {
// 获取所有请求参数
Map<?, ?> paramsMap = ServletUtils.getParamMap(ServletUtils.getRequest());
// 获取请求方式
String requestMethod = loginLogVO.getRequestMethod();
// 请求参数判空
if (StringUtils.isEmpty(paramsMap) && (HttpMethod.PUT.name().equals(requestMethod) || HttpMethod.POST.name().equals(requestMethod))) {
// 格式化请求参数
String params = getJSONParam(point.getArgs(), excludeParamNames);
// 设置请求日志参数
loginLogVO.setParam(StringUtils.substring(params, 0, 2000));
} else {
// 设置请求日志参数
loginLogVO.setParam(StringUtils.substring(JSON.toJSONString(paramsMap, excludePropertyPreFilter(excludeParamNames)), 0, 2000));
}
// 设置网络响应
if (StringUtils.isNotNull(jsonResult)) {
loginLogVO.setResult(StringUtils.substring(JSON.toJSONString(jsonResult), 0, 2000));
}
}
/**
* 请求参数转JSON字符串
*
* @param objects 参数对象
* @param excludeParamNames 敏感属性字段名称集合
* @return 返回结果
*/
private String getJSONParam(Object[] objects, String[] excludeParamNames) {
// 参数对象判空
if (StringUtils.isEmpty(objects)) {
return "";
}
// 实例化参数列表
List<String> paramList = new ArrayList<>();
// 遍历属性
for (Object obj : objects) {
// 对象判空
if (StringUtils.isNull(obj)) {
continue;
}
// 对象转JSON,排除敏感属性字段
String jsonObj = JSON.toJSONString(obj, excludePropertyPreFilter(excludeParamNames));
if (StringUtils.isEmpty(jsonObj)) {
continue;
}
// 加入列表
paramList.add(jsonObj);
}
// 参数列表转拼接字符串
String params = StringUtils.join(paramList.toArray(), " ");
// 返回结果
return params;
}
/**
* 敏感属性过滤
*
* @param excludeParamNames 参数名称集合
* @return 返回结果
*/
public ExcludePropertyPreFilter excludePropertyPreFilter(String[] excludeParamNames) {
return new ExcludePropertyPreFilter().addExcludes(ArrayUtils.addAll(EXCLUDE_PROPERTIES, excludeParamNames));
}
/**
* 请求日志抽象类
*
* @param loginLogVO 请求日志VO
* @return 返回结果
*/
public abstract boolean loginLog(LoginLogVO loginLogVO);
}
添加依赖
在 pom.xml
配置文件中引入以下依赖:
js
<!-- 依赖声明 -->
<dependencies>
<!-- 安全认证依赖模块 -->
<dependency>
<groupId>com.xiaomayi</groupId>
<artifactId>xiaomayi-security</artifactId>
</dependency>
<!-- 日志依赖模块 -->
<dependency>
<groupId>com.xiaomayi</groupId>
<artifactId>xiaomayi-logger</artifactId>
</dependency>
</dependencies>
注解使用
在需要记录登录日志的方法上添加以下注解:
js
@LoginLog(title = "用户登录", type = LoginType.LOGIN)
使用案例:
js
package com.xiaomayi.admin.controller;
import com.xiaomayi.core.utils.R;
import com.xiaomayi.logger.annotation.LoginLog;
import com.xiaomayi.logger.annotation.RequestLog;
import com.xiaomayi.logger.enums.LoginType;
import com.xiaomayi.logger.enums.RequestType;
import com.xiaomayi.system.dto.LoginDTO;
import com.xiaomayi.admin.service.LoginService;
import com.xiaomayi.system.dto.RegisterDTO;
import com.xiaomayi.tenant.annotation.TenantIgnore;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.AllArgsConstructor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
/**
* <p>
* 用户表 前端控制器
* </p>
*
* @author 小蚂蚁云团队
* @since 2024-05-21
*/
@RestController
@Tag(name = "登录", description = "登录")
@AllArgsConstructor
public class LoginController {
@Autowired
private LoginService loginService;
/**
* 用户登录
*
* @param loginDTO 参数
* @return 返回结果
*/
@TenantIgnore
@Operation(summary = "用户登录", description = "用户登录")
@LoginLog(title = "用户登录", type = LoginType.LOGIN)
@PostMapping("/login")
public R login(@RequestBody @Validated LoginDTO loginDTO) {
return loginService.login(loginDTO);
}
}
总结
通过 AOP
注解实现登录日志,能够显著提升代码质量和开发效率。