找回密码逻辑:集成 Flask-Mail 发送验证邮件

📂 所属阶段:第三阶段 — 用户系统(安全篇)
🔗 相关章节:Flask-Login 实战 · 密码安全加密


1. 找回密码流程

忘记密码? → 输入邮箱 → 发送重置链接 → 邮件含 token → 点击 token 页面 → 设置新密码

2. 安装 Flask-Mail

pip install flask-mail

3. 配置

# app/extensions.py
from flask_mail import Mail

mail = Mail()

# app/__init__.py
def create_app():
    app = Flask(__name__)
    app.config["MAIL_SERVER"] = "smtp.gmail.com"
    app.config["MAIL_PORT"] = 587
    app.config["MAIL_USE_TLS"] = True
    app.config["MAIL_USERNAME"] = os.getenv("MAIL_USERNAME")
    app.config["MAIL_PASSWORD"] = os.getenv("MAIL_PASSWORD")
    app.config["MAIL_DEFAULT_SENDER"] = ("道满博客", os.getenv("MAIL_USERNAME"))

    mail.init_app(app)

4. Token 生成与验证

4.1 使用 itsdangerous 生成 Token

from itsdangerous import URLSafeTimedSerializer

class TokenManager:
    def __init__(self, secret_key):
        self.serializer = URLSafeTimedSerializer(secret_key)

    def generate_token(self, email):
        """生成密码重置 Token"""
        return self.serializer.dumps(email, salt="password-reset-salt")

    def verify_token(self, token, expiration=3600):
        """验证 Token(默认 1 小时有效)"""
        try:
            email = self.serializer.loads(
                token,
                salt="password-reset-salt",
                max_age=expiration
            )
            return email
        except Exception:
            return None

token_manager = TokenManager(app.config["SECRET_KEY"])

4.2 发送重置邮件

from flask_mail import Message

@auth_bp.route("/forgot-password", methods=["GET", "POST"])
def forgot_password():
    form = ForgotPasswordForm()
    if form.validate_on_submit():
        user = User.query.filter_by(email=form.email.data).first()

        if user:
            # 生成 Token
            token = token_manager.generate_token(user.email)

            # 构造重置链接
            reset_url = url_for("auth.reset_password", token=token, _external=True)

            # 发送邮件
            msg = Message(
                subject="【道满博客】密码重置请求",
                recipients=[user.email],
                body=f"""
您好,{user.username or user.email}

我们收到了您对 {user.email} 账户的密码重置请求。

请在 1 小时内点击以下链接重置密码:
{reset_url}

如果这不是您本人的操作,请忽略此邮件,您的账户安全不受影响。

—— 道满博客团队
                """
            )
            mail.send(msg)

        # 无论用户存不存在都显示成功(防止枚举攻击)
        flash("如果该邮箱已注册,我们已发送重置链接到您的邮箱。", "info")
        return redirect(url_for("auth.login"))

    return render_template("auth/forgot_password.html", form=form)

4.3 重置密码页面

@auth_bp.route("/reset-password/<token>", methods=["GET", "POST"])
def reset_password(token):
    # 验证 Token
    email = token_manager.verify_token(token)
    if not email:
        flash("重置链接已过期或无效,请重新申请。", "danger")
        return redirect(url_for("auth.forgot_password"))

    user = User.query.filter_by(email=email).first()
    if not user:
        flash("用户不存在。", "danger")
        return redirect(url_for("auth.register"))

    form = ResetPasswordForm()
    if form.validate_on_submit():
        user.password = form.password.data  # 自动哈希
        db.session.commit()
        flash("密码已重置,请使用新密码登录!", "success")
        return redirect(url_for("auth.login"))

    return render_template("auth/reset_password.html", form=form, token=token)

5. 安全注意事项

✅ 安全实践
1. Token 设置过期时间(1 小时内有效)
2. 无论用户是否存在都显示同样信息(防枚举)
3. 重置后使旧 Token 失效
4. 记录密码重置日志
5. 发送重置邮件前验证用户是否存在

❌ 避免
1. 在邮件中直接发送明文新密码
2. Token 无过期时间
3. 验证失败时暴露用户是否存在

6. 小结

# 找回密码四步:

# 1. 生成 Token
token = generate_token(email)

# 2. 发送邮件
msg = Message(subject="...", recipients=[email], body="...")
mail.send(msg)

# 3. 验证 Token
email = verify_token(token)  # 过期返回 None

# 4. 重置密码
user.password = new_password  # 自动哈希
db.session.commit()

💡 提示:生产环境推荐使用 SendGrid、Mailgun 等专业邮件服务,自建 SMTP 在发送量和送达率上不如专业服务。


🔗 扩展阅读