本文目录导读:

实现登录日志是Java应用中的常见需求,主要有几种典型的实现方式,下面从基础实现到生产级方案进行说明。
基础实现:使用AOP + 数据库
这是最常用且标准的方案,通过Spring AOP拦截登录操作,记录日志到数据库。
1 数据库表设计
CREATE TABLE sys_login_log (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
username VARCHAR(50) NOT NULL COMMENT '用户名',
ip VARCHAR(50) COMMENT '登录IP',
location VARCHAR(100) COMMENT '登录地点',
browser VARCHAR(100) COMMENT '浏览器',
os VARCHAR(100) COMMENT '操作系统',
status TINYINT NOT NULL COMMENT '状态:1成功 0失败',
message VARCHAR(255) COMMENT '提示消息',
login_time DATETIME NOT NULL COMMENT '登录时间',
created_time DATETIME DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
2 实体类
@Data
@TableName("sys_login_log")
public class LoginLog {
private Long id;
private String username;
private String ip;
private String location;
private String browser;
private String os;
private Integer status;
private String message;
private LocalDateTime loginTime;
private LocalDateTime createdTime;
}
3 自定义注解
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface LoginLog {
String value() default "";
}
4 AOP切面实现
@Aspect
@Component
@Slf4j
public class LoginLogAspect {
@Autowired
private LoginLogService loginLogService;
@Around("@annotation(loginLogAnnotation)")
public Object around(ProceedingJoinPoint point, LoginLog loginLogAnnotation) throws Throwable {
// 获取请求上下文
HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder
.getRequestAttributes()).getRequest();
// 构建日志对象
LoginLog loginLog = new LoginLog();
loginLog.setLoginTime(LocalDateTime.now());
loginLog.setIp(getIpAddress(request));
loginLog.setBrowser(getBrowser(request));
loginLog.setOs(getOperatingSystem(request));
// 获取用户名(从请求参数中获取)
Object[] args = point.getArgs();
if (args.length > 0) {
loginLog.setUsername(String.valueOf(args[0]));
}
try {
Object result = point.proceed();
// 登录成功
loginLog.setStatus(1);
loginLog.setMessage("登录成功");
return result;
} catch (Exception e) {
// 登录失败
loginLog.setStatus(0);
loginLog.setMessage(e.getMessage());
throw e;
} finally {
// 异步保存日志
loginLogService.save(loginLog);
}
}
/**
* 获取客户端IP
*/
private String getIpAddress(HttpServletRequest request) {
String ip = request.getHeader("X-Forwarded-For");
if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("Proxy-Client-IP");
}
if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("WL-Proxy-Client-IP");
}
if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
ip = request.getRemoteAddr();
}
return ip;
}
/**
* 获取浏览器信息
*/
private String getBrowser(HttpServletRequest request) {
String userAgent = request.getHeader("User-Agent");
// 使用UserAgentUtils解析(需要引入依赖)
return "Chrome 120.0"; // 简化实现
}
/**
* 获取操作系统
*/
private String getOperatingSystem(HttpServletRequest request) {
String userAgent = request.getHeader("User-Agent");
return "Windows 10"; // 简化实现
}
}
5 登录Controller使用
@RestController
public class LoginController {
@PostMapping("/login")
@LoginLog // 使用自定义注解
public Result login(@RequestParam String username, @RequestParam String password) {
// 登录逻辑
if ("admin".equals(username) && "123456".equals(password)) {
return Result.success("登录成功");
} else {
throw new BusinessException("用户名或密码错误");
}
}
}
使用Spring Security + 事件监听
如果项目使用Spring Security,可以通过监听AuthenticationSuccessEvent和AuthenticationFailureBadCredentialsEvent事件来实现。
1 事件监听器
@Component
@Slf4j
public class AuthenticationEventListener {
@Autowired
private LoginLogService loginLogService;
@EventListener
public void onSuccess(AuthenticationSuccessEvent event) {
UserDetails userDetails = (UserDetails) event.getAuthentication().getPrincipal();
LoginLog loginLog = buildLoginLog(userDetails.getUsername(), 1, "登录成功");
loginLogService.save(loginLog);
}
@EventListener
public void onFailure(AuthenticationFailureBadCredentialsEvent event) {
String username = (String) event.getAuthentication().getPrincipal();
LoginLog loginLog = buildLoginLog(username, 0, "用户名或密码错误");
loginLogService.save(loginLog);
}
private LoginLog buildLoginLog(String username, Integer status, String message) {
LoginLog loginLog = new LoginLog();
loginLog.setUsername(username);
loginLog.setStatus(status);
loginLog.setMessage(message);
loginLog.setLoginTime(LocalDateTime.now());
// 获取IP等信息
return loginLog;
}
}
高级功能:登录地点解析
使用IP库解析登录地点(需要引入IP库):
<dependency>
<groupId>org.lionsoul</groupId>
<artifactId>ip2region</artifactId>
<version>2.7.0</version>
</dependency>
@Component
public class IpLocationUtils {
private static DbSearcher searcher;
static {
try {
String dbPath = IpLocationUtils.class.getResource("/ip2region.xdb").getPath();
searcher = new DbSearcher(new DbConfig(), dbPath);
} catch (Exception e) {
log.error("IP库加载失败", e);
}
}
public static String getLocation(String ip) {
try {
Searcher searcher = Searcher.newWithFileOnly("ip2region.xdb");
String region = searcher.search(ip);
// region格式:中国|0|上海|上海市|电信
String[] parts = region.split("\\|");
return parts[0] + parts[2] + parts[3];
} catch (Exception e) {
return "未知";
}
}
}
性能优化:异步日志写入
避免日志写入影响登录性能,使用异步方式:
1 Spring异步支持
@EnableAsync
@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
2 异步Service
@Service
@Slf4j
public class LoginLogService {
@Autowired
private LoginLogMapper loginLogMapper;
@Async // 异步执行
public void save(LoginLog loginLog) {
try {
loginLogMapper.insert(loginLog);
} catch (Exception e) {
log.error("保存登录日志失败", e);
}
}
}
消息队列方案(高并发场景)
对于高并发系统,使用MQ异步处理:
@Service
public class LoginLogService {
@Autowired
private RabbitTemplate rabbitTemplate;
public void save(LoginLog loginLog) {
// 发送到消息队列
rabbitTemplate.convertAndSend("login.log.exchange", "login.log", loginLog);
}
}
@Component
@RabbitListener(queues = "login.log.queue")
public class LoginLogConsumer {
@Autowired
private LoginLogMapper loginLogMapper;
@RabbitHandler
public void process(LoginLog loginLog) {
try {
loginLogMapper.insert(loginLog);
} catch (Exception e) {
log.error("消费登录日志消息失败", e);
// 可以重试或发送到死信队列
}
}
}
查询接口示例
@RestController
@RequestMapping("/api/logs")
public class LoginLogController {
@Autowired
private LoginLogService loginLogService;
@GetMapping("/login")
public Result page(@RequestParam(defaultValue = "1") Integer page,
@RequestParam(defaultValue = "10") Integer size,
@RequestParam(required = false) String username,
@RequestParam(required = false) Integer status,
@RequestParam(required = false) String startTime,
@RequestParam(required = false) String endTime) {
Page<LoginLog> pageInfo = loginLogService.queryPage(page, size, username, status, startTime, endTime);
return Result.success(pageInfo);
}
@DeleteMapping("/login/{id}")
public Result delete(@PathVariable Long id) {
loginLogService.removeById(id);
return Result.success();
}
}
推荐方案
- 中小型项目:AOP + 数据库 + @Async异步写入
- Spring Security项目:事件监听 + 异步写入
- 高并发项目:AOP + 消息队列 + 批量写入
完整的登录日志系统还应该包括:
- 日志清理策略(定期删除旧日志)
- 异常登录告警(如连续失败N次)
- 日志统计分析(登录趋势、活跃度等)
需要根据实际项目规模选择合适的实现方案。