Files
djangoadmin-wiki/DjangoAdmin_Django_EleVue/zh/volume4/tools/ip-ua.md
T
2026-09-12 23:12:15 +08:00

250 lines
8.0 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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': '未知'}` |
三者组合为登录日志、操作日志等场景提供完整的客户端身份信息链路。所有接口均采用降级策略,保证日志落库等核心流程不受异常影响。