Features: - Receive SMS from TranspondSms Android APP - HMAC-SHA256 signature verification (optional) - SQLite database storage - Web UI with login authentication - Multiple API tokens support - Timezone conversion (Asia/Shanghai) - Search, filter, and statistics - Auto refresh and session management Tech Stack: - Flask 3.0 - SQLite database - HTML5/CSS3 responsive design
21 KiB
21 KiB
短信转发接收端 - 开发文档
目录
项目概述
技术架构
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ Android APP │ │ Flask Web │ │ SQLite DB │
│ TranspondSms │────────▶│ Server │────────▶│ sms_receiver │
│ │ POST │ │ Store │ .db │
└─────────────────┘ └─────────────────┘ └─────────────────┘
│
▼
┌─────────────────┐
│ Web UI │
│ (登录认证) │
└─────────────────┘
核心功能
- 短信接收:接收 TranspondSms Android APP 转发的短信
- 签名验证:HMAC-SHA256 签名验证,防止伪造请求
- 数据存储:SQLite 数据库存储短信和日志
- Web 管理:登录验证 + 短信列表、详情、日志、统计
- 时区转换:UTC 存储时区转换显示
- Token 配置:支持多设备、多 Token 配置
实现逻辑
1. 整体数据流
Android APP 接收短信
│
├─ 解析短信内容(支持多PDU)
├─ 提取发送方、内容、时间戳
├─ 可选:生成签名(HMAC-SHA256)
│
▼
POST 请求发送到 /api/receive
│
├─ 解析 multipart/form-data
├─ 验证必填参数(from, content)
├─ 可选:验证签名
│ ├─ 检查时间戳是否过期
│ ├─ 生成期望的签名
│ └─ 比较签名是否匹配
│
├─ 保存到数据库
│ ├─ 存储为 UTC 时间
│ ├─ 关联 Token 和 Secret
│ └─ 记录接收日志
│
▼
返回成功响应
2. 签名验证逻辑
TranspondSms 的签名规则:
# 1. 拼接待签名字符串
string_to_sign = timestamp + "\n" + secret
# 2. HMAC-SHA256 计算
hmac_code = hmac.new(
secret.encode('utf-8'),
string_to_sign.encode('utf-8'),
digestmod=hashlib.sha256
).digest()
# 3. Base64 编码
sign_bytes = base64.b64encode(hmac_code)
# 4. URL 编码
sign = urllib.parse.quote(sign_bytes.decode())
防重放攻击:
- 检查时间戳是否在允许范围内(默认1小时)
- 超出范围的请求拒绝
3. 时区转换逻辑
数据库存储(UTC)
│
├─ created_at: 2024-02-06 14:30:00 (UTC)
└─ timestamp: 1707223800000 (毫秒时间戳)
│
▼
读取时转换
│
├─ UTC 时间 + 时区偏移(8小时)
└─ datetime.fromtimestamp(timestamp / 1000)
│
▼
显示(本地时间)
│
└─ created_at: 2024-02-06 22:30:00 (Asia/Shanghai)
4. 登录验证流程
用户访问页面
│
▼
检查 session['logged_in']
│
├─ 已登录 ──▶ 更新 last_activity ──▶ 允许访问
│
└─ 未登录 ──▶ 跳转到 /login
│
▼
提交表单
│
├─ 验证用户名和密码
│
├─ 成功:
│ ├─ session['logged_in'] = True
│ ├─ session['username'] = username
│ ├─ session['login_time'] = now
│ └─ 跳转到原页面或首页
│
└─ 失败:显示错误消息
会话超时检查:
last_activity = session.get('last_activity')
if now - last_activity > SESSION_LIFETIME:
# 清空会话,重定向到登录页
session.clear()
return redirect('/login')
5. Token 匹配逻辑
# 接收请求时
token = request.form.get('token') # 从参数获取
# 在配置中查找对应的 secret
for token_config in API_TOKENS:
if token_config['token'] == token and token_config['enabled']:
secret = token_config['secret']
break
# 使用找到的 secret 进行签名验证
if secret and SIGN_VERIFY:
verify_sign(secret, sign, timestamp)
6. 数据库设计
sms_messages 表
| 字段 | 类型 | 说明 |
|---|---|---|
| id | INTEGER | 主键,自增 |
| from_number | TEXT | 发送方手机号 |
| content | TEXT | 短信内容 |
| timestamp | INTEGER | 原始时间戳(毫秒) |
| device_info | TEXT | 设备信息(可选) |
| sim_info | TEXT | SIM 卡信息(可选) |
| sign_verified | INTEGER | 是否通过签名验证(0/1) |
| ip_address | TEXT | 来源 IP 地址 |
| created_at | TIMESTAMP | 创建时间(UTC) |
receive_logs 表
| 字段 | 类型 | 说明 |
|---|---|---|
| id | INTEGER | 主键,自增 |
| from_number | TEXT | 发送方手机号 |
| content | TEXT | 短信内容 |
| timestamp | INTEGER | 时间戳 |
| sign | TEXT | 签名 |
| sign_valid | INTEGER | 签名是否有效(0/1/null) |
| ip_address | TEXT | IP 地址 |
| status | TEXT | 处理状态(success/error) |
| error_message | TEXT | 错误消息 |
| created_at | TIMESTAMP | 创建时间(UTC)** |
分层架构
┌─────────────────────────────────────────┐
│ Flask Application Layer │
│ (app.py - 路由、业务逻辑、会话管理) │
└─────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────┐
│ Database Layer │
│ (database.py - 数据库操作、时区转换) │
└─────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────┐
│ SQLite Database │
│ (sms_receiver.db - 数据持久化) │
└─────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────┐
│ Template Layer │
│ (templates/ - HTML、CSS、JS) │
└─────────────────────────────────────────┘
核心模块职责
| 模块 | 职责 |
|---|---|
app.py |
Flask 主应用,路由注册,业务逻辑 |
config.py |
配置加载,从 config.json 读取配置 |
database.py |
数据库模型,CRUD 操作,时区转换 |
sign_verify.py |
签名生成和验证 |
templates/ |
HTML 模板,前端展示 |
使用指南
前置要求
- Python 3.7+
- Flask 3.0+
- SQLite3
安装依赖
pip install -r requirements.txt
配置文件
创建 config.json:
{
"server": {
"host": "0.0.0.0",
"port": 9518,
"debug": false
},
"security": {
"enabled": true,
"username": "admin",
"password": "YourStrongPassword123",
"session_lifetime": 3600,
"secret_key": "RandomSecretKeyHere",
"sign_verify": true,
"sign_max_age": 3600000
},
"sms": {
"max_messages": 10000,
"auto_cleanup": true,
"cleanup_days": 30
},
"database": {
"path": "sms_receiver.db"
},
"timezone": "Asia/Shanghai",
"api_tokens": [
{
"name": "我的手机",
"token": "my_phone_token",
"secret": "my_phone_secret",
"enabled": true
}
]
}
启动服务
python3 app.py
服务启动后访问:http://你的IP:9518
配置 TranspondSms APP
- 下载并安装 TranspondSms APP
- 打开 APP,进入"发送方"页面
- 添加"网页通知"
- 填写配置:
Token (URL): http://你的服务器IP:9518/api/receive?token=my_phone_token
Secret: my_phone_secret
- 点击"测试"按钮,验证是否成功
- 配置转发规则(如"转发全部")
使用 Web 界面
登录
- 访问 http://你的IP:9518
- 输入用户名和密码
- 登录成功后进入短信列表
查看短信
- 短信列表:主页显示所有收到短信
- 搜索:支持按号码或内容搜索
- 筛选:按发送方号码快捷筛选
- 详情:点击短信查看完整内容和元数据
查看日志
- 访问"接收日志"页面
- 查看每次请求的处理结果
- 包括签名验证状态、IP 地址、错误信息
统计信息
- 访问"统计信息"页面
- 查看短信总数、今日、本周
- 签名验证比例
- 发送方号码排行榜
- 清理旧数据
部署指南
开发环境部署
# 克隆项目
git clone <your-repo-url>
cd smsweb
# 创建虚拟环境(推荐)
python3 -m venv venv
source venv/bin/activate
# 安装依赖
pip install -r requirements.txt
# 配置 config.json
cp config.example.json config.json
vim config.json
# 启动服务
python3 app.py
生产环境部署(推荐方案)
1. 使用 Gunicorn + Nginx
安装 Gunicorn:
pip install gunicorn
创建 systemd 服务:
sudo vim /etc/systemd/system/sms-receiver.service
内容:
[Unit]
Description=SMS Receiver Service
After=network.target
[Service]
Type=notify
User=www-data
WorkingDirectory=/var/www/sms-receiver
Environment="PATH=/var/www/sms-receiver/venv/bin"
ExecStart=/var/www/sms-receiver/venv/bin/gunicorn \
-w 4 -b 127.0.0.1:9518 \
--timeout 120 \
--access-logfile /var/log/sms-receiver/access.log \
--error-logfile /var/log/sms-receiver/error.log \
app:app
Restart=always
RestartSec=10
[Install]
WantedBy=multi-user.target
启动服务:
sudo systemctl daemon-reload
sudo systemctl enable sms-receiver
sudo systemctl start sms-receiver
sudo systemctl status sms-receiver
配置 Nginx 反向代理:
sudo vim /etc/nginx/sites-available/sms-receiver
内容:
upstream sms_receiver {
server 127.0.0.1:9518;
}
server {
listen 80;
server_name sms.example.com;
access_log /var/log/nginx/sms-receiver-access.log;
error_log /var/log/nginx/sms-receiver-error.log;
location / {
proxy_pass http://sms_receiver;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_redirect off;
proxy_buffering off;
}
# 接收 API 不需要登录,但对客户端透明
location /api/receive {
proxy_pass http://sms_receiver/api/receive;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
}
启用站点:
sudo ln -s /etc/nginx/sites-available/sms-receiver /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl reload nginx
2. 配置 HTTPS(Let's Encrypt)
sudo certbot --nginx -d sms.example.com
3. 防火墙配置
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw enable
4. 日志轮转
sudo vim /etc/logrotate.d/sms-receiver
内容:
/var/log/sms-receiver/*.log {
daily
rotate 14
compress
missingok
notifempty
create 0640 www-data www-data
sharedscripts
postrotate
systemctl reload sms-receiver >/dev/null 2>&1 || true
endscript
}
Docker 部署
创建 Dockerfile:
FROM python:3.11-slim
WORKDIR /app
RUN apt-get update && apt-get install -y \
gcc \
&& rm -rf /var/lib/apt/lists/*
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
EXPOSE 9518
CMD ["gunicorn", "-w", "4", "-b", "0.0.0.0:9518", "app:app"]
创建 docker-compose.yml:
version: '3.8'
services:
sms-receiver:
build: .
ports:
- "9518:9518"
volumes:
- ./sms_receiver.db:/app/sms_receiver.db
- ./config.json:/app/config.json
- ./logs:/app/logs
environment:
- FLASK_ENV=production
restart: unless-stopped
启动:
docker-compose up -d
安全加固
- 修改默认密码:首次部署后立即修改登录密码
- 使用强密码:至少16位,包含大小写字母、数字、特殊字符
- 启用 HTTPS:使用 Let's Encrypt 免费证书
- 限制访问:配置防火墙,只开放必要端口
- 启用签名验证:设置 Token 的 secret
- 定期更新:定期更新 Python 和 Flask 版本
监控和维护
查看日志:
# 应用日志
tail -f /var/log/sms-receiver/error.log
# Nginx 日志
tail -f /var/log/nginx/sms-receiver-error.log
备份数据库:
#!/bin/bash
BACKUP_DIR="/backup/sms-receiver"
DATE=$(date +%Y%m%d_%H%M%S)
mkdir -p $BACKUP_DIR
# 备份数据库
cp /var/www/sms-receiver/sms_receiver.db $BACKUP_DIR/sms_receiver_$DATE.db
# 备份配置
cp /var/www/sms-receiver/config.json $BACKUP_DIR/config_$DATE.json
# 删除30天前的备份
find $BACKUP_DIR -name "*.db" -mtime +30 -delete
find $BACKUP_DIR -name "*.json" -mtime +30 -delete
API 文档
POST /api/receive
接收短信接口。
请求方式:POST multipart/form-data
URL 参数:
token(可选): API Token,用于匹配对应的 secret
表单参数:
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| from | string | 是 | 发送方手机号 |
| content | string | 是 | 短信内容 |
| timestamp | string | 否 | 时间戳(毫秒),用于签名验证 |
| sign | string | 否 | 签名(HMAC-SHA256 + Base64 + URL Encode) |
| device | string | 否 | 设备信息 |
| sim | string | 否 | SIM 卡信息 |
请求示例:
curl -X POST http://your-server:9518/api/receive?token=my_token \
-F "from=10086" \
-F "content=验证码: 123456" \
-F "timestamp=1707223800000" \
-F "sign=xxx"
响应示例:
成功:
{
"success": true,
"message_id": 123,
"message": "短信已接收"
}
失败:
{
"error": "缺少必填参数"
}
HTTP 状态码:
- 200: 成功
- 400: 参数错误
- 403: 签名验证失败
- 500: 服务器错误
GET /api/messages
获取短信列表(需要登录)。
URL 参数:
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| page | int | 否 | 页码,默认1 |
| limit | int | 否 | 每页数量,默认20 |
| from | string | 否 | 按发送方号码筛选 |
| search | string | 否 | 搜索内容或号码 |
响应示例:
{
"success": true,
"data": [
{
"id": 1,
"from_number": "10086",
"content": "验证码: 123456",
"timestamp": 1707223800000,
"local_timestamp": "2024-02-06 22:30:00",
"created_at": "2024-02-06 14:30:00",
"sign_verified": true
}
],
"total": 1,
"page": 1,
"limit": 20
}
GET /api/statistics
获取统计信息(需要登录)。
响应示例:
{
"success": true,
"data": {
"total": 100,
"today": 10,
"week": 50,
"verified": 80,
"unverified": 20
}
}
配置说明
完整配置示例
{
"app": {
"name": "短信转发接收端",
"version": "1.0.0"
},
"server": {
"host": "0.0.0.0",
"port": 9518,
"debug": false
},
"security": {
"enabled": true,
"username": "admin",
"password": "YourStrongPassword123",
"session_lifetime": 3600,
"secret_key": "RandomSecretKeyChangeMe",
"sign_verify": true,
"sign_max_age": 3600000
},
"sms": {
"max_messages": 10000,
"auto_cleanup": true,
"cleanup_days": 30
},
"database": {
"path": "sms_receiver.db"
},
"timezone": "Asia/Shanghai",
"api_tokens": [
{
"name": "主手机",
"token": "main_phone_token",
"secret": "main_phone_secret_key",
"enabled": true
},
{
"name": "备用机",
"token": "backup_phone_token",
"secret": "backup_phone_secret_key",
"enabled": true
},
{
"name": "测试设备",
"token": "test_token",
"secret": "",
"enabled": false
}
]
}
配置项详解
server - 服务器配置
| 字段 | 类型 | 默认值 | 说明 |
|---|---|---|---|
| host | string | 0.0.0.0 | 监听地址 |
| port | int | 9518 | 监听端口 |
| debug | bool | false | 调试模式 |
security - 安全配置
| 字段 | 类型 | 默认值 | 说明 |
|---|---|---|---|
| enabled | bool | true | 是否启用登录验证 |
| username | string | admin | 登录用户名 |
| password | string | admin123 | 登录密码 |
| session_lifetime | int | 3600 | 会话有效期(秒) |
| secret_key | string | - | Flask 会话密钥 |
| sign_verify | bool | true | 是否验证签名 |
| sign_max_age | int | 3600000 | 签名最大有效时间(毫秒) |
sms - 短信配置
| 字段 | 类型 | 默认值 | 说明 |
|---|---|---|---|
| max_messages | int | 10000 | 最多保留短信数 |
| auto_cleanup | bool | true | 是否自动清理老数据 |
| cleanup_days | int | 30 | 清理多少天前的数据 |
api_tokens - API Token 配置
| 字段 | 类型 | 说明 |
|---|---|---|
| name | string | 配置名称(可选) |
| token | string | Token 值,用于匹配 secret |
| secret | string | 密钥,用于签名验证(可选) |
| enabled | bool | 是否启用此 Token |
常见问题
Q1: 如何禁用登录验证?
在 config.json 中设置:
{
"security": {
"enabled": false
}
}
Q2:签名验证失败怎么办?
检查以下几点:
- 时间戳是否正确:确保客户端时间准确,误差不超过1小时
- Secret 是否匹配:确保客户端和服务器端的 secret 完全一致
- 签名生成算法:使用正确的算法(HMAC-SHA256)
调试签名:
# 生成签名
import time, hmac, hashlib, base64, urllib.parse
timestamp = str(int(time.time() * 1000))
secret = "your_secret"
string_to_sign = f"{timestamp}\n{secret}"
sign = urllib.parse.quote(base64.b64encode(
hmac.new(secret.encode(), string_to_sign.encode(), hashlib.sha256).digest()
).decode())
print(f"Timestamp: {timestamp}")
print(f"Sign: {sign}")
Q3: 如何配置多个设备?
在 api_tokens 中添加多个配置:
{
"api_tokens": [
{
"name": "设备A",
"token": "device_a",
"secret": "secret_a",
"enabled": true
},
{
"name": "设备B",
"token": "device_b",
"secret": "secret_b",
"enabled": true
}
]
}
在 APP 中配置不同的设备使用不同的 Token。
Q4: 会话总是过期?
调整 session_lifetime:
{
"security": {
"session_lifetime": 86400 // 24小时
}
}
Q5: 如何备份数据?
直接复制数据库文件:
cp sms_receiver.db sms_receiver.db.backup
或者使用 SQLite 导出:
sqlite3 sms_receiver.db ".dump" > backup.sql
Q6: 如何清理所有数据?
删除数据库文件,重启服务会自动重建:
rm sms_receiver.db
python3 app.py
Q7: 时间显示不正确?
检查时区配置:
{
"timezone": "Asia/Shanghai"
}
可用时区列表:https://en.wikipedia.org/wiki/List_of_tz_database_time_zones
开发规范
代码风格
- 遵循 PEP 8 Python 代码规范
- 使用有意义的变量和函数名
- 添加必要的类型注解
Git 提交规范
feat: 添加新功能
fix: 修复 bug
docs: 更新文档
style: 代码格式化
refactor: 重构
test: 添加测试
chore: 构建/工具链
测试建议
# 测试签名生成
python3 sign_verify.py
# 测试 API
curl -X POST http://localhost:9518/api/receive \
-F "from=10086" \
-F "content=test"
许可证
MIT License
联系方式
- 项目地址:https://gitea.king.nyc.mn/openclaw/smsweb
- 问题反馈:提交 Issue