三亩地 三亩地SAN MU DI · CODE DIARY
ARTICLE DETAIL

日记详情

真实记录编程学习的某一天,欢迎挑你感兴趣的翻一翻。

Flask实现短信验证码功能全流程指南

Flask实现短信验证码功能全流程指南

1. 为什么需要短信验证码功能?

在现代Web应用中,短信验证码已经成为身份验证的标配。我最近为一个电商项目实现用户注册流程时,就深刻体会到它的重要性。相比传统的邮箱验证,短信验证码有三大不可替代的优势:

首先,手机号的唯一性更高。大多数人不会频繁更换手机号,但可能拥有多个邮箱。通过短信验证,我们能更准确地识别用户身份。其次,短信的到达率和打开率远超邮件。实测数据显示,短信在5秒内的到达率超过99%,而邮件的平均打开时间可能需要几小时。最后,从用户体验角度,输入6位数字远比点击邮件链接要便捷得多。

2. Flask项目的基础配置

2.1 创建Flask应用骨架

我习惯使用Python 3.8+和Flask 2.0+的组合,这个版本既稳定又具备现代特性。先用以下命令创建虚拟环境:

python -m venv venv source venv/bin/activate # Linux/Mac venv\Scripts\activate # Windows pip install flask

项目结构建议这样组织:

/sms_verification /app __init__.py routes.py config.py /templates run.py

在config.py中,我会预先设置好这些关键配置:

import os class Config: SECRET_KEY = os.environ.get('SECRET_KEY') or 'your-hard-to-guess-string' SMS_API_KEY = '' # 稍后从云厂商获取 SMS_API_SECRET = '' SMS_SIGN_NAME = '你的签名' # 需提前申请 SMS_TEMPLATE_CODE = 'SMS_123456' # 模板ID

2.2 安装必要依赖

除了Flask核心包,我们还需要几个关键扩展:

pip install requests python-dotenv
  • requests:用于调用HTTP API
  • python-dotenv:管理环境变量

我强烈建议使用.env文件存储敏感信息,记得把它加入.gitignore:

SECRET_KEY=your_actual_secret SMS_API_KEY=your_api_key SMS_API_SECRET=your_api_secret

3. 云厂商API的选择与接入

3.1 主流云厂商对比

我调研过国内三大云服务商的短信服务:

厂商免费额度到达率价格(元/条)特色功能
阿里云100条/月99.5%0.045模板审核快,文档完善
腾讯云200条/月99.3%0.05与企业微信深度整合
华为云50条/月99.1%0.048国际短信支持好

对于中小项目,我推荐阿里云。它的控制台最直观,错误提示也最友好。我在处理"api error: 400"这类问题时,阿里云的文档总能快速定位到原因。

3.2 API密钥获取实战

以阿里云为例,获取凭证的完整流程:

  1. 登录控制台,进入"短信服务"
  2. 申请签名(需企业资质,个人开发者可用测试签名)
  3. 创建模板,内容类似:"您的验证码是${code},5分钟内有效"
  4. 在"AccessKey管理"中创建RAM子账号,仅授予SMS权限

重要安全提示:绝对不要使用主账号AK!遵循最小权限原则,这是我在多个项目中积累的血泪教训。

3.3 封装通用发送类

我习惯将短信逻辑封装成独立类,下面是经过生产验证的代码:

import hashlib import hmac import time import requests from urllib.parse import quote class AliyunSMS: def __init__(self, app=None): self.app = app if app: self.init_app(app) def init_app(self, app): self.key = app.config['SMS_API_KEY'] self.secret = app.config['SMS_API_SECRET'] self.sign_name = app.config['SMS_SIGN_NAME'] self.template_code = app.config['SMS_TEMPLATE_CODE'] def _sign(self, params): sorted_params = sorted(params.items()) canonicalized = '&'.join( f'{k}={quote(str(v), safe="")}' for k, v in sorted_params ) string_to_sign = f'GET&%2F&{quote(canonicalized)}' h = hmac.new( (self.secret + '&').encode(), string_to_sign.encode(), hashlib.sha1 ) return h.hexdigest() def send(self, phone, code): params = { 'SignatureMethod': 'HMAC-SHA1', 'SignatureNonce': str(int(time.time() * 1000)), 'AccessKeyId': self.key, 'SignatureVersion': '1.0', 'Timestamp': time.strftime('%Y-%m-%dT%H:%M:%SZ', time.gmtime()), 'Format': 'JSON', 'Action': 'SendSms', 'Version': '2017-05-25', 'RegionId': 'cn-hangzhou', 'PhoneNumbers': phone, 'SignName': self.sign_name, 'TemplateCode': self.template_code, 'TemplateParam': f'{{"code":"{code}"}}' } params['Signature'] = self._sign(params) try: resp = requests.get( 'https://dysmsapi.aliyuncs.com/', params=params, timeout=5 ) data = resp.json() if data.get('Code') != 'OK': self.app.logger.error(f'SMS failed: {data}') return False return True except Exception as e: self.app.logger.error(f'SMS exception: {str(e)}') return False

这个实现有几个关键点:

  1. 使用HMAC-SHA1签名,这是云API的通用安全要求
  2. 每个请求都有唯一Nonce防止重放攻击
  3. 完善的错误处理和日志记录

4. 验证码业务逻辑实现

4.1 生成随机验证码

看似简单的验证码生成其实有讲究。我见过直接使用random.randint(100000, 999999)的方案,但这存在两个问题:

  1. 可能生成不足6位的数字(虽然概率极低)
  2. 随机性不够强

改进后的版本:

import secrets def generate_code(length=6): return ''.join( str(secrets.randbelow(10)) for _ in range(length) )

使用secrets模块比random更安全,适合密码学场景。

4.2 验证码存储方案

我评估过几种存储方式:

  1. Session存储:简单但不适合分布式环境
  2. 数据库存储:可靠但增加查询开销
  3. Redis存储:最佳选择,性能与可靠性兼备

推荐使用Redis的完整实现:

import redis from datetime import timedelta class CodeStorage: def __init__(self, app=None): self.redis = None if app: self.init_app(app) def init_app(self, app): self.redis = redis.Redis( host=app.config['REDIS_HOST'], port=app.config['REDIS_PORT'], db=app.config['REDIS_DB'], decode_responses=True ) self.expire = app.config.get('CODE_EXPIRE', 300) def save_code(self, phone, code): key = f'sms:{phone}' self.redis.setex(key, timedelta(seconds=self.expire), code) def verify_code(self, phone, code): key = f'sms:{phone}' stored = self.redis.get(key) if not stored: return False self.redis.delete(key) return stored == code

4.3 完整业务流程

结合上述组件,路由层实现示例:

from flask import Blueprint, request, jsonify, current_app from .sms import AliyunSMS, generate_code from .storage import CodeStorage bp = Blueprint('auth', __name__) sms = AliyunSMS() storage = CodeStorage() @bp.route('/send_code', methods=['POST']) def send_code(): phone = request.json.get('phone') if not phone or len(phone) != 11: return jsonify({'error': 'Invalid phone'}), 400 code = generate_code() if not sms.send(phone, code): return jsonify({'error': 'SMS send failed'}), 500 storage.save_code(phone, code) return jsonify({'success': True}) @bp.route('/verify_code', methods=['POST']) def verify_code(): phone = request.json.get('phone') code = request.json.get('code') if not all([phone, code]): return jsonify({'error': 'Missing parameters'}), 400 if not storage.verify_code(phone, code): return jsonify({'error': 'Invalid code'}), 401 # 验证通过后的业务逻辑 return jsonify({'success': True})

5. 生产环境关键优化

5.1 安全防护措施

在实际运营中,我遇到过这些攻击手段:

  1. 短信轰炸:攻击者频繁调用接口导致用户收到大量短信
  2. 验证码爆破:尝试大量组合猜测有效验证码
  3. 接口滥用:利用开放接口发送垃圾内容

对应的防御方案:

频率限制实现:

from flask_limiter import Limiter from flask_limiter.util import get_remote_address limiter = Limiter( app, key_func=get_remote_address, default_limits=["200 per day", "50 per hour"] ) @bp.route('/send_code', methods=['POST']) @limiter.limit("1/5minute") # 同一IP 5分钟只能发1次 def send_code(): # 原有逻辑

验证码复杂度增强:

def generate_complex_code(length=6): chars = '23456789ABCDEFGHJKLMNPQRSTUVWXYZ' # 去掉了易混淆字符 return ''.join(secrets.choice(chars) for _ in range(length))

5.2 性能优化技巧

当用户量增长后,我通过以下优化将短信吞吐量提升了3倍:

  1. 异步发送:使用Celery或RQ将短信发送移出主线程

    @celery.task def async_send_sms(phone, code): sms.send(phone, code) # 在路由中改为 async_send_sms.delay(phone, code)
  2. 连接池优化

    session = requests.Session() adapter = requests.adapters.HTTPAdapter( pool_connections=10, pool_maxsize=100, max_retries=3 ) session.mount('https://', adapter)
  3. 批量发送支持:云厂商通常支持批量接口,可以减少API调用次数

5.3 监控与告警

完善的监控体系应该包括:

  1. 成功率监控(低于95%触发告警)
  2. 延迟监控(P99>1s需要关注)
  3. 余额监控(避免因欠费停服)

我在Prometheus中的关键指标:

from prometheus_client import Counter, Histogram SMS_REQUESTS = Counter( 'sms_requests_total', 'Total SMS requests', ['provider', 'status'] ) SMS_DURATION = Histogram( 'sms_duration_seconds', 'SMS sending duration', ['provider'] ) # 在发送方法中添加 start_time = time.time() try: result = send_actual() status = 'success' if result else 'failure' finally: duration = time.time() - start_time SMS_REQUESTS.labels(provider='aliyun', status=status).inc() SMS_DURATION.labels(provider='aliyun').observe(duration)

6. 常见问题排查指南

6.1 错误代码速查表

我在运维过程中整理的典型错误:

错误代码原因解决方案
400 InvalidSign签名计算错误检查AccessKeySecret和签名算法
400 InvalidTemplate模板未审核通过检查控制台模板状态
403 OutOfService账户欠费充值或检查余额
500 InternalError服务端异常重试或联系技术支持

6.2 调试技巧

当遇到"api error: 400"这类模糊错误时,我的排查步骤:

  1. 开启详细日志

    import http.client http.client.HTTPConnection.debuglevel = 1
  2. 使用Postman重现请求,对比签名参数

  3. 检查时间同步(GMT时间误差需在15分钟内)

  4. 验证模板参数格式,特别是JSON转义

6.3 降级方案

当云服务不可用时,我设计的备用流程:

  1. 自动切换备用云厂商(如阿里云→腾讯云)
  2. 启用邮件验证码作为fallback
  3. 对于核心操作,增加人工审核通道

降级开关实现:

from circuitbreaker import circuit @circuit(failure_threshold=3, recovery_timeout=60) def send_sms_with_circuit_breaker(phone, code): return sms.send(phone, code)

7. 进阶功能扩展

7.1 国际短信支持

处理国际号码时需要特别注意:

  1. 号码格式:+[国家码][号码],如+8613812345678
  2. 时区问题:验证码有效期需要考虑用户所在地时区
  3. 模板差异:不同国家有不同的合规要求

改进后的发送逻辑:

def parse_phone_number(phone): if phone.startswith('+'): return phone elif phone.startswith('0') and len(phone) > 10: return f'+86{phone[1:]}' else: return f'+86{phone}'

7.2 语音验证码集成

对于重要操作,可以增加语音验证码。阿里云的实现差异:

params.update({ 'Action': 'SendBatchVoice', 'CalledNumber': phone, 'VoiceCode': code, 'CalledShowNumber': '显示号码' })

7.3 无感验证方案

结合行为验证码提升体验:

  1. 先进行滑动/点选验证
  2. 通过后自动发送短信
  3. 前端自动填充验证码

前端示例:

// 使用腾讯云验证码 new TencentCaptcha({ ready: function() { // 预加载 }, callback: function(res) { if(res.ret === 0) { fetch('/send_code', { method: 'POST', body: JSON.stringify({phone: '138...'}) }); } } });

8. 项目部署注意事项

8.1 容器化部署

我的Dockerfile配置:

FROM python:3.8-slim WORKDIR /app COPY requirements.txt . RUN pip install --no-cache-dir -r requirements.txt COPY . . ENV FLASK_APP=app ENV FLASK_ENV=production CMD ["gunicorn", "-w 4", "-b :5000", "app:app"]

关键优化:

  1. 使用多阶段构建减小镜像体积
  2. 配置适当的Gunicorn worker数量
  3. 分离配置文件和密钥

8.2 性能调优

生产环境实测参数:

# Gunicorn配置 workers = min(4, (os.cpu_count() or 1) * 2 + 1) timeout = 120 keepalive = 75 # Redis连接池 pool = redis.ConnectionPool( max_connections=100, host=config.REDIS_HOST, port=config.REDIS_PORT )

8.3 持续集成

我的CI流程包括:

  1. 单元测试(覆盖所有边界条件)
  2. 集成测试(模拟真实API调用)
  3. 安全扫描(检查依赖漏洞)
  4. 性能基准测试

示例GitLab CI配置:

stages: - test - deploy unit_test: stage: test script: - pytest tests/unit --cov=app --cov-report=xml integration_test: stage: test services: - redis:alpine script: - pytest tests/integration --disable-warnings deploy_prod: stage: deploy when: manual only: - master script: - docker-compose up -d --build
← 返回列表