☰
从Nginx日志到在线用户列表:后端可观测性两招实战
2026/10/5 2:39:49 网站建设 项目流程

“你都看到了谁?”——这句话放在后端场景里,其实是一道相当现实的题目:线上服务每一天都在接收大量请求,但如果你被问到“当前都有哪些人在访问系统”,能不能在几分钟内给出明确答案?这些人用的是浏览器还是脚本,来自哪些 IP,登录了哪些账号,访问了哪些接口,是否存在异常行为?

很多业务系统在开发阶段只关心功能能不能跑通,上线之后才发现“可观测性”非常欠缺:用户在线状态靠猜,访问来源靠翻日志,安全排查靠运气。本文就围绕“你都看到了谁”这个问题,拆解两条完整的落地方案:一条是用 Python 分析 Nginx 访问日志,从静态数据里还原访问者的痕迹;另一条是用 Spring Boot 实现一个在线用户列表,从动态维度实时掌握当前在线的人。同时会补充核心概念、环境准备、常见坑点和工程建议,适合后端开发者、运维工程师以及刚开始接触 Web 可观测性的新手。

1. 背景:这个问题为什么值得花时间研究

1.1 “在线用户”和“活跃用户”不是一回事

在动手之前,先把两个容易混淆的概念分开。

“在线用户”通常指当前仍然保持着会话状态、并且在最近一段时间内有请求活动的用户。比如一个用户登录了后台系统,五分钟内又刷新了一次页面,那他可以算作在线用户;如果他关掉浏览器已经两个小时,虽然会话记录还在,但已经不应该出现在“在线列表”里。

“活跃用户”则是一个更大的口径,一般指某个统计周期内有访问行为的用户。一天内有登录记录就算当日活跃,一周内有访问就算周活跃。在线状态强调的是“此刻”,活跃状态强调的是“某段时间”。很多团队在统计在线人数时数字虚高,往往就是把活跃口径混进了在线口径。

1.2 常见的三类访问来源

当我们试着回答“你都看到了谁”时,能观察到的信息其实来自三个层面。

第一层是网络入口层。只要请求到达服务器,就可以拿到来源 IP、端口,以及经过代理时携带的转发头信息。这一层能看到“哪个网络位置发起了请求”,但看不到“具体是哪个人”。

第二层是应用请求层。HTTP 请求里带有 User-Agent、Referer、Cookie、Authorization 等字段,可以判断客户端类型、来源页面、登录凭证。这一层能看到“请求用什么工具发出”“是否携带登录态”,但 User-Agent 和 Referer 都可以伪造。

第三层是业务身份层。用户登录之后,服务端会记录 userId、角色、权限、操作日志等。这一层才能把请求关联到具体账号,也是做在线用户列表、操作审计、安全风控的基础。

1.3 从“看到谁”到“做了什么”

只回答“谁在访问”其实还不够,更完整的可观测性还要回答“做了什么”。同样是看到一批 IP,如果只是常规浏览页面,那是正常流量;如果短时间内高频请求登录接口、遍历订单接口、批量下载文件,那就是需要重点关注的异常行为。

所以本文的两个实战并不冲突:日志分析适合回答“历史上有谁来过、谁频率异常”,在线用户列表适合回答“此刻都有谁在线、谁最近活跃”。两者结合,才能形成完整的访问者画像。

2. 核心概念:HTTP 请求中能“看到”谁

2.1 一次请求中可以被观察的字段

下面是一张常用的“可见信息”表,后端开发者最好能熟练判断哪些字段可用、哪些字段不可信。

信息来源说明注意点
客户端 IPremote_addr发起 TCP 连接的地址经过代理后不一定准确
X-Forwarded-For请求头记录经过的代理链可伪造,需在可信代理后方可信任
User-Agent请求头标识客户端类型可伪造,只能作辅助判断
Referer请求头记录来源页面 URL可空,可伪造
Cookie / Session请求头登录态身份凭证日志中不应输出完整 Cookie
Authorization请求头接口认证凭证日志中必须脱敏
请求方法 + 路径请求行用户行为痕迹分析热点接口和异常路径
状态码响应行请求结果大量 4xx/5xx 需要关注

在实际项目中,日志里最容易犯的错误就是把 Token、Cookie、密码明文打印出来。日志是给人排查用的,不是给攻击者送信息的,脱敏是一条底线。

2.2 在线用户状态的判定原理

在线状态最核心的模型是“心跳 + 过期”。

用户登录成功后,服务端记录一条在线记录,包含 userId、登录时间、最后活跃时间、IP 等信息。之后每次请求都看作一次心跳,刷新最后活跃时间。当某个用户超过设定时间没有任何请求,就判定为离线。

整体流程可以概括为三步:

  1. 登录成功后写入在线记录,设置最后活跃时间为当前时间。
  2. 请求进入拦截器,如果携带登录用户标识,则刷新该用户的最后活跃时间。
  3. 查询在线列表时,过滤掉最后活跃时间超过阈值的用户,即可得到实时在线列表。

这里最关键的是“阈值”。阈值太短,用户只是中途离开一会儿就被踢出在线列表;阈值太长,离线用户又长时间占着在线列表。通常 Web 管理后台会把阈值设置为 5 到 15 分钟,具体要根据业务场景调整。

2.3 日志分析的核心思路

访问日志分析本质上做三件事:读取、解析、统计。

Nginx 的 access.log 默认是combined格式,每行代表一次请求,包含 IP、时间、请求行、状态码、响应大小、Referer、User-Agent。只要写一个正则表达式把关键字段提取出来,再用Counter做频率统计,就能得到“哪些 IP 来得多、哪些 User-Agent 是爬虫、哪些接口被频繁访问”等信息。

日志分析是离线视角,适合做回顾性排查;在线用户列表是实时视角,适合做现网监控。两者一起用,才能更好回答“你都看到了谁”。

3. 环境准备与示例素材

3.1 版本说明

本文的示例环境如下,实际项目请根据自己的版本调整:

  • 日志分析脚本:Python 3.8 及以上,仅使用标准库,不需要额外安装第三方包。
  • 在线用户列表:Spring Boot 2.7.x,Maven 3.6+,JDK 1.8 或更高版本。
  • 演示环境:Linux 或 Windows 均可,本文命令以 Bash 风格为主。

如果你使用的是 Spring Boot 3.x,需要注意javax.servlet包名变成了jakarta.servlet,核心思路不变,替换包名即可。

3.2 准备一份 access.log 示例

分析日志之前,我们需要有一个格式统一的日志文件。Nginx 默认的combined格式类似下面这样:

127.0.0.1 - - [10/Oct/2023:13:55:36 +0800] "GET /index.html HTTP/1.1" 200 2326 "https://example.com/" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0 Safari/537.36" 192.168.1.100 - - [10/Oct/2023:13:56:02 +0800] "GET /api/user/list HTTP/1.1" 200 4512 "-" "python-requests/2.31.0" 192.168.1.100 - - [10/Oct/2023:13:56:03 +0800] "POST /api/login HTTP/1.1" 401 182 "-" "python-requests/2.31.0"

从这份日志里,我们已经能看到三个层次的蛛丝马迹:IP 地址、请求路径、状态码、客户端工具。后文的正则就是按照这种格式编写的。

3.3 Spring Boot 演示项目结构

在线用户列表部分,我会搭建一个最小可运行项目,目录结构如下:

who-is-online ├── pom.xml └── src/main/java/com/example/whoisonline ├── WhoIsOnlineApplication.java ├── OnlineUser.java ├── OnlineUserManager.java ├── OnlineUserInterceptor.java ├── OnlineController.java ├── IpUtils.java └── WebConfig.java

这个结构没有数据库、没有 Redis,核心是用内存 Map 保存在线用户,思路非常清晰,适合先跑通,再扩展。

4. 实战一:Python 脚本分析 Nginx 日志

4.1 先确认日志格式

不要拿到日志就直接套正则,先打开日志文件,看一两条真实记录,确认字段顺序和数量。本文的正则针对的是标准的combined格式,如果你在 Nginx 配置里添加了$request_time、$upstream_addr等自定义字段,就需要自行调整正则。

确认格式之后,可以把日志文件放在任意目录,脚本只负责读取和统计,不修改原文件。

4.2 编写日志解析脚本

下面是完整的analyze_access_log.py脚本。它的功能包括:逐行读取日志、提取 IP / 时间 / 请求 / 状态码 / UA、统计状态码分布、TOP IP、TOP UA、小时维度的请求趋势,以及标记高频访问者。

import re import sys import argparse from collections import Counter, defaultdict from datetime import datetime LOG_PATTERN = re.compile( r'^(?P<ip>\S+) \S+ \S+ \[(?P<time>[^\]]+)\] ' r'"(?P<request>[^"]*)" (?P<status>\d{3}) (?P<body_size>\S+) ' r'"(?P<referer>[^"]*)" "(?P<ua>[^"]*)"' ) def parse_line(line): match = LOG_PATTERN.match(line) if not match: return None return match.groupdict() def parse_time(time_str): return datetime.strptime(time_str, "%d/%b/%Y:%H:%M:%S %z") def extract_path(request): parts = request.split(" ") if len(parts) >= 2: return parts[1] return request def analyze_log(file_path, high_freq_threshold=60): ip_counter = Counter() ua_counter = Counter() status_counter = Counter() hour_counter = Counter() path_counter = Counter() invalid = 0 total = 0 with open(file_path, "r", encoding="utf-8", errors="ignore") as f: for line in f: record = parse_line(line) if not record: invalid += 1 continue total += 1 ip = record["ip"] ua = record["ua"] status = record["status"] path = extract_path(record["request"]) try: hour = parse_time(record["time"]).strftime("%Y-%m-%d %H") except ValueError: hour = "unknown" ip_counter[ip] += 1 ua_counter[ua] += 1 status_counter[status] += 1 hour_counter[hour] += 1 path_counter[path] += 1 print(f"总请求数: {total}") print(f"无法解析行数: {invalid}") print("\n=== 状态码分布 ===") for status, count in status_counter.most_common(): print(f"{status}: {count}") print("\n=== TOP 10 客户端 IP ===") for ip, count in ip_counter.most_common(10): print(f"{ip}: {count} 次") print("\n=== TOP 10 User-Agent ===") for ua, count in ua_counter.most_common(10): print(f"{ua}: {count} 次") print("\n=== TOP 10 请求路径 ===") for path, count in path_counter.most_common(10): print(f"{path}: {count} 次") print("\n=== 请求量 TOP 小时 ===") for hour, count in hour_counter.most_common(10): print(f"{hour}: {count} 次") print(f"\n=== 高频访问者(超过 {high_freq_threshold} 次/小时) ===") for ip, count in ip_counter.most_common(): if count >= high_freq_threshold: print(f"{ip}: {count} 次") def main(): parser = argparse.ArgumentParser(description="分析 Nginx access.log") parser.add_argument("logfile", help="日志文件路径") parser.add_argument("--threshold", type=int, default=60, help="高频请求阈值(次/小时),默认 60") args = parser.parse_args() try: analyze_log(args.logfile, args.threshold) except FileNotFoundError: print(f"文件不存在: {args.logfile}") sys.exit(1) if __name__ == "__main__": main()

脚本里体现了几个工程细节:正则匹配失败的行不会中断程序,而是计入invalid;日志编码使用errors="ignore"避免个别乱码行导致崩溃;时间解析失败时归为unknown。这些处理在真实日志分析中非常实用。

4.3 运行与分析结果

在命令行执行:

python analyze_access_log.py /var/log/nginx/access.log --threshold 60

如果日志文件在当前目录,可以简化为:

python analyze_access_log.py access.log

运行后输出大致如下:

总请求数: 10234 无法解析行数: 0 === 状态码分布 === 200: 8231 301: 1023 404: 890 500: 90 === TOP 10 客户端 IP === 210.12.34.56: 1520 次 103.88.46.132: 980 次 ...

看到这样的结果,第一反应不应该是直接封 IP,而是结合请求路径和 User-Agent 进一步判断:这些高频 IP 访问的是静态资源还是敏感接口?UA 是浏览器还是python-requests?如果两者都指向异常,再考虑安全策略。

4.4 扩展:识别高频访问者和爬虫

上面的脚本已经包含高频访问者识别功能。标准是“每小时超过 N 次”,N 由--threshold控制。

对于 User-Agent 中的爬虫特征,可以在脚本中增加一个简单的关键词集合:

SPIDER_KEYWORDS = ["bot", "spider", "curl", "python-requests", "scrapy"] def is_spider_ua(ua): ua_lower = ua.lower() return any(keyword in ua_lower for keyword in SPIDER_KEYWORDS)

然后在解析循环里,对每条 UA 做一次判断,单独统计爬虫请求量。需要说明的是:正常搜索引擎爬虫也会访问站点,不一定要拒绝,但异常高频的脚本访问往往意味着接口被盗刷或数据被爬取,需要另做治理。

5. 实战二:Spring Boot 实现在线用户列表

5.1 需求与设计

在线用户列表的需求很直接:用户登录成功后出现在列表中;用户有请求时刷新最后活跃时间;超过 5 分钟没有请求则从列表消失;管理员可以查看当前在线用户。

为了不引入数据库和认证框架,本文在演示时用请求头X-User-Id模拟登录用户的身份。实际项目中,这个值应该来自登录后的 Token 解析结果,而不是让客户端随意传一个 userId。

5.2 项目结构与依赖

在 Spring Boot 项目中,只需要引入spring-boot-starter-web。pom.xml如下:

<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.7.18</version> <relativePath/> </parent> <groupId>com.example</groupId> <artifactId>who-is-online</artifactId> <version>1.0.0</version> <properties> <java.version>1.8</java.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build> </project>

5.3 核心代码实现

先创建启动类WhoIsOnlineApplication.java:

package com.example.whoisonline; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication public class WhoIsOnlineApplication { public static void main(String[] args) { SpringApplication.run(WhoIsOnlineApplication.class, args); } }

再创建在线用户实体类OnlineUser.java。这里使用普通 JavaBean 而不是 Lombok,是为了避免读者因为 Lombok 版本问题导致编译失败:

package com.example.whoisonline; public class OnlineUser { private String userId; private String nickname; private String ip; private long loginTime; private long lastActiveTime; public String getUserId() { return userId; } public void setUserId(String userId) { this.userId = userId; } public String getNickname() { return nickname; } public void setNickname(String nickname) { this.nickname = nickname; } public String getIp() { return ip; } public void setIp(String ip) { this.ip = ip; } public long getLoginTime() { return loginTime; } public void setLoginTime(long loginTime) { this.loginTime = loginTime; } public long getLastActiveTime() { return lastActiveTime; } public void setLastActiveTime(long lastActiveTime) { this.lastActiveTime = lastActiveTime; } }

核心管理类OnlineUserManager.java负责在线状态的写入、心跳续期、离线删除和在线列表查询:

package com.example.whoisonline; import org.springframework.stereotype.Component; import java.util.ArrayList; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; @Component public class OnlineUserManager { private static final long EXPIRE_MILLIS = 5 * 60 * 1000L; private final Map<String, OnlineUser> onlineUsers = new ConcurrentHashMap<>(); public void online(String userId, String nickname, String ip) { OnlineUser user = new OnlineUser(); user.setUserId(userId); user.setNickname(nickname); user.setIp(ip); long now = System.currentTimeMillis(); user.setLoginTime(now); user.setLastActiveTime(now); onlineUsers.put(userId, user); } public void heartbeat(String userId, String ip) { OnlineUser user = onlineUsers.get(userId); if (user != null) { user.setLastActiveTime(System.currentTimeMillis()); if (ip != null && !ip.isEmpty()) { user.setIp(ip); } } } public void offline(String userId) { onlineUsers.remove(userId); } public List<OnlineUser> listOnlineUsers() { long now = System.currentTimeMillis(); List<OnlineUser> result = new ArrayList<>(); Iterator<Map.Entry<String, OnlineUser>> iterator = onlineUsers.entrySet().iterator(); while (iterator.hasNext()) { OnlineUser user = iterator.next().getValue(); if (now - user.getLastActiveTime() > EXPIRE_MILLIS) { iterator.remove(); } else { result.add(user); } } return result; } public boolean isOnline(String userId) { OnlineUser user = onlineUsers.get(userId); return user != null && System.currentTimeMillis() - user.getLastActiveTime() <= EXPIRE_MILLIS; } }

这里有一个容易被忽略的细节:heartbeat方法不会为不存在的 userId 创建在线记录。也就是说,只有登录过的用户才能保持在线状态,单纯伪造一个X-User-Id请求头是没法让自己出现在在线列表里的。

IP 工具类IpUtils.java用于从请求中获取客户端地址:

package com.example.whoisonline; import org.springframework.util.StringUtils; import javax.servlet.http.HttpServletRequest; public class IpUtils { private IpUtils() { } public static String getClientIp(HttpServletRequest request) { String forwarded = request.getHeader("X-Forwarded-For"); if (StringUtils.hasText(forwarded)) { String first = forwarded.split(",")[0].trim(); if (!first.isEmpty()) { return first; } } String realIp = request.getHeader("X-Real-IP"); if (StringUtils.hasText(realIp)) { return realIp; } return request.getRemoteAddr(); } }

拦截器OnlineUserInterceptor.java会在每个请求进入时读取用户标识并刷新活跃时间:

package com.example.whoisonline; import org.springframework.util.StringUtils; import org.springframework.web.servlet.HandlerInterceptor; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; public class OnlineUserInterceptor implements HandlerInterceptor { private final OnlineUserManager onlineUserManager; public OnlineUserInterceptor(OnlineUserManager onlineUserManager) { this.onlineUserManager = onlineUserManager; } @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { String userId = request.getHeader("X-User-Id"); if (StringUtils.hasText(userId)) { onlineUserManager.heartbeat(userId, IpUtils.getClientIp(request)); } return true; } }

拦截器配置类WebConfig.java:

package com.example.whoisonline; import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.config.annotation.InterceptorRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; @Configuration public class WebConfig implements WebMvcConfigurer { private final OnlineUserManager onlineUserManager; public WebConfig(OnlineUserManager onlineUserManager) { this.onlineUserManager = onlineUserManager; } @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(new OnlineUserInterceptor(onlineUserManager)) .addPathPatterns("/**") .excludePathPatterns("/api/login", "/error"); } }

控制器OnlineController.java提供登录、登出、在线列表三个接口:

package com.example.whoisonline; 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; import javax.servlet.http.HttpServletRequest; import java.util.HashMap; import java.util.List; import java.util.Map; @RestController public class OnlineController { private final OnlineUserManager onlineUserManager; public OnlineController(OnlineUserManager onlineUserManager) { this.onlineUserManager = onlineUserManager; } @PostMapping("/api/login") public Map<String, Object> login(@RequestBody Map<String, String> body, HttpServletRequest request) { String userId = body.get("userId"); String nickname = body.get("nickname"); Map<String, Object> result = new HashMap<>(); if (userId == null || userId.trim().isEmpty()) { result.put("success", false); result.put("message", "userId 不能为空"); return result; } // 实际项目:这里应该先校验账号密码,再签发 Token,最后记录在线状态。 onlineUserManager.online(userId, nickname, IpUtils.getClientIp(request)); result.put("success", true); result.put("message", "登录成功,已记录在线状态"); return result; } @GetMapping("/api/online/list") public List<OnlineUser> onlineList() { // 生产环境必须给该接口增加管理员权限校验,不能匿名访问。 return onlineUserManager.listOnlineUsers(); } @PostMapping("/api/logout") public Map<String, Object> logout(@RequestBody Map<String, String> body) { String userId = body.get("userId"); Map<String, Object> result = new HashMap<>(); if (userId == null || userId.trim().isEmpty()) { result.put("success", false); result.put("message", "userId 不能为空"); return result; } onlineUserManager.offline(userId); result.put("success", true); result.put("message", "已下线"); return result; } }

这段代码把登录模拟、在线查询、登出操作放在一起,完整可运行。生产环境需要把“从请求头取 userId”替换成“从 Token 解析 userId”,并给/api/online/list增加权限控制。

5.4 启动并验证

启动项目:

mvn spring-boot:run

使用 curl 模拟用户登录:

curl -X POST http://localhost:8080/api/login \ -H "Content-Type: application/json" \ -d '{"userId":"10001","nickname":"张三"}'

查询在线列表:

curl http://localhost:8080/api/online/list

预期输出类似:

[ { "userId": "10001", "nickname": "张三", "ip": "127.0.0.1", "loginTime": 1696948536000, "lastActiveTime": 1696948536000 } ]

再模拟一次心跳访问:

curl -H "X-User-Id: 10001" http://localhost:8080/api/online/list

这次请求本身会触发拦截器,刷新lastActiveTime。如果你在 5 分钟内不再次请求,用户就会从在线列表中消失。

登出接口演示:

curl -X POST http://localhost:8080/api/logout \ -H "Content-Type: application/json" \ -d '{"userId":"10001"}'

5.5 生产环境扩展:Redis 版思路

上面的内存版适合单机演示和小型项目。一旦系统部署了多个实例,每个实例的内存 Map 是各自独立的,用户在实例 A 登录,请求被负载均衡到实例 B 时,在线状态就丢失了。生产环境建议使用 Redis 作为在线状态存储。

核心思路是用 Redis 的 TTL 天然实现过期:

  • key 为online:user:{userId};
  • 登录时写入该 key,值为用户信息的 JSON 字符串,设置 5 分钟过期;
  • 每次请求心跳时刷新过期时间;
  • 查询在线用户时,通过扫描online:user:*或额外维护一个用户索引集合来获取在线列表。

使用StringRedisTemplate的核心片段思路如下:

stringRedisTemplate.opsForValue().set( "online:user:" + userId, userJson, Duration.ofMinutes(5) ); // 心跳续期 stringRedisTemplate.expire("online:user:" + userId, Duration.ofMinutes(5));

这段代码展示了基本思路,实际使用时需要按项目的 Redis 客户端版本稍作调整。引入 Redis 后,内存 Map 的清理逻辑就不再需要了,TTL 会由 Redis 自动处理。

6. 常见问题与排查思路

6.1 在线人数统计为什么不准

问题现象常见原因解决思路
在线人数长期虚高过期时间太长,或者根本没有过期清理逻辑设置合理的过期阈值,并在查询时过滤超时用户
在线人数波动剧烈阈值太短,用户稍久没操作就被清除根据业务场景调整 TTL,比如管理后台设为 15 分钟
用户退出后仍在在线列表退出接口没有调用 offline 方法在登出逻辑中主动删除在线记录
多个实例在线列表不一致内存 Map 方案无法跨节点共享改用 Redis 存储在线状态

6.2 为什么拿到的 IP 不是真实 IP

如果你的服务前面有 Nginx、SLB、CDN 等代理,request.getRemoteAddr()拿到的是代理服务器地址,而不是真实客户端 IP。

排查时先看X-Forwarded-For和X-Real-IP请求头有没有值。如果为空,说明代理层没有配置转发头;如果包含多个逗号分隔的地址,说明请求经过了多级代理,通常第一个是原始客户端地址,但没有绝对保证。

需要特别注意的是,X-Forwarded-For是客户端可伪造的。只有在 Nginx 层正确配置并覆盖该请求头,应用层拿到它才有意义。

6.3 日志脚本匹配不到记录

运行日志分析脚本时如果发现无法解析行数很大,大概率是日志格式和正则不匹配。

解决办法是先执行head -n 1 access.log看真实日志内容,然后对照正则里的字段顺序逐项确认。Nginx 默认的 combined 格式包含 IP、两个-占位符、时间、请求行、状态码、响应大小、Referer、UA。如果加了自定义字段,需要同步修改正则。

6.4 在线列表接口被当作“公开接口”滥用

在线用户列表接口如果没做权限控制,任何人都能查看系统当前有哪些用户在线,这是一个安全隐患。

最佳做法是:

  • 将在线列表接口限制在管理后台或内网环境;
  • 接口增加登录认证和管理员角色校验;
  • 返回数据使用 VO 对象,只暴露必要字段,隐藏 IP 等敏感信息。

7. 最佳实践与工程建议

7.1 合规与隐私

在线用户信息、IP 地址、User-Agent 在特定场景下都可能属于个人信息。系统在采集这些数据前,需要遵循“合法、正当、必要”的收集原则,并在隐私政策中告知用户。

实际操作中有几条具体建议:

  • 日志中不要打印完整 Token、Cookie、密码、手机号等敏感字段;
  • 在线用户列表不向普通用户开放,只面向有权限的管理人员;
  • 日志保留时间要有策略,比如保留 90 天或 180 天,过期清理;
  • 分析日志时只处理自己有权限访问的服务器日志,不探测外部站点。

7.2 安全与性能

安全方面,在线用户列表、日志查询这类功能属于高敏感功能,要做单独的权限控制,不能混在普通业务接口里。

性能方面,需要注意以下几点:

  • 不要在每个请求里都更新数据库,心跳更新应该使用 Redis TTL 或内存结构;
  • 日志分析脚本要逐行读取,不要一次性把整个日志文件 load 进内存;
  • 在线列表查询如果数据量大,要做分页;
  • 在线状态阈值不要设成代码里的魔法数字,建议放入配置中心或application.yml。

7.3 可维护性与拓展

在线用户功能看起来简单,但维护起来有不少细节。把过期时间做成配置项,可以方便在不同环境调整。登录时间、最后活跃时间、登录 IP 这些字段都应该记录,因为它们是安全排查的基础数据。

再往后拓展,可以把“在线用户”升级成“用户行为轨迹”:记录每个用户访问了哪些接口、停留了多久、操作顺序是什么。这时可以在请求入口埋点,再配合日志采集链路,逐步建立自己的可观测体系。

8. 总结与下一步学习方向

这篇文章用一个问句“你都看到了谁”串联了两条技术主线:一条是通过 Python 解析 Nginx 日志,从历史数据中还原访问者的来源和行为;另一条是通过 Spring Boot 实现在线用户列表,从实时数据中掌握当前在线用户。前者的关键词是“离线分析”,后者的关键词是“心跳过期”,两者并不冲突,而是互补关系。

如果要把这套能力做得更深入,下一步可以学习三类内容:第一是分布式会话与单点登录,解决在线状态在多实例下的一致性;第二是设备指纹与用户行为分析,从访问者识别过渡到用户风险识别;第三是可观测性体系搭建,比如通过 Prometheus 和 Grafana 对你的活跃用户指标做可视化监控。

最后,建议你手头有服务器日志的话,先把 Python 脚本跑一遍,看看自己系统里到底“都有谁”。动态在线列表也可以从一个最小的 Spring Boot 项目开始,理解了心跳和过期机制之后,再考虑引入 Redis。动手验证,永远比只看文章有效。如果这篇教程对你有帮助,可以先收藏备用,后面排查问题时再翻出来对照。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询