Java案例如何实现登录日志?

wen python案例 6

本文目录导读:

Java案例如何实现登录日志?

  1. 基础实现:使用AOP + 数据库
  2. 使用Spring Security + 事件监听
  3. 高级功能:登录地点解析
  4. 性能优化:异步日志写入
  5. 消息队列方案(高并发场景)
  6. 查询接口示例
  7. 推荐方案

实现登录日志是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,可以通过监听AuthenticationSuccessEventAuthenticationFailureBadCredentialsEvent事件来实现。

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 + 消息队列 + 批量写入

完整的登录日志系统还应该包括:

  1. 日志清理策略(定期删除旧日志)
  2. 异常登录告警(如连续失败N次)
  3. 日志统计分析(登录趋势、活跃度等)

需要根据实际项目规模选择合适的实现方案。

抱歉,评论功能暂时关闭!