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:
@@ -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
237
TOKEN_ONLY_MODE.md
Normal 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` - 本文档
|
||||||
42
app.py
42
app.py
@@ -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,14 +273,37 @@ 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
|
||||||
if token_param:
|
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']:
|
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:
|
||||||
secret = token_config.get('secret')
|
token_valid = True
|
||||||
|
secret = token_config.get('secret', '')
|
||||||
|
app.logger.info(f'Token 验证成功: {token_param}')
|
||||||
break
|
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:
|
||||||
|
for token_config in app.config['API_TOKENS']:
|
||||||
|
if token_config.get('enabled') and token_config.get('token') == token_param:
|
||||||
|
secret = token_config.get('secret')
|
||||||
|
break
|
||||||
|
|
||||||
# 解析时间戳
|
# 解析时间戳
|
||||||
timestamp = None
|
timestamp = None
|
||||||
if timestamp_str:
|
if timestamp_str:
|
||||||
@@ -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,
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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
93
templates/base.html
Normal 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 %}
|
||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user