332 lines
8.9 KiB
Markdown
332 lines
8.9 KiB
Markdown
# 阿里云短信服务部署指南
|
||
|
||
> 注意:这份文档是早期短信接入草稿,里面包含“修改 config.py / 替换 sms.py”等旧步骤。当前项目已经内置 `SMS_MODE=local/aliyun` 和阿里云短信发送逻辑,正式部署请以 `docs/正式部署操作手册.md` 的“短信验证码配置”章节为准。
|
||
|
||
## 前置条件
|
||
|
||
- 阿里云账号(已实名认证)
|
||
- 已备案的域名(短信签名需要)
|
||
- 服务器(已部署后端服务)
|
||
|
||
## 第一步:开通短信服务
|
||
|
||
1. 登录阿里云控制台:https://console.aliyun.com
|
||
2. 搜索「短信服务」或访问:https://dysms.console.aliyun.com
|
||
3. 点击「免费开通」
|
||
4. 阅读并同意服务协议
|
||
5. 点击「立即开通」
|
||
|
||
## 第二步:申请短信签名
|
||
|
||
短信签名是短信开头的标识,如【培训结业系统】。
|
||
|
||
1. 进入短信服务控制台
|
||
2. 左侧菜单选择「国内消息」→「签名管理」
|
||
3. 点击「添加签名」
|
||
4. 填写信息:
|
||
- **签名名称**:培训结业证书系统(或其他名称,2-12个字符)
|
||
- **适用场景**:选择「自用-验证码」
|
||
- **签名来源**:选择「企事业单位的全称或简称」
|
||
- **上传证明**:营业执照照片
|
||
- **申请说明**:用于学员查询培训结业证书时的短信验证码
|
||
5. 点击「提交」
|
||
6. 等待审核(通常1-2小时)
|
||
|
||
## 第三步:申请短信模板
|
||
|
||
短信模板是短信内容的格式。
|
||
|
||
1. 左侧菜单选择「国内消息」→「模板管理」
|
||
2. 点击「添加模板」
|
||
3. 填写信息:
|
||
- **模板名称**:验证码模板
|
||
- **模板类型**:选择「验证码」
|
||
- **模板内容**:`您的验证码是${code},5分钟内有效,请勿泄露给他人。`
|
||
- **申请说明**:用于学员查询培训结业证书时的身份验证
|
||
4. 点击「提交」
|
||
5. 等待审核(通常1-2小时)
|
||
|
||
**注意**:模板变量格式必须是 `${变量名}`,不能是其他格式。
|
||
|
||
## 第四步:获取 AccessKey
|
||
|
||
AccessKey 用于后端调用阿里云 API。
|
||
|
||
1. 点击右上角头像 →「AccessKey管理」
|
||
2. 或直接访问:https://ram.console.aliyun.com/manage/ak
|
||
3. 点击「创建AccessKey」
|
||
4. 进行身份验证(短信验证码或邮箱验证)
|
||
5. 创建成功后,保存:
|
||
- **AccessKey ID**
|
||
- **AccessKey Secret**
|
||
|
||
**重要**:AccessKey Secret 只显示一次,请立即保存到安全的地方。
|
||
|
||
**安全建议**:
|
||
- 不要将 AccessKey 写在代码里
|
||
- 使用 RAM 子账号的 AccessKey,并只授予短信服务权限
|
||
- 定期轮换 AccessKey
|
||
|
||
## 第五步:配置环境变量
|
||
|
||
在服务器上配置环境变量,编辑 `.env` 文件或系统环境变量:
|
||
|
||
```bash
|
||
# 阿里云短信服务配置
|
||
ALIYUN_ACCESS_KEY_ID=你的AccessKeyID
|
||
ALIYUN_ACCESS_KEY_SECRET=你的AccessKeySecret
|
||
ALIYUN_SMS_SIGN_NAME=你的短信签名
|
||
ALIYUN_SMS_TEMPLATE_CODE=你的模板CODE
|
||
```
|
||
|
||
**示例**:
|
||
```bash
|
||
ALIYUN_ACCESS_KEY_ID=LTAI5tQkB3VRg8xR
|
||
ALIYUN_ACCESS_KEY_SECRET=your_secret_key_here
|
||
ALIYUN_SMS_SIGN_NAME=培训结业系统
|
||
ALIYUN_SMS_TEMPLATE_CODE=SMS_123456789
|
||
```
|
||
|
||
## 第六步:安装依赖
|
||
|
||
```bash
|
||
cd backend
|
||
pip install alibabacloud-dysmsapi20170525==3.0.0
|
||
```
|
||
|
||
或在 `requirements.txt` 中添加:
|
||
```
|
||
alibabacloud-dysmsapi20170525==3.0.0
|
||
```
|
||
|
||
然后执行:
|
||
```bash
|
||
pip install -r requirements.txt
|
||
```
|
||
|
||
## 第七步:配置后端代码
|
||
|
||
编辑 `backend/app/core/config.py`,添加短信配置:
|
||
|
||
```python
|
||
import os
|
||
|
||
class Settings:
|
||
# ... 其他配置 ...
|
||
|
||
# 阿里云短信配置
|
||
ALIYUN_ACCESS_KEY_ID: str = os.getenv("ALIYUN_ACCESS_KEY_ID", "")
|
||
ALIYUN_ACCESS_KEY_SECRET: str = os.getenv("ALIYUN_ACCESS_KEY_SECRET", "")
|
||
ALIYUN_SMS_SIGN_NAME: str = os.getenv("ALIYUN_SMS_SIGN_NAME", "")
|
||
ALIYUN_SMS_TEMPLATE_CODE: str = os.getenv("ALIYUN_SMS_TEMPLATE_CODE", "")
|
||
|
||
settings = Settings()
|
||
```
|
||
|
||
## 第八步:修改短信发送服务
|
||
|
||
编辑 `backend/app/services/sms.py`,替换为真实的短信发送逻辑:
|
||
|
||
```python
|
||
import logging
|
||
import random
|
||
import secrets
|
||
import string
|
||
import time
|
||
|
||
from alibabacloud_dysmsapi20170525.client import Client
|
||
from alibabacloud_dysmsapi20170525 import models as dysmsapi_models
|
||
from alibabacloud_tea_openapi import models as open_api_models
|
||
|
||
from app.core.config import settings
|
||
|
||
logger = logging.getLogger(__name__)
|
||
|
||
SMS_CODE_TTL_SECONDS = 300 # 5分钟有效期
|
||
_sms_codes: dict[str, tuple[str, float]] = {}
|
||
|
||
|
||
def _get_sms_client() -> Client:
|
||
"""获取阿里云短信客户端"""
|
||
config = open_api_models.Config(
|
||
access_key_id=settings.ALIYUN_ACCESS_KEY_ID,
|
||
access_key_secret=settings.ALIYUN_ACCESS_KEY_SECRET,
|
||
)
|
||
config.endpoint = "dysmsapi.aliyuncs.com"
|
||
return Client(config)
|
||
|
||
|
||
def send_sms(phone: str, code: str) -> bool:
|
||
"""发送短信验证码"""
|
||
try:
|
||
client = _get_sms_client()
|
||
request = dysmsapi_models.SendSmsRequest(
|
||
phone_numbers=phone,
|
||
sign_name=settings.ALIYUN_SMS_SIGN_NAME,
|
||
template_code=settings.ALIYUN_SMS_TEMPLATE_CODE,
|
||
template_param=f'{{"code":"{code}"}}',
|
||
)
|
||
response = client.send_sms(request)
|
||
|
||
if response.body.code == "OK":
|
||
logger.info(f"[短信发送成功] 手机号: {phone}")
|
||
return True
|
||
else:
|
||
logger.error(f"[短信发送失败] 手机号: {phone}, 错误: {response.body.message}")
|
||
return False
|
||
except Exception as e:
|
||
logger.error(f"[短信发送异常] 手机号: {phone}, 异常: {str(e)}")
|
||
return False
|
||
|
||
|
||
def generate_sms_code(phone: str) -> str:
|
||
"""生成短信验证码"""
|
||
clean_expired_codes()
|
||
code = "".join(random.choices(string.digits, k=6))
|
||
sms_id = secrets.token_urlsafe(16)
|
||
_sms_codes[sms_id] = (code, time.time() + SMS_CODE_TTL_SECONDS)
|
||
|
||
# 发送短信
|
||
send_sms(phone, code)
|
||
|
||
# 开发环境下记录日志
|
||
logger.info(f"[短信验证码] 手机号: {phone}, 验证码: {code}, 有效期: {SMS_CODE_TTL_SECONDS}秒")
|
||
|
||
return sms_id
|
||
|
||
|
||
def verify_sms_code(sms_id: str, code: str) -> bool:
|
||
"""验证短信验证码"""
|
||
clean_expired_codes()
|
||
record = _sms_codes.pop(sms_id, None)
|
||
if not record:
|
||
return False
|
||
expected, expires_at = record
|
||
if expires_at < time.time():
|
||
return False
|
||
return expected == code.strip()
|
||
|
||
|
||
def clean_expired_codes() -> None:
|
||
"""清理过期的验证码"""
|
||
now = time.time()
|
||
expired = [sms_id for sms_id, (_, expires_at) in _sms_codes.items() if expires_at < now]
|
||
for sms_id in expired:
|
||
_sms_codes.pop(sms_id, None)
|
||
```
|
||
|
||
## 第九步:测试验证
|
||
|
||
### 1. 检查配置
|
||
|
||
```bash
|
||
# 查看环境变量
|
||
echo $ALIYUN_ACCESS_KEY_ID
|
||
echo $ALIYUN_SMS_SIGN_NAME
|
||
echo $ALIYUN_SMS_TEMPLATE_CODE
|
||
```
|
||
|
||
### 2. 重启服务
|
||
|
||
```bash
|
||
# 重启后端服务
|
||
systemctl restart backend
|
||
# 或
|
||
pm2 restart backend
|
||
```
|
||
|
||
### 3. 查看日志
|
||
|
||
```bash
|
||
# 查看日志
|
||
tail -f /var/log/backend/app.log
|
||
```
|
||
|
||
### 4. 测试发送
|
||
|
||
访问公开查询页面,输入手机号,点击「获取验证码」,查看:
|
||
- 是否收到短信
|
||
- 日志是否有发送记录
|
||
|
||
## 常见问题排查
|
||
|
||
### 1. 签名或模板审核不通过
|
||
|
||
**原因**:
|
||
- 签名与备案域名不一致
|
||
- 模板内容不符合规范
|
||
- 证明材料不清晰
|
||
|
||
**解决**:
|
||
- 确保签名与备案域名一致
|
||
- 模板变量格式使用 `${变量名}`
|
||
- 上传清晰的营业执照照片
|
||
|
||
### 2. 发送失败,返回错误码
|
||
|
||
**常见错误码**:
|
||
- `isv.BUSINESS_LIMIT_CONTROL`:发送频率超限
|
||
- `isv.INVALID_PARAMETERS`:参数错误
|
||
- `isv.SMS_SIGNATURE_ILLEGAL`:签名不合法
|
||
- `isv.SMS_TEMPLATE_ILLEGAL`:模板不合法
|
||
|
||
**解决**:
|
||
- 检查签名和模板是否审核通过
|
||
- 检查手机号格式是否正确
|
||
- 检查模板变量格式是否正确
|
||
|
||
### 3. 发送成功但收不到短信
|
||
|
||
**可能原因**:
|
||
- 手机号被运营商拦截
|
||
- 短信内容触发风控
|
||
- 手机号在黑名单中
|
||
|
||
**解决**:
|
||
- 换个手机号测试
|
||
- 检查短信内容是否合规
|
||
- 联系阿里云客服
|
||
|
||
### 4. AccessKey 权限不足
|
||
|
||
**错误信息**:
|
||
```
|
||
Code: InvalidAccessKeyId.NotFound
|
||
```
|
||
|
||
**解决**:
|
||
- 检查 AccessKey ID 是否正确
|
||
- 检查 AccessKey 是否已启用
|
||
- 检查 RAM 子账号权限
|
||
|
||
## 费用说明
|
||
|
||
- **短信费用**:0.045元/条(国内)
|
||
- **免费额度**:新用户有100条免费额度
|
||
- **计费方式**:按条计费,发送成功才计费
|
||
|
||
## 安全建议
|
||
|
||
1. **使用 RAM 子账号**:
|
||
- 创建专门的 RAM 子账号
|
||
- 只授予「短信服务」权限
|
||
- 不使用主账号的 AccessKey
|
||
|
||
2. **限制发送频率**:
|
||
- 同一手机号:1条/分钟,5条/小时
|
||
- 同一IP:5条/分钟
|
||
|
||
3. **监控告警**:
|
||
- 设置短信发送量告警
|
||
- 监控异常发送行为
|
||
|
||
4. **定期轮换 AccessKey**:
|
||
- 建议每3个月轮换一次
|
||
- 轮换时先创建新 Key,再删除旧 Key
|
||
|
||
## 相关文档
|
||
|
||
- [阿里云短信服务官方文档](https://help.aliyun.com/product/44286.html)
|
||
- [短信服务API参考](https://help.aliyun.com/document_detail/101414.html)
|
||
- [短信服务错误码](https://help.aliyun.com/document_detail/101345.html)
|