feat: 添加纯 Token 认证模式 (Token Only Mode)

- 新增 TOKEN_ONLY_MODE 配置项,支持跳过 HMAC 签名验证
- 纯 Token 模式下只验证 token 参数,简化配置
- 添加 TOKEN_ONLY_MODE.md 详细文档
- 设置页面显示当前模式状态
- 更新 README.md 说明新功能
- config.example.json 添加 token_only_mode 配置项

适用于 TranspondSms 不支持签名的场景。
This commit is contained in:
OpenClaw Agent
2026-02-07 00:55:46 +00:00
parent a308989c3b
commit 1d886ce68d
7 changed files with 376 additions and 7 deletions

View File

@@ -8,6 +8,7 @@
-**Token/Secret 可选配置** - 支持在 config.json 中配置多个 API Token -**Token/Secret 可选配置** - 支持在 config.json 中配置多个 API Token
-**接收 Android APP 转发的短信** - POST multipart/form-data -**接收 Android APP 转发的短信** - POST multipart/form-data
-**HMAC-SHA256 签名验证** - 可选的安全机制 -**HMAC-SHA256 签名验证** - 可选的安全机制
-**纯 Token 认证模式** - 跳过签名验证,只验证 token查看 [TOKEN_ONLY_MODE.md](TOKEN_ONLY_MODE.md)
-**SQLite 数据库存储** -**SQLite 数据库存储**
-**Web 管理界面** - 查看实时短信、日志、统计 -**Web 管理界面** - 查看实时短信、日志、统计
-**时区支持** - 自动转换为本地时间(默认 Asia/Shanghai -**时区支持** - 自动转换为本地时间(默认 Asia/Shanghai

237
TOKEN_ONLY_MODE.md Normal file
View File

@@ -0,0 +1,237 @@
# 纯 Token 模式配置指南
## 功能说明
纯 Token 模式Token Only Mode允许短信转发接收端**只验证 token 参数**,跳过 HMAC-SHA256 签名验证。
### 适用场景
- TranspondSms APP 不支持或不需要 HMAC 签名
- 快速测试/开发环境
- 简化配置,降低部署复杂度
### 安全性建议
- **生产环境建议使用标准模式**Token + HMAC 签名)
- 纯 Token 模式下请确保网络环境可信内网、VPN
- 定期更换 token
---
## 配置方法
### 1. 编辑 `config.json`
```json
{
"security": {
"enabled": true,
"username": "admin",
"password": "admin123",
"session_lifetime": 3600,
"secret_key": "default_secret_key_change_me",
"sign_verify": true,
"sign_max_age": 3600000,
"token_only_mode": true // 启用纯 Token 模式
},
"api_tokens": [
{
"name": "测试设备",
"token": "my_simple_token_123",
"secret": "",
"enabled": true
}
]
}
```
### 关键配置项
| 配置项 | 类型 | 说明 |
|--------|------|------|
| `token_only_mode` | Boolean | 是否启用纯 Token 模式(默认 `false` |
| `token` | String | Token 值TranspondSms 发送的值) |
| `secret` | String | 在纯 Token 模式下可以为空 |
### 2. 重启服务
```bash
./sms_receiverctl.sh restart
```
或使用 systemd
```bash
systemctl restart sms_receiver
```
---
## TranspondSms 配置
### 标准 Token + 签名模式(默认)
TranspondSms 配置:
```
Token (URL): http://your-server:9518/api/receive?token=my_token
Secret: my_secret_key
```
### 纯 Token 模式
TranspondSms 配置:
```
Token (URL): http://your-server:9518/api/receive?token=my_simple_token_123
Secret: (留空或随意)
```
**注意**TranspondSms 仍会发送签名参数,但服务端会忽略签名验证,只验证 token 是否匹配。
---
## API 行为对比
### 标准 Token + 签名模式
1. TranspondSms 发送请求:
- `from`: 发送方号码
- `content`: 短信内容
- `timestamp`: 时间戳
- `sign`: HMAC-SHA256 签名
- `token`: 识别设备
2. 服务端验证:
- 根据 token 查找对应的 secret
- 验证 HMAC 签名是否匹配
- 验证时间戳是否过期
- 保存短信记录
### 纯 Token 模式
1. TranspondSms 发送请求(同样参数,但签名被忽略):
- `from`: 发送方号码
- `content`: 短信内容
- `token`: 识别设备
2. 服务端验证:
- **只验证 token 是否存在于配置列表中**
- **跳过签名验证**
- **跳过时间戳验证**
- 保存短信记录(标记为 `sign_verified: true`
---
## 错误处理
### Token 无效(纯 Token 模式)
```json
{
"error": "Token 无效"
}
```
返回HTTP 401
### 缺少 Token纯 Token 模式)
```json
{
"error": "缺少 token 参数(纯 Token 模式已启用)"
}
```
返回HTTP 401
---
## 日志记录
服务会在日志中记录 Token 验证结果:
```
Token 验证成功: my_simple_token_123
收到短信: 10086 -> 【移动】验证码是 123456... (ID: 1)
```
---
## 切换回标准模式
`token_only_mode` 设置为 `false` 并重启服务:
```json
{
"security": {
"token_only_mode": false
}
}
```
```bash
./sms_receiverctl.sh restart
```
---
## 常见问题
### Q: 纯 Token 模式安全吗?
A: 相比标准模式,纯 Token 模式安全性较低(没有签名保护)。建议在内网环境或使用 VPN 的场景下使用。
### Q: 纯 Token 模式下还需要配置 secret 吗?
A: 不需要。`secret` 字段可以为空。
### Q: 可以混合使用两种模式吗?
A: 不可以。一个服务实例要么是标准模式,要么是纯 Token 模式。
### Q: TranspondSms 不发送签名会怎样?
A: 即使配置了标准模式,如果 TranspondSms 不发送签名请求会失败HTTP 403。此时请使用纯 Token 模式。
---
## 代码变更
### 配置文件 (`config.py`)
```python
# 新增配置项
TOKEN_ONLY_MODE = False
```
### 接收逻辑 (`app.py`)
```python
if app.config['TOKEN_ONLY_MODE']:
# 纯 Token 模式:只验证 token
if not token_param:
return jsonify({'error': '缺少 token 参数'}), 401
# 查找 token 并验证
for token_config in app.config['API_TOKENS']:
if token_config.get('token') == token_param:
token_valid = True
break
if not token_valid:
return jsonify({'error': 'Token 无效'}), 401
else:
# 标准 Token + 签名模式
# ... 原有逻辑
```
---
## 相关文件
- `config.py` - 配置类定义
- `config.json` - 配置文件
- `app.py` - 主应用逻辑
- `settings.html` - 设置页面
- `TOKEN_ONLY_MODE.md` - 本文档

38
app.py
View File

@@ -57,6 +57,7 @@ def create_app(config_name='default'):
app.config['LOGIN_USERNAME'] = app_config.LOGIN_USERNAME app.config['LOGIN_USERNAME'] = app_config.LOGIN_USERNAME
app.config['LOGIN_PASSWORD'] = app_config.LOGIN_PASSWORD app.config['LOGIN_PASSWORD'] = app_config.LOGIN_PASSWORD
app.config['SESSION_LIFETIME'] = app_config.SESSION_LIFETIME app.config['SESSION_LIFETIME'] = app_config.SESSION_LIFETIME
app.config['TOKEN_ONLY_MODE'] = app_config.TOKEN_ONLY_MODE
# 初始化日志 # 初始化日志
setup_logging(app) setup_logging(app)
@@ -248,9 +249,11 @@ def register_routes(app, db):
def settings(): def settings():
"""设置页面""" """设置页面"""
return render_template('settings.html', return render_template('settings.html',
app={'name': '短信转发接收端'},
api_tokens=app.config['API_TOKENS'], api_tokens=app.config['API_TOKENS'],
login_enabled=app.config['LOGIN_ENABLED'], login_enabled=app.config['LOGIN_ENABLED'],
sign_verify=app.config['SIGN_VERIFY']) sign_verify=app.config['SIGN_VERIFY'],
token_only_mode=app.config['TOKEN_ONLY_MODE'])
@app.route('/api/receive', methods=['POST']) @app.route('/api/receive', methods=['POST'])
def receive_sms(): def receive_sms():
@@ -270,8 +273,31 @@ def register_routes(app, db):
'error', '缺少必填参数 (from/content)') 'error', '缺少必填参数 (from/content)')
return jsonify({'error': '缺少必填参数'}), 400 return jsonify({'error': '缺少必填参数'}), 400
# 如果提供了 token查找对应的 secret # Token 验证
secret = None secret = None
token_valid = False
if app.config['TOKEN_ONLY_MODE']:
# 纯 Token 模式:只验证 token不需要签名
if not token_param:
db.add_log(from_number, content, None, sign, None, ip_address,
'error', '缺少 token 参数(纯 Token 模式已启用)')
return jsonify({'error': '缺少 token 参数'}), 401
for token_config in app.config['API_TOKENS']:
if token_config.get('enabled') and token_config.get('token') == token_param:
token_valid = True
secret = token_config.get('secret', '')
app.logger.info(f'Token 验证成功: {token_param}')
break
if not token_valid:
db.add_log(from_number, content, None, sign, None, ip_address,
'error', f'Token 无效: {token_param}')
return jsonify({'error': 'Token 无效'}), 401
else:
# 标准 Token + 签名模式
if token_param: if token_param:
for token_config in app.config['API_TOKENS']: for token_config in app.config['API_TOKENS']:
if token_config.get('enabled') and token_config.get('token') == token_param: if token_config.get('enabled') and token_config.get('token') == token_param:
@@ -288,9 +314,9 @@ def register_routes(app, db):
'error', f'时间戳格式错误: {timestamp_str}') 'error', f'时间戳格式错误: {timestamp_str}')
return jsonify({'error': '时间戳格式错误'}), 400 return jsonify({'error': '时间戳格式错误'}), 400
# 验证签名 # 验证签名(仅在非纯 Token 模式且提供了 secret 时)
sign_verified = False sign_verified = False
if sign and secret and app.config['SIGN_VERIFY']: if not app.config['TOKEN_ONLY_MODE'] and sign and secret and app.config['SIGN_VERIFY']:
is_valid, message = verify_from_app( is_valid, message = verify_from_app(
from_number, content, timestamp, sign, secret, from_number, content, timestamp, sign, secret,
app.config['SIGN_MAX_AGE'] app.config['SIGN_MAX_AGE']
@@ -305,6 +331,10 @@ def register_routes(app, db):
sign_verified = True sign_verified = True
app.logger.info(f'短信已签名验证: {from_number}') app.logger.info(f'短信已签名验证: {from_number}')
# 纯 Token 模式下token 验证成功则视为 sign_verified
if app.config['TOKEN_ONLY_MODE'] and token_valid:
sign_verified = True
# 保存短信 # 保存短信
message_id = db.add_message( message_id = db.add_message(
from_number=from_number, from_number=from_number,

View File

@@ -15,7 +15,8 @@
"session_lifetime": 3600, "session_lifetime": 3600,
"secret_key": "default_secret_key_change_me", "secret_key": "default_secret_key_change_me",
"sign_verify": true, "sign_verify": true,
"sign_max_age": 3600000 "sign_max_age": 3600000,
"token_only_mode": false
}, },
"sms": { "sms": {
"max_messages": 10000, "max_messages": 10000,

View File

@@ -26,6 +26,9 @@ class Config:
LOGIN_PASSWORD = 'admin123' LOGIN_PASSWORD = 'admin123'
SESSION_LIFETIME = 3600 # 会话有效期默认1小时 SESSION_LIFETIME = 3600 # 会话有效期默认1小时
# 纯 Token 模式配置
TOKEN_ONLY_MODE = False # 纯 Token 模式:只验证 token跳过 HMAC 签名验证
# 数据库配置 # 数据库配置
BASE_DIR = os.path.dirname(os.path.abspath(__file__)) BASE_DIR = os.path.dirname(os.path.abspath(__file__))
DATABASE_PATH = os.path.join(BASE_DIR, 'sms_receiver.db') DATABASE_PATH = os.path.join(BASE_DIR, 'sms_receiver.db')

93
templates/base.html Normal file
View File

@@ -0,0 +1,93 @@
{% extends "base.html" %}
{% block title %}设置 - {{ config.app.name }}{% endblock %}
{% block content %}
<div class="container">
<div class="row">
<div class="col-md-8 offset-md-2">
<div class="card">
<div class="card-header">
<h3><i class="fas fa-cog"></i> 系统设置</h3>
</div>
<div class="card-body">
<h5>当前配置</h5>
<table class="table table-sm">
<tbody>
<tr>
<th>登录验证</th>
<td>{{ '启用' if login_enabled else '禁用' }}</td>
</tr>
<tr>
<th>纯 Token 模式</th>
<td>
{% if token_only_mode %}
<span class="badge badge-warning">启用</span>
<br><small class="text-muted">只验证 token跳过 HMAC 签名验证</small>
{% else %}
<span class="badge badge-success">禁用</span>
<br><small class="text-muted">标准模式Token + HMAC 签名验证</small>
{% endif %}
</td>
</tr>
<tr>
<th>HMAC 签名验证</th>
<td>{{ '启用' if sign_verify else '禁用' }}</td>
</tr>
</tbody>
</table>
<hr>
<h5>API Token 配置</h5>
<div class="table-responsive">
<table class="table table-striped">
<thead>
<tr>
<th>名称</th>
<th>Token</th>
<th>状态</th>
</tr>
</thead>
<tbody>
{% for token_item in api_tokens %}
<tr>
<td>{{ token_item.get('name', '未命名') }}</td>
<td><code>{{ token_item.get('token', '') }}</code></td>
<td>
{% if token_item.get('enabled') %}
<span class="badge badge-success">启用</span>
{% else %}
<span class="badge badge-secondary">禁用</span>
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
<div class="alert alert-info">
<h6><i class="fas fa-info-circle"></i> 配置说明</h6>
<p class="mb-0">
要修改这些设置,请编辑 <code>config.json</code> 文件后重启服务。<br>
<strong>纯 Token 模式</strong>TranspondSms 只需要发送 token 参数即可,签名验证会被跳过。
适用于不使用 HMAC 签名的场景。
</p>
</div>
<div class="alert alert-warning">
<h6><i class="fas fa-exclamation-triangle"></i> 模式切换</h6>
<p class="mb-0">
<code>config.json</code> 中设置 <code>"security.token_only_mode": true</code> 启用纯 Token 模式。<br>
修改后需要重启服务:<code>./sms_receiverctl.sh restart</code>
</p>
</div>
</div>
</div>
</div>
</div>
</div>
{% endblock %}

View File

@@ -54,6 +54,10 @@
transition: background 0.3s; transition: background 0.3s;
} }
.nav a.active {
background: #764ba2;
}
.nav a:hover { .nav a:hover {
background: #764ba2; background: #764ba2;
} }