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

8.6 KiB
Raw Blame History

阿里云短信服务部署指南

前置条件

  • 阿里云账号(已实名认证)
  • 已备案的域名(短信签名需要)
  • 服务器(已部署后端服务)

第一步:开通短信服务

  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 文件或系统环境变量:

# 阿里云短信服务配置
ALIYUN_ACCESS_KEY_ID=你的AccessKeyID
ALIYUN_ACCESS_KEY_SECRET=你的AccessKeySecret
ALIYUN_SMS_SIGN_NAME=你的短信签名
ALIYUN_SMS_TEMPLATE_CODE=你的模板CODE

示例

ALIYUN_ACCESS_KEY_ID=LTAI5tQkB3VRg8xR
ALIYUN_ACCESS_KEY_SECRET=your_secret_key_here
ALIYUN_SMS_SIGN_NAME=培训结业系统
ALIYUN_SMS_TEMPLATE_CODE=SMS_123456789

第六步:安装依赖

cd backend
pip install alibabacloud-dysmsapi20170525==3.0.0

或在 requirements.txt 中添加:

alibabacloud-dysmsapi20170525==3.0.0

然后执行:

pip install -r requirements.txt

第七步:配置后端代码

编辑 backend/app/core/config.py,添加短信配置:

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,替换为真实的短信发送逻辑:

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. 检查配置

# 查看环境变量
echo $ALIYUN_ACCESS_KEY_ID
echo $ALIYUN_SMS_SIGN_NAME
echo $ALIYUN_SMS_TEMPLATE_CODE

2. 重启服务

# 重启后端服务
systemctl restart backend
# 或
pm2 restart backend

3. 查看日志

# 查看日志
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

相关文档