338 lines
9.2 KiB
Markdown
338 lines
9.2 KiB
Markdown
# 技术文档:基于 `aiohttp` 的认证与会话管理系统
|
||
|
||
---
|
||
|
||
## 概述
|
||
|
||
本文档描述了一个基于 `aiohttp` 构建的异步 Web 应用中的 **用户认证(Authentication)** 与 **会话管理(Session Management)** 系统。该系统支持:
|
||
|
||
- 基于 Cookie 或 Redis 存储的会话机制
|
||
- 使用加密 Ticket 的安全认证策略(`TktAuthentication`)
|
||
- 支持客户端唯一标识(`client_uuid`)绑定
|
||
- RSA 加密解密功能用于敏感数据处理
|
||
- 可扩展的权限检查接口
|
||
- 请求日志记录与异常追踪
|
||
|
||
该模块适用于需要高安全性、可扩展性和分布式部署能力的 Web 后端服务。
|
||
|
||
---
|
||
|
||
## 依赖库说明
|
||
|
||
| 包名 | 用途 |
|
||
|------|------|
|
||
| `aiohttp` | 异步 Web 框架核心 |
|
||
| `aiohttp_auth` / `TktAuthentication` | 基于票据(ticket)的身份认证中间件 |
|
||
| `aiohttp_session` | 会话管理中间件,支持多种存储后端 |
|
||
| `redis.asyncio` | 异步 Redis 客户端,用于持久化会话 |
|
||
| `cryptography`(隐式依赖) | 加密 Cookie 所需(由 `EncryptedCookieStorage` 内部使用) |
|
||
| `appPublic.*` | 自定义公共工具模块(配置、日志、RSA 加解密等) |
|
||
|
||
> ⚠️ 注意:原代码中注释了 `aioredis`,实际使用的是新版本 `redis.asyncio`
|
||
|
||
---
|
||
|
||
## 核心功能模块
|
||
|
||
### 1. 会话管理(Session Management)
|
||
|
||
#### 支持两种会话存储方式:
|
||
|
||
| 存储类型 | 配置开关 | 特点 |
|
||
|--------|---------|------|
|
||
| 加密 Cookie 存储 | 默认启用 | 无外部依赖,适合单机部署 |
|
||
| Redis 存储 | `conf.website.session_redis.url` 存在时启用 | 支持集群、集中管理、更安全 |
|
||
|
||
#### 自定义 Redis 存储类:`MyRedisStorage`
|
||
|
||
继承自 `RedisStorage`,增强以下特性:
|
||
|
||
- **自定义 Session Key 生成逻辑**
|
||
- 优先从请求头 `client_uuid` 获取客户端唯一标识
|
||
- 若不存在,则随机生成 UUID4 并作为响应返回给客户端
|
||
- 对字符串 key 进行 hex 编码以保证兼容性
|
||
|
||
```python
|
||
def key_gen(self, request):
|
||
key = request.headers.get('client_uuid')
|
||
if not key:
|
||
key = uuid.uuid4().hex
|
||
return key
|
||
if isinstance(key, str):
|
||
key = key.encode('utf-8')
|
||
key = binascii.hexlify(key).decode('utf-8')
|
||
return key
|
||
```
|
||
|
||
> ✅ 目的:实现跨设备/浏览器的稳定会话识别,避免频繁重新登录。
|
||
|
||
---
|
||
|
||
### 2. 认证机制(Authentication)
|
||
|
||
使用 `aiohttp_auth.auth.ticket_auth.TktAuthentication` 实现基于时间戳和签名的票据认证。
|
||
|
||
#### 关键参数配置:
|
||
|
||
| 参数 | 默认值 | 配置项 | 说明 |
|
||
|------|-------|--------|------|
|
||
| `session_max_time` | 120 秒 | `website.session_max_time` | 会话最长有效期 |
|
||
| `reissue_time` | 30 秒 | `website.session_reissue_time` | 自动续签间隔 |
|
||
| `include_ip` | `True` | 固定设置 | 将客户端 IP 加入票据哈希,防劫持 |
|
||
|
||
#### 自定义 `_new_ticket` 方法
|
||
|
||
重写了票据创建过程,加入对 `client_uuid` 的支持:
|
||
|
||
```python
|
||
def _new_ticket(self, request, user_id):
|
||
client_uuid = request.headers.get('client_uuid')
|
||
ip = self._get_ip(request)
|
||
valid_until = int(time.time()) + self._max_age
|
||
return self._ticket.new(
|
||
user_id,
|
||
valid_until=valid_until,
|
||
client_ip=ip,
|
||
user_data=client_uuid # 将 client_uuid 附加到票据中
|
||
)
|
||
```
|
||
|
||
> 🔐 安全提示:IP 绑定 + client_uuid 提升了会话安全性,防止 CSRF 和会话固定攻击。
|
||
|
||
---
|
||
|
||
### 3. 用户信息提取
|
||
|
||
提供异步函数用于从当前会话中获取用户信息。
|
||
|
||
#### 函数列表
|
||
|
||
| 函数 | 返回值 | 说明 |
|
||
|------|--------|------|
|
||
| `get_session_userinfo(request)` | `DictObject(userid, username, userorgid)` | 解析认证数据并封装为对象 |
|
||
| `get_session_user(request)` | `userid` 字符串 | 快速获取当前用户 ID |
|
||
|
||
> 💡 数据格式:认证信息以 `userid:username:userorgid` 形式存储于 ticket 中。
|
||
|
||
示例:
|
||
```python
|
||
await auth.remember(request, "U1001:alice:ORG789")
|
||
```
|
||
|
||
---
|
||
|
||
### 4. 登录与登出操作
|
||
|
||
#### `user_login(request, userid, username='', userorgid='')`
|
||
将用户信息编码后写入认证票据。
|
||
|
||
```python
|
||
ui = f'{userid}:{username}:{userorgid}'
|
||
await auth.remember(request, ui)
|
||
```
|
||
|
||
#### `user_logout(request)`
|
||
清除认证状态。
|
||
|
||
```python
|
||
await auth.forget(request)
|
||
```
|
||
|
||
> 🧽 清除的是服务器端票据与客户端 Cookie。
|
||
|
||
---
|
||
|
||
### 5. 权限控制中间件:`checkAuth`
|
||
|
||
通过 `@web.middleware` 装饰器注册为全局中间件,负责:
|
||
|
||
1. 记录访问开始时间
|
||
2. 获取当前用户身份
|
||
3. 调用 `checkUserPermission()` 判断是否有权访问路径
|
||
4. 记录耗时与异常信息
|
||
5. 控制响应流程或抛出 `HTTPUnauthorized` / `HTTPForbidden`
|
||
|
||
#### 日志输出示例
|
||
|
||
```text
|
||
INFO timecost=client(192.168.1.100) U1001 access /api/data cost 0.045, (0.002)
|
||
ERROR Exception=client(192.168.1.100) U1001 access /api/admin/delete cost 0.12, (0.003), except=ValueError...
|
||
```
|
||
|
||
> ✅ 成功请求记录总耗时及权限判断耗时;异常则完整打印 traceback。
|
||
|
||
---
|
||
|
||
### 6. RSA 加解密支持
|
||
|
||
#### 类方法:`AuthAPI`
|
||
|
||
| 方法 | 功能 |
|
||
|------|------|
|
||
| `getPrivateKey()` | 延迟加载私钥文件(仅首次调用读取),避免重复 IO |
|
||
| `rsaDecode(cdata)` | 使用私钥解密 Base64 编码的数据 |
|
||
|
||
依赖:
|
||
- `appPublic.rsawrap.RSA`:封装了 PyCryptodome 的 RSA 操作
|
||
- 配置路径:`conf.website.rsakey.privatekey`
|
||
|
||
典型用途:解密前端传来的加密密码或其他敏感字段。
|
||
|
||
---
|
||
|
||
### 7. 初始化与集成:`setupAuth(app)`
|
||
|
||
此方法完成整个认证系统的初始化,步骤如下:
|
||
|
||
1. **构建 secret 密钥**
|
||
基于端口号拼接固定字符串,补足 32 字节用于 AES 加密(Cookie Storage 所需)
|
||
|
||
2. **选择会话存储方式**
|
||
```python
|
||
if self.conf.website.session_redis:
|
||
redisdb = await redis.Redis.from_url(url)
|
||
storage = MyRedisStorage(redisdb)
|
||
else:
|
||
storage = EncryptedCookieStorage(secret)
|
||
```
|
||
|
||
3. **安装会话中间件**
|
||
```python
|
||
aiohttp_session.setup(app, storage)
|
||
```
|
||
|
||
4. **配置 Ticket 认证策略**
|
||
```python
|
||
policy = SessionTktAuthentication(
|
||
secret=secret,
|
||
max_age=session_max_time,
|
||
reissue_time=session_reissue_time,
|
||
include_ip=True
|
||
)
|
||
auth.setup(app, policy)
|
||
```
|
||
|
||
5. **替换默认 IP 获取逻辑**
|
||
```python
|
||
TktAuthentication._get_ip = get_client_ip # 使用 request['client_ip']
|
||
```
|
||
|
||
6. **注入权限检查中间件**
|
||
```python
|
||
app.middlewares.append(self.checkAuth)
|
||
```
|
||
|
||
---
|
||
|
||
## 配置要求(`jsonConfig` 结构)
|
||
|
||
`getConfig()` 应返回包含以下字段的配置对象:
|
||
|
||
```json
|
||
{
|
||
"website": {
|
||
"port": 8080,
|
||
"rsakey": {
|
||
"privatekey": "/path/to/private.pem"
|
||
},
|
||
"session_max_time": 3600,
|
||
"session_reissue_time": 1800,
|
||
"session_redis": {
|
||
"url": "redis://localhost:6379/0"
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
> ⚠️ 若未设置 `session_redis.url`,则自动降级为本地加密 Cookie 存储。
|
||
|
||
---
|
||
|
||
## 使用方式(集成到 AIOHTTP 应用)
|
||
|
||
```python
|
||
from aiohttp import web
|
||
from your_module import AuthAPI
|
||
|
||
async def init_app():
|
||
app = web.Application()
|
||
|
||
auth_api = AuthAPI()
|
||
await auth_api.setupAuth(app)
|
||
|
||
# 添加路由
|
||
# app.router.add_get('/protected', protected_handler)
|
||
|
||
return app
|
||
|
||
if __name__ == '__main__':
|
||
web.run_app(init_app(), port=8080)
|
||
```
|
||
|
||
---
|
||
|
||
## 安全建议
|
||
|
||
| 项目 | 推荐做法 |
|
||
|------|----------|
|
||
| Secret Key | 不应硬编码,建议从环境变量或密钥管理系统加载 |
|
||
| client_uuid | 前端应在首次访问时生成并持久化(localStorage),每次请求带上 |
|
||
| Redis 安全 | 开启密码认证、限制网络访问 |
|
||
| 日志敏感信息 | 禁止记录用户密码、token 明文 |
|
||
| HTTPS | 生产环境必须启用 TLS,防止 Cookie 被窃听 |
|
||
|
||
---
|
||
|
||
## 扩展接口
|
||
|
||
### `checkUserPermission(request, user, path)`
|
||
抽象方法,子类可覆盖实现 RBAC、ACL 等权限模型。
|
||
|
||
```python
|
||
async def checkUserPermission(self, request, user, path):
|
||
# 示例:仅允许特定用户访问管理员接口
|
||
if path.startswith("/admin") and user != "admin":
|
||
return False
|
||
return True
|
||
```
|
||
|
||
### `needAuth(path)`
|
||
预留钩子,未来可用于跳过某些路径的认证检查。
|
||
|
||
---
|
||
|
||
## 错误处理
|
||
|
||
| 异常场景 | 处理方式 |
|
||
|--------|---------|
|
||
| 未登录访问受保护资源 | 抛出 `HTTPUnauthorized (401)` |
|
||
| 有登录但无权限 | 抛出 `HTTPForbidden (403)` |
|
||
| 内部错误 | 捕获并记录 traceback,重新抛出异常 |
|
||
| Redis 连接失败 | 初始化阶段抛出异常,应用无法启动 |
|
||
|
||
---
|
||
|
||
## 性能考量
|
||
|
||
- **Redis 存储模式**:增加一次网络往返,但支持横向扩展
|
||
- **Ticket 验证**:轻量级 HMAC 验证,性能优异
|
||
- **自动续签机制**:每 `reissue_time` 秒更新票据,延长会话寿命而不影响用户体验
|
||
|
||
---
|
||
|
||
## 总结
|
||
|
||
本模块提供了一套完整的、安全的、可扩展的异步认证解决方案,特点包括:
|
||
|
||
✅ 支持分布式部署(Redis)
|
||
✅ 客户端绑定(UUID + IP)提升安全性
|
||
✅ 细粒度权限控制接口
|
||
✅ 全链路日志跟踪与性能监控
|
||
✅ 支持 RSA 解密敏感数据
|
||
|
||
适用于企业级后台管理系统、API 网关、微服务认证中心等场景。
|
||
|
||
---
|
||
|
||
> 📝 文档版本:v1.0
|
||
> © 2025 公共技术组件团队 |