Files
certificate-system/backend/docs/aliyun-sms-deployment.md

330 lines
8.6 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 阿里云短信服务部署指南
## 前置条件
- 阿里云账号(已实名认证)
- 已备案的域名(短信签名需要)
- 服务器(已部署后端服务)
## 第一步:开通短信服务
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条/小时
- 同一IP5条/分钟
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)