签入版本
This commit is contained in:
@@ -0,0 +1,249 @@
|
||||
# IP/UA 解析
|
||||
|
||||
<!-- Copyright (c) 2021-2026 小蚂蚁云团队 版权所有 -->
|
||||
|
||||
:::tip 说明
|
||||
提供客户端 IP 获取、IP 地理位置查询和 User-Agent 解析三大能力,位于 `utils/common.py`。三者组合使用,为登录日志、操作日志等场景提供客户端身份信息。
|
||||
:::
|
||||
|
||||
## 架构总览
|
||||
|
||||
```
|
||||
请求进入
|
||||
│
|
||||
▼
|
||||
┌──────────────────────────────────────────────────────┐
|
||||
│ get_client_ip(request) │
|
||||
│ ├─ HTTP_X_FORWARDED_FOR 存在? │
|
||||
│ │ └─ 是 → 取第一个 IP(客户端真实 IP) │
|
||||
│ └─ 否 → REMOTE_ADDR(直接连接 IP) │
|
||||
└──────────┬───────────────────────────────────────────┘
|
||||
│
|
||||
┌─────┴─────┐
|
||||
▼ ▼
|
||||
┌─────────┐ ┌──────────────────────────────────┐
|
||||
│ 日志记录 │ │ get_ip_location(ip) │
|
||||
│ 操作日志 │ │ ├─ 内网 IP → "内网IP" │
|
||||
└─────────┘ │ ├─ ip-api.com 在线查询 │
|
||||
│ └─ 异常降级 → "未知" │
|
||||
└──────────┬───────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌──────────────────────┐
|
||||
│ parse_user_agent(ua) │
|
||||
│ ├─ 操作系统(正则) │
|
||||
│ └─ 浏览器(正则) │
|
||||
└──────────┬───────────┘
|
||||
│
|
||||
▼
|
||||
┌──────────────────────┐
|
||||
│ 写入日志表 │
|
||||
│ ip / location / │
|
||||
│ os / browser │
|
||||
└──────────────────────┘
|
||||
```
|
||||
|
||||
## 一、IP 获取
|
||||
|
||||
### get_client_ip(request)
|
||||
|
||||
```python
|
||||
# utils/common.py
|
||||
def get_client_ip(request) -> str:
|
||||
"""
|
||||
获取客户端真实IP地址
|
||||
|
||||
优先从 HTTP_X_FORWARDED_FOR 获取,其次使用 REMOTE_ADDR。
|
||||
"""
|
||||
x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR')
|
||||
if x_forwarded_for:
|
||||
ip = x_forwarded_for.split(',')[0]
|
||||
else:
|
||||
ip = request.META.get('REMOTE_ADDR')
|
||||
return ip
|
||||
```
|
||||
|
||||
**返回值**:客户端 IP 地址字符串(如 `"1.2.3.4"`)。
|
||||
|
||||
### 代理环境说明
|
||||
|
||||
当应用部署在 Nginx 等反向代理之后,`REMOTE_ADDR` 拿到的是代理 IP 而非真实客户端 IP。此时需要从 `X-Forwarded-For`(XFF)头提取真实 IP。
|
||||
|
||||
```
|
||||
客户端 (1.2.3.4)
|
||||
│
|
||||
▼
|
||||
Nginx 代理 (10.0.0.1) ← 追加 XFF: 1.2.3.4
|
||||
│
|
||||
▼
|
||||
Django 应用
|
||||
REMOTE_ADDR = 10.0.0.1
|
||||
HTTP_X_FORWARDED_FOR = "1.2.3.4"
|
||||
```
|
||||
|
||||
:::warning 安全提示
|
||||
如果应用被直接暴露在公网,客户端可伪造 XFF 首部。生产环境务必通过 Nginx 等反向代理转发请求。
|
||||
:::
|
||||
|
||||
## 二、IP 地理位置查询
|
||||
|
||||
### get_ip_location(ip)
|
||||
|
||||
通过第三方 API 获取 IP 地理位置:
|
||||
|
||||
```python
|
||||
# utils/common.py
|
||||
def get_ip_location(ip: str) -> str:
|
||||
"""
|
||||
根据IP获取地理位置
|
||||
|
||||
内网IP直接返回"内网IP",调用 ip-api.com API 获取地理位置。
|
||||
"""
|
||||
# 内网IP判断
|
||||
if ip in ['127.0.0.1', 'localhost'] or ip.startswith('192.168.') or ip.startswith('10.'):
|
||||
return "内网IP"
|
||||
|
||||
try:
|
||||
url = f"http://ip-api.com/json/{ip}?lang=zh-CN"
|
||||
with urllib.request.urlopen(url, timeout=3) as response:
|
||||
data = json.loads(response.read().decode('utf-8'))
|
||||
if data.get('status') == 'success':
|
||||
return f"{data.get('country', '')} {data.get('regionName', '')} {data.get('city', '')}"
|
||||
except Exception as e:
|
||||
logger.warning(f"获取IP地理位置失败: {e}")
|
||||
|
||||
return "未知"
|
||||
```
|
||||
|
||||
**返回值示例**:
|
||||
|
||||
| IP | 返回值 |
|
||||
|:----|:----|
|
||||
| `114.114.114.114` | `中国 江苏省 南京市` |
|
||||
| `8.8.8.8` | `United States California` |
|
||||
| `10.0.0.1` | `内网IP` |
|
||||
| `172.abc` | `未知` |
|
||||
|
||||
### IP 地理位置配置
|
||||
|
||||
在 `config/env.py` 中配置 IP 查询相关参数:
|
||||
|
||||
```python
|
||||
# IP地理位置信息来源: 'local' 或 'api'
|
||||
IP_LOCATION_SOURCE = os.environ.get('IP_LOCATION_SOURCE', 'api')
|
||||
|
||||
# 本地IP数据库文件路径
|
||||
IP_DATABASE_PATH = os.environ.get('IP_DATABASE_PATH', ...)
|
||||
|
||||
# IP查询API超时时间(秒)
|
||||
IP_API_TIMEOUT = int(os.environ.get('IP_API_TIMEOUT', 5))
|
||||
|
||||
# 是否启用IP地理位置缓存
|
||||
IP_CACHE_ENABLED = os.environ.get('IP_CACHE_ENABLED', 'True') == 'True'
|
||||
|
||||
# IP缓存时间(秒),默认1天
|
||||
IP_CACHE_TIMEOUT = int(os.environ.get('IP_CACHE_TIMEOUT', 86400))
|
||||
```
|
||||
|
||||
### 内网 IP 判断
|
||||
|
||||
内网/保留 IP 直接返回 `"内网IP"`,不查询 API:
|
||||
|
||||
| 类型 | 匹配规则 | 示例 |
|
||||
|:-----|:---------|:-----|
|
||||
| 回环地址 | `127.0.0.1`、`localhost` | `127.0.0.1` |
|
||||
| C 类私有 | `192.168.x.x` | `192.168.1.1` |
|
||||
| A 类私有 | `10.x.x.x` | `10.0.0.1` |
|
||||
|
||||
## 三、UA 解析
|
||||
|
||||
### parse_user_agent(user_agent)
|
||||
|
||||
通过正则匹配从 User-Agent 字符串提取操作系统和浏览器信息:
|
||||
|
||||
```python
|
||||
# utils/common.py
|
||||
def parse_user_agent(user_agent: str) -> dict:
|
||||
"""
|
||||
解析User-Agent,获取操作系统和浏览器信息
|
||||
|
||||
Returns:
|
||||
dict: {'os': '操作系统', 'browser': '浏览器'}
|
||||
"""
|
||||
```
|
||||
|
||||
**使用示例**:
|
||||
|
||||
```python
|
||||
ua = request.META.get('HTTP_USER_AGENT', '')
|
||||
info = parse_user_agent(ua)
|
||||
print(f"操作系统: {info['os']}, 浏览器: {info['browser']}")
|
||||
```
|
||||
|
||||
### 支持的操作系统
|
||||
|
||||
| 操作系统 | UA 关键字 | 输出 |
|
||||
|:---------|:---------|:-----|
|
||||
| Windows 10 | `Windows NT 10.0` | `Windows 10` |
|
||||
| Windows 7 | `Windows NT 6.1` | `Windows 7` |
|
||||
| macOS | `Mac OS X` | `macOS` |
|
||||
| Linux | `Linux` | `Linux` |
|
||||
| Android | `Android` | `Android` |
|
||||
| iOS | `iPhone` / `iPad` | `iOS` |
|
||||
|
||||
### 支持的浏览器
|
||||
|
||||
按匹配优先级排列(Edge 优先于 Chrome):
|
||||
|
||||
| 浏览器 | UA 关键字 | 输出 |
|
||||
|:-------|:---------|:-----|
|
||||
| Edge | `Edg` | `Edge` |
|
||||
| Chrome | `Chrome` | `Chrome` |
|
||||
| Safari | `Safari` | `Safari` |
|
||||
| Firefox | `Firefox` | `Firefox` |
|
||||
| IE | `MSIE` / `Trident` | `IE` |
|
||||
|
||||
## 四、应用场景
|
||||
|
||||
### 登录日志
|
||||
|
||||
登录/退出时自动记录 IP、地理位置、操作系统、浏览器:
|
||||
|
||||
```python
|
||||
ip = get_client_ip(request)
|
||||
location = get_ip_location(ip)
|
||||
ua = request.META.get('HTTP_USER_AGENT', '')
|
||||
info = parse_user_agent(ua)
|
||||
```
|
||||
|
||||
### 操作日志
|
||||
|
||||
写操作(add/update/delete/status)通过 `@operation_log` 装饰器自动记录操作者 IP 和设备信息。
|
||||
|
||||
## 五、常见问题
|
||||
|
||||
### 开发环境日志中 IP 显示为 127.0.0.1
|
||||
|
||||
**原因**:本地直连无代理,`REMOTE_ADDR` 就是 `127.0.0.1`。
|
||||
|
||||
**说明**:这是正常行为。生产环境配置 Nginx 反向代理后即可获取真实客户端 IP。
|
||||
|
||||
### 地理位置显示为"未知"
|
||||
|
||||
**可能原因**:
|
||||
|
||||
1. ip-api.com API 请求超时(免费版限制 45 次/分钟)
|
||||
2. 网络连接问题
|
||||
3. 畸形 IP 地址
|
||||
|
||||
**解决**:生产环境建议使用本地 IP 数据库(如纯真 IP 库),配置 `IP_LOCATION_SOURCE=local`。
|
||||
|
||||
## 六、总结
|
||||
|
||||
| 模块 | 文件 | 核心能力 | 降级策略 |
|
||||
|:-----|:-----|:---------|:---------|
|
||||
| IP 获取 | `utils/common.py` | 从 Request 提取真实客户端 IP | 优先 XFF,回退 REMOTE_ADDR |
|
||||
| 地理位置 | `utils/common.py` | 基于 ip-api.com 在线查询 | 异常/内网统一降级返回 `"未知"` 或 `"内网IP"` |
|
||||
| UA 解析 | `utils/common.py` | 正则匹配提取操作系统和浏览器 | UA 为空时返回 `{'os': '未知', 'browser': '未知'}` |
|
||||
|
||||
三者组合为登录日志、操作日志等场景提供完整的客户端身份信息链路。所有接口均采用降级策略,保证日志落库等核心流程不受异常影响。
|
||||
Reference in New Issue
Block a user