Flask登录邮箱验证如何防范邮件滥发导致域名被标记为垃圾邮件
风险属性说明
这是所有具备邮箱注册/验证功能的站点都会面临的通用滥发风险,并非个别实现独有的问题。攻击者利用无限制的发信触发逻辑批量向陌生邮箱推送站点邮件,本质是盗用你的邮件服务器发送垃圾信息,轻则导致发件域名被邮箱服务商标记为垃圾源,重则域名被拉黑、正规邮件发送通道被封禁。
现有逻辑的核心缺陷
你当前的实现存在两个明显的可利用点:
- 触发逻辑无门槛:未验证账号只要登录成功就自动发信,不需要用户主动触发,攻击者只要掌握自己注册的虚假未验证账号密码,就可以通过反复登录批量刷发信
- 无频率控制:没有任何发信冷却、次数限制机制,同一邮箱可以在极短时间内收到成百上千封验证邮件,非常容易触发收件人的垃圾邮件举报
可落地的优化方案
1. 取消登录自动发信,改为用户主动触发
将未验证用户登录后的自动发信逻辑移除,登录后统一跳转到专门的邮箱验证提示页,页面展示账号未验证的提示,同时放置一个【重新发送验证邮件】的可点击按钮,仅当用户主动点击该按钮时才触发发信逻辑。按钮点击后直接置灰60秒,防止前端连点。
这一改动可以直接把批量刷信的成本从“脚本自动跑登录请求”拉高到“需要模拟用户交互点击”,大幅降低攻击效率。
2. 全链路加发信频率限制
在用户表新增两个字段用于发信频控:
last_verification_sent_at:DateTime类型,记录上一次发送验证邮件的时间verification_send_count:Integer类型,记录统计周期内的发信次数
发信前做两层校验:
- 冷却拦截:距离上一次发信不足60秒的,直接拒绝发信请求,提示用户稍后再试
- 次数拦截:单个账号24小时内最多发送5封验证邮件,达到阈值后锁定发信权限24小时,到期自动重置
3. 前置注册环节防护
从注册源头减少虚假账号存量:
- 注册接口加滑块/图形验证码,拦截机器批量注册
- 单IP1小时内最多允许注册3个账号,超出后临时限制注册权限
- 新注册用户仅在注册成功时自动发送1封验证邮件,后续发信全部走主动触发+频控逻辑
4. 降低被标记为垃圾邮件的概率
- 所有验证邮件正文明确标注发信原因:“您于[时间]在[站点名]注册账号,如非本人操作请直接忽略本邮件”,不要使用垃圾邮件高频敏感词
- 配置好发件域名的SPF、DKIM、DMARC解析记录,提升域名发信信誉,降低单用户举报对域名整体信誉的影响
- 对累计发送3封以上验证邮件仍未完成验证、且所有邮件链接均无点击记录的邮箱,自动加入静默列表,后续不再向其发送任何站点邮件,避免持续骚扰
代码修改参考
首先修改原有登录路由,移除自动发信逻辑:
@app.route('/login', methods=['GET', 'POST']) def login(): if current_user.is_authenticated: flash('您已完成登录') return redirect(url_for('index')) form = LoginForm() if request.method == 'GET' and 'email' in request.args: form.email.data = request.args.get('email') if form.validate_on_submit(): user = User.query.filter_by(email=form.email.data).first() idea = request.args.get('idea', '') if user is None or not user.check_password(form.password.data): flash('用户名或密码错误', 'error') return redirect(url_for('login', idea=idea)) login_user(user) next_page = request.args.get('next') if not next_page or url_parse(next_page).netloc != '': next_page = url_for('idea', idea=idea) if idea else url_for('home') # 移除自动发信逻辑,未验证用户跳转到专门的提示页 if not user.confirmed: flash(f'账号绑定邮箱{user.email}尚未验证,请完成验证后使用完整功能', 'warning') return redirect(url_for('verify_notice')) return redirect(next_page) return render_template('login.html', title="登录", form=form)
新增主动重发验证邮件的接口,内置频控逻辑:
from datetime import datetime, timedelta from flask_login import login_required, current_user @app.route('/resend_verify_email', methods=['POST']) @login_required def resend_verify_email(): # 已验证用户不需要发信 if current_user.confirmed: return redirect(url_for('home')) now = datetime.utcnow() # 60秒冷却拦截 if current_user.last_verification_sent_at and \ now - current_user.last_verification_sent_at < timedelta(seconds=60): flash('操作过于频繁,请1分钟后再尝试', 'error') return redirect(url_for('verify_notice')) # 24小时次数拦截 day_ago = now - timedelta(days=1) if current_user.last_verification_sent_at and \ current_user.last_verification_sent_at > day_ago and \ current_user.verification_send_count >= 5: flash('今日验证邮件发送次数已达上限,请24小时后再尝试', 'error') return redirect(url_for('verify_notice')) # 满足条件发信 send_verification_email(current_user) # 更新发信统计字段 current_user.last_verification_sent_at = now current_user.verification_send_count = (current_user.verification_send_count + 1) if \ (current_user.last_verification_sent_at and current_user.last_verification_sent_at > day_ago) else 1 db.session.commit() flash('验证邮件已发送,请查收收件箱(含垃圾邮件文件夹)', 'success') return redirect(url_for('verify_notice'))
对应User模型需要新增两个字段:
class User(UserMixin, db.Model): # 原有字段(id、email、password_hash、confirmed等)保持不变 last_verification_sent_at = db.Column(db.DateTime) verification_send_count = db.Column(db.Integer, default=0)
内容的提问来源于stack exchange,提问作者dannypernik
相关产品推荐
相关产品推荐

