python邮箱令牌/点击验证、邮箱验证码实现

一、邮箱相关功能概览

当前项目包含 3 种邮箱相关功能 :

二、功能一:注册邮箱验证

1. 后端实现

接口路由 :

  • POST /api/register - 注册时发送验证邮件
  • GET /api/verify-email?token=xxx - 邮件链接直接验证
  • POST /api/verify-email - 前端手动输入令牌验证
  • POST /api/resend-verification-email - 重新发送验证邮件

核心流程 :

PlainText 复制代码
用户注册 → 创建用户(is_active=False) → 生成Token → 存入数据库 → 发送邮
件 → 用户点击链接 → 验证Token → 激活用户(is_active=True)

Token 特性 :

  • 使用 secrets.token_urlsafe(32) 生成安全随机字符串

  • 存储在数据库 verification_token 表

  • 有效期 24 小时

  • 一次性使用(验证后删除)

    邮件内容 :

  • 包含验证按钮(链接到后端验证接口)

  • 显示完整 Token 供复制

  • 提供备用链接(防止按钮无法点击)

python 复制代码
def send_verification_email(user, verification_url, token):
    """发送邮箱验证邮件"""
    try:
        config = current_app.config
        
        # 构建邮件内容
        subject = '房屋租赁系统 - 邮箱验证'
        
        html_content = f"""
        <html>
            <head>
                <style>
                    body {{ font-family: Arial, sans-serif; line-height: 1.6; color: #333; max-width: 600px; margin: 0 auto; padding: 20px; }}
                    .header {{ background-color: #5b6bf8; color: white; padding: 20px; text-align: center; border-radius: 8px; }}
                    .content {{ padding: 20px; background-color: #f9f9f9; border-radius: 8px; margin-top: 20px; }}
                    .button {{ display: inline-block; background-color: #5b6bf8; color: white; padding: 12px 30px; text-decoration: none; border-radius: 6px; margin: 20px 0; }}
                    .token-box {{ background-color: #fff; border: 2px dashed #5b6bf8; border-radius: 8px; padding: 20px; margin: 20px 0; text-align: center; }}
                    .token-label {{ color: #666; font-size: 14px; margin-bottom: 10px; }}
                    .token-code {{ font-size: 28px; font-weight: bold; color: #5b6bf8; letter-spacing: 3px; word-break: break-all; background-color: #f0f4ff; padding: 12px; border-radius: 6px; margin: 10px 0; }}
                    .copy-hint {{ color: #999; font-size: 12px; margin-top: 10px; }}
                    .footer {{ text-align: center; margin-top: 20px; color: #666; font-size: 12px; }}
                </style>
            </head>
            <body>
                <div class="header">
                    <h1>欢迎加入房屋租赁系统</h1>
                </div>
                <div class="content">
                    <h2>尊敬的 {user.username},您好!</h2>
                    <p>感谢您注册房屋租赁系统。为了完成注册,请点击下方按钮验证您的邮箱:</p>
                    <div style="text-align: center;">
                        <a href="{verification_url}" class="button">验证邮箱</a>
                    </div>
                    
                    <div class="token-box">
                        <p class="token-label">您的验证令牌(可直接复制):</p>
                        <div class="token-code">{token}</div>
                        <p class="copy-hint">请复制上方令牌,在验证页面输入完成验证</p>
                    </div>
                    
                    <p>如果按钮无法点击,请复制以下链接到浏览器地址栏:</p>
                    <p style="word-break: break-all; color: #666; font-size: 12px;">{verification_url}</p>
                    <p>本验证链接有效期为 24 小时,过期后请重新申请。</p>
                </div>
                <div class="footer">
                    <p>此邮件由系统自动发送,请勿直接回复。</p>
                </div>
            </body>
        </html>
        """
        
        # 创建邮件
        msg = MIMEMultipart()
        
        # 设置 From 头:QQ邮箱要求使用纯邮箱地址格式
        msg['From'] = config['MAIL_USERNAME']
        msg['To'] = user.email
        msg['Subject'] = subject
        
        msg.attach(MIMEText(html_content, 'html', 'utf-8'))
        
        # 发送邮件
        if config['MAIL_USE_SSL']:
            server = smtplib.SMTP_SSL(config['MAIL_SERVER'], config['MAIL_PORT'])
        else:
            server = smtplib.SMTP(config['MAIL_SERVER'], config['MAIL_PORT'])
            if config['MAIL_USE_TLS']:
                server.starttls()
        
        server.login(config['MAIL_USERNAME'], config['MAIL_PASSWORD'])
        text = msg.as_string()
        server.sendmail(config['MAIL_USERNAME'], user.email, text)
        server.quit()
        
        current_app.logger.info(f"验证邮件已发送到 {user.email}")
        return True
    except Exception as e:
        current_app.logger.error(f"发送验证邮件失败: {str(e)}")
        return False



@auth_bp.route('/register', methods=['POST'])
def register():
    """用户注册(支持短信验证码或邮箱)"""
    data = request.get_json()
    username = data.get('username')
    password = data.get('password')
    phone = data.get('phone')
    email = data.get('email')
    code = data.get('code')
    use_sms = data.get('use_sms', False)
    use_email = data.get('use_email', False)
    
    # 获取极验验证参数
    lot_number = data.get('lot_number')
    captcha_output = data.get('captcha_output')
    pass_token = data.get('pass_token')
    gen_time = data.get('gen_time')
    
    # 验证极验
    if not verify_geetest(lot_number, captcha_output, pass_token, gen_time):
        return jsonify({"msg": "验证码验证失败"}), 400
    
    if use_email:
        # 邮箱注册
        if not username or not password or not email:
            return jsonify({"msg": "用户名、密码和邮箱都是必填项"}), 400
        
        if User.query.filter_by(username=username).first():
            return jsonify({"msg": "用户名已存在"}), 400
        
        if User.query.filter_by(email=email).first():
            return jsonify({"msg": "该邮箱已注册,请直接登录"}), 400
        
        # 创建用户,设置为未激活状态
        hashed_password = generate_password_hash(password)
        new_user = User(
            username=username,
            password_hash=hashed_password,
            email=email,
            phone='',
            is_active=False,
            email_verified=False
        )
        db.session.add(new_user)
        db.session.commit()
        
        # 生成验证令牌
        verification_token = VerificationToken.create_email_verification_token(new_user.id)
        
        # 构建验证链接(后端直接验证)
        verification_url = f"{request.url_root}api/verify-email?token={verification_token.token}"
        
        # 发送验证邮件
        email_sent = send_verification_email(new_user, verification_url, verification_token.token)
        
        return jsonify({
            "msg": "注册成功,请查收邮件验证您的邮箱",
            "email_sent": email_sent,
            "user_id": new_user.id
        })
    elif use_sms:
        # 短信注册(手机注册无需邮箱验证)
        if not phone or not code:
            return jsonify({"msg": "手机号和验证码都是必填项"}), 400
        
        if not username:
            return jsonify({"msg": "用户名是必填项"}), 400
        
        if not password:
            return jsonify({"msg": "密码是必填项"}), 400
        
        config = get_sms_config()
        phone_key = f"register:{phone}"
        
        if phone_key not in sms_codes:
            return jsonify({"msg": "请先获取验证码"}), 400
        
        sms_info = sms_codes[phone_key]
        current_time = time.time()
        
        if sms_info['used']:
            return jsonify({"msg": "验证码已使用"}), 400
        
        if current_time - sms_info['time'] > config['valid_time']:
            return jsonify({"msg": "验证码已过期"}), 400
        
        if sms_info['code'] != code:
            return jsonify({"msg": "验证码错误"}), 400
        
        sms_info['used'] = True
        
        if User.query.filter_by(username=username).first():
            return jsonify({"msg": "用户名已存在"}), 400
        
        if User.query.filter_by(phone=phone).first():
            return jsonify({"msg": "该手机号已注册,请直接登录"}), 400
        
        # 手机注册直接激活
        hashed_password = generate_password_hash(password)
        new_user = User(
            username=username,
            password_hash=hashed_password,
            phone=phone,
            is_active=True
        )
        db.session.add(new_user)
        db.session.commit()
        
        return jsonify({"msg": "注册成功"})
    else:
        # 普通注册(无验证,不推荐)
        if not username or not password:
            return jsonify({"msg": "用户名和密码都是必填项"}), 400
        
        if User.query.filter_by(username=username).first():
            return jsonify({"msg": "用户名已存在"}), 400
        
        hashed_password = generate_password_hash(password)
        new_user = User(
            username=username,
            password_hash=hashed_password,
            phone='',
            is_active=True
        )
        db.session.add(new_user)
        db.session.commit()
        
        return jsonify({"msg": "注册成功"})
python 复制代码
def render_verify_result(status, message):
    """渲染邮箱验证结果页面"""
    if status == 'success':
        icon_class = 'fa-check-circle'
        icon_color = '#52c41a'
        bg_gradient = 'linear-gradient(135deg, #52c41a, #73d13d)'
    else:
        icon_class = 'fa-times-circle'
        icon_color = '#ff4d4f'
        bg_gradient = 'linear-gradient(135deg, #ff4d4f, #ff7875)'
    
    html = f"""
    <!DOCTYPE html>
    <html lang="zh-CN">
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <title>邮箱验证 - 房屋租赁系统</title>
        <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
        <style>
            * {{
                margin: 0;
                padding: 0;
                box-sizing: border-box;
            }}
            body {{
                font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
                background-color: #f5f5f5;
                min-height: 100vh;
                display: flex;
                justify-content: center;
                align-items: center;
                padding: 20px;
            }}
            .container {{
                background-color: #fff;
                border-radius: 24px;
                padding: 60px 40px 40px;
                max-width: 480px;
                width: 100%;
                text-align: center;
                box-shadow: 0 8px 40px rgba(0, 0, 0, 0.12);
            }}
            .icon-wrapper {{
                width: 120px;
                height: 120px;
                background: {bg_gradient};
                border-radius: 50%;
                display: flex;
                justify-content: center;
                align-items: center;
                margin: 0 auto 24px;
                box-shadow: 0 8px 24px rgba(0, 0, 0, 0.15);
            }}
            .icon {{
                font-size: 60px;
                color: #fff;
            }}
            .title {{
                font-size: 28px;
                font-weight: 700;
                color: #333;
                margin-bottom: 16px;
            }}
            .message {{
                font-size: 16px;
                color: #666;
                line-height: 1.6;
                margin-bottom: 32px;
            }}
            .version {{
                margin-top: 32px;
                font-size: 12px;
                color: #999;
            }}
        </style>
    </head>
    <body>
        <div class="container">
            <div class="icon-wrapper">
                <i class="fa-solid {icon_class} icon"></i>
            </div>
            <h1 class="title">{'验证成功' if status == 'success' else '验证失败'}</h1>
            <p class="message">{message}</p>
            <p class="version">房屋出租管理系统 v1.0</p>
        </div>
    </body>
    </html>
    """
    return Response(html, mimetype='text/html')



@auth_bp.route('/verify-email', methods=['GET'])
def verify_email_get():
    """通过邮件链接验证邮箱(GET请求)"""
    token = request.args.get('token')
    
    if not token:
        return render_verify_result('error', '验证令牌不能为空')
    
    # 验证令牌
    verification_token = VerificationToken.verify_token(token, 'email_verification')
    
    if not verification_token:
        return render_verify_result('error', '验证链接无效或已过期')
    
    # 获取用户
    user = User.query.get(verification_token.user_id)
    
    if not user:
        return render_verify_result('error', '用户不存在')
    
    if user.email_verified:
        return render_verify_result('success', '邮箱已经验证过了,您可以直接登录')
    
    # 激活用户
    user.is_active = True
    user.email_verified = True
    db.session.commit()
    
    # 删除令牌
    VerificationToken.delete_token(token)
    
    return render_verify_result('success', '邮箱验证成功,账号已激活!')
python 复制代码
@auth_bp.route('/resend-verification-email', methods=['POST'])
def resend_verification_email():
    """重新发送验证邮件"""
    data = request.get_json()
    email = data.get('email')
    
    if not email:
        return jsonify({"msg": "邮箱不能为空"}), 400
    
    # 获取极验验证参数
    lot_number = data.get('lot_number')
    captcha_output = data.get('captcha_output')
    pass_token = data.get('pass_token')
    gen_time = data.get('gen_time')
    
    # 验证极验
    if not verify_geetest(lot_number, captcha_output, pass_token, gen_time):
        return jsonify({"msg": "验证码验证失败"}), 400
    
    # 查找用户
    user = User.query.filter_by(email=email).first()
    
    if not user:
        return jsonify({"msg": "该邮箱未注册"}), 404
    
    if user.email_verified:
        return jsonify({"msg": "邮箱已经验证过了"}), 400
    
    # 删除旧的验证令牌
    old_tokens = VerificationToken.query.filter_by(
        user_id=user.id,
        token_type='email_verification'
    ).all()
    for token in old_tokens:
        db.session.delete(token)
    db.session.commit()
    
    # 创建新的验证令牌
    verification_token = VerificationToken.create_email_verification_token(user.id)
    
    # 构建验证链接(后端直接验证)
    verification_url = f"{request.url_root}api/verify-email?token={verification_token.token}"
    
    # 发送验证邮件
    email_sent = send_verification_email(user, verification_url, verification_token.token)
    
    return jsonify({
        "msg": "验证邮件已重新发送",
        "email_sent": email_sent
    })

2.前端实现

页面文件 :

  • uniapp/pages/auth/register.vue - 注册页面,注册成功后显示全屏遮罩弹窗

  • uniapp - 邮箱验证页面

    验证页面功能 :

  • 自动验证(URL 带 token 参数时)

  • 手动输入 Token 验证

  • 重新发送验证邮件(带极验验证码)

  • 60秒发送间隔倒计时

三、功能二:忘记密码邮箱验证码

1. 后端实现

接口路由 :

  • POST /api/send-email-code - 发送邮箱验证码
  • POST /api/forgot-password - 使用验证码重置密码
    核心流程 :
PlainText 复制代码
用户输入邮箱 → 极验验证 → 检查邮箱已注册 → 生成6位验证码 → 发送邮件 → 用
户输入验证码 → 验证并重置密码

验证码特性 :

  • 6位数字验证码

  • 存储在内存字典 email_codes

  • 有效期 5 分钟(300秒)

  • 发送间隔 60 秒

  • 一次性使用(验证后标记 used=True)

    邮件内容 :

  • 大字体显示 6 位验证码

  • 提示 5 分钟内有效

  • 简洁的 HTML 模板

python 复制代码
def generate_sms_code(length=6):
    """生成随机短信验证码"""
    return ''.join(random.choice(string.digits) for _ in range(length))

# 内存存储邮箱验证码(生产环境建议使用Redis)
email_codes = {}
email_send_times = {}


def send_email_verification_code(email, code):
    """发送邮箱验证码"""
    try:
        config = current_app.config
        
        subject = '房屋租赁系统 - 忘记密码验证码'
        
        html_content = f"""
        <html>
            <head>
                <style>
                    body {{ font-family: Arial, sans-serif; line-height: 1.6; color: #333; max-width: 600px; margin: 0 auto; padding: 20px; }}
                    .header {{ background-color: #5b6bf8; color: white; padding: 20px; text-align: center; border-radius: 8px; }}
                    .content {{ padding: 20px; background-color: #f9f9f9; border-radius: 8px; margin-top: 20px; }}
                    .token-box {{ background-color: #fff; border: 2px dashed #5b6bf8; border-radius: 8px; padding: 20px; margin: 20px 0; text-align: center; }}
                    .token-label {{ color: #666; font-size: 14px; margin-bottom: 10px; }}
                    .token-code {{ font-size: 36px; font-weight: bold; color: #5b6bf8; letter-spacing: 5px; background-color: #f0f4ff; padding: 15px; border-radius: 6px; margin: 10px 0; }}
                    .copy-hint {{ color: #999; font-size: 12px; margin-top: 10px; }}
                    .footer {{ text-align: center; margin-top: 20px; color: #666; font-size: 12px; }}
                </style>
            </head>
            <body>
                <div class="header">
                    <h1>忘记密码 - 验证码</h1>
                </div>
                <div class="content">
                    <h2>您好!</h2>
                    <p>您正在重置房屋租赁系统的密码,请使用以下验证码完成操作:</p>
                    
                    <div class="token-box">
                        <p class="token-label">您的验证码:</p>
                        <div class="token-code">{code}</div>
                        <p class="copy-hint">请在5分钟内使用此验证码</p>
                    </div>
                    
                    <p>如果您没有进行此操作,请忽略此邮件。</p>
                </div>
                <div class="footer">
                    <p>此邮件由系统自动发送,请勿直接回复。</p>
                </div>
            </body>
        </html>
        """
        
        msg = MIMEMultipart()
        msg['From'] = config['MAIL_USERNAME']
        msg['To'] = email
        msg['Subject'] = subject
        
        msg.attach(MIMEText(html_content, 'html', 'utf-8'))
        
        if config['MAIL_USE_SSL']:
            server = smtplib.SMTP_SSL(config['MAIL_SERVER'], config['MAIL_PORT'])
        else:
            server = smtplib.SMTP(config['MAIL_SERVER'], config['MAIL_PORT'])
            if config['MAIL_USE_TLS']:
                server.starttls()
        
        server.login(config['MAIL_USERNAME'], config['MAIL_PASSWORD'])
        text = msg.as_string()
        server.sendmail(config['MAIL_USERNAME'], email, text)
        server.quit()
        
        current_app.logger.info(f"验证码邮件已发送到 {email}")
        return True
    except Exception as e:
        current_app.logger.error(f"发送验证码邮件失败: {str(e)}")
        return False


@auth_bp.route('/send-email-code', methods=['POST'])
def send_email_code():
    """发送邮箱验证码(用于忘记密码)"""
    data = request.get_json()
    email = data.get('email')
    
    # 获取极验验证参数
    lot_number = data.get('lot_number')
    captcha_output = data.get('captcha_output')
    pass_token = data.get('pass_token')
    gen_time = data.get('gen_time')

    if not email:
        return jsonify({"msg": "邮箱是必填项"}), 400

    # 验证极验
    if not verify_geetest(lot_number, captcha_output, pass_token, gen_time):
        return jsonify({"msg": "验证码验证失败"}), 400

    # 检查邮箱是否已注册
    user = User.query.filter_by(email=email).first()
    if not user:
        return jsonify({"msg": "该邮箱未注册"}), 400

    email_key = f"forget:{email}"
    current_time = time.time()
    email_interval = 60  # 60秒间隔
    email_valid_time = 300  # 5分钟有效

    # 检查是否有未过期的验证码
    if email_key in email_codes:
        email_info = email_codes[email_key]
        if not email_info['used'] and (current_time - email_info['time'] <= email_valid_time):
            return jsonify({"msg": "验证码仍在有效期内,请直接使用"}), 400

    if email_key in email_send_times:
        last_send_time = email_send_times[email_key]
        if current_time - last_send_time < email_interval:
            remaining = int(email_interval - (current_time - last_send_time))
            return jsonify({"msg": f"请{remaining}秒后再试"}), 400

    code = generate_sms_code()

    # 发送验证码邮件
    email_sent = send_email_verification_code(email, code)
    
    if not email_sent:
        return jsonify({"msg": "验证码邮件发送失败,请稍后重试"}), 500

    email_codes[email_key] = {
        'code': code,
        'time': current_time,
        'used': False
    }
    email_send_times[email_key] = current_time

    return jsonify({"msg": "验证码已发送到您的邮箱"})

2.前端实现

页面文件 :

  • uniapp/pages/auth/forget.vue - 忘记密码页面

    功能特性 :

  • 手机号/邮箱切换标签

  • 邮箱格式验证(包含 @)

  • 极验验证码保护

  • 60秒倒计时

  • 表单验证(根据重置类型验证不同字段)

四、功能三:重新发送验证邮件

1. 后端实现

接口路由 :

  • POST /api/resend-verification-email - 重新发送注册验证邮件

核心流程 :

复制代码
用户输入邮箱 → 极验验证 → 查找用户 → 删除旧Token → 生成新Token → 发送邮
件

功能特性 :

  • 需要极验验证码
  • 删除旧的验证令牌
  • 生成新的验证令牌
  • 重新发送邮件
python 复制代码
@auth_bp.route('/resend-verification-email', methods=['POST'])
def resend_verification_email():
    """重新发送验证邮件"""
    data = request.get_json()
    email = data.get('email')
    
    if not email:
        return jsonify({"msg": "邮箱不能为空"}), 400
    
    # 获取极验验证参数
    lot_number = data.get('lot_number')
    captcha_output = data.get('captcha_output')
    pass_token = data.get('pass_token')
    gen_time = data.get('gen_time')
    
    # 验证极验
    if not verify_geetest(lot_number, captcha_output, pass_token, gen_time):
        return jsonify({"msg": "验证码验证失败"}), 400
    
    # 查找用户
    user = User.query.filter_by(email=email).first()
    
    if not user:
        return jsonify({"msg": "该邮箱未注册"}), 404
    
    if user.email_verified:
        return jsonify({"msg": "邮箱已经验证过了"}), 400
    
    # 删除旧的验证令牌
    old_tokens = VerificationToken.query.filter_by(
        user_id=user.id,
        token_type='email_verification'
    ).all()
    for token in old_tokens:
        db.session.delete(token)
    db.session.commit()
    
    # 创建新的验证令牌
    verification_token = VerificationToken.create_email_verification_token(user.id)
    
    # 构建验证链接(后端直接验证)
    verification_url = f"{request.url_root}api/verify-email?token={verification_token.token}"
    
    # 发送验证邮件
    email_sent = send_verification_email(user, verification_url, verification_token.token)
    
    return jsonify({
        "msg": "验证邮件已重新发送",
        "email_sent": email_sent
    })

五、数据库设计

1. User 模型相关字段

2.VerificationToken 模型

六、邮箱发送配置

配置项 (在 config.py 中):

发送方式 :

  • 使用 Python smtplib 库
  • 支持 SSL/TLS 连接
  • HTML 格式邮件
  • From 头使用纯邮箱地址格式

七、安全机制

八、存在的问题和改进建议

相关推荐
倔强的猴子(翻版)1 小时前
我用 Python 写了个排序库,一亿数据量下比 C 级 np.sort() 快 7 倍
人工智能·python·算法·阿里云·文心一言
郝学胜-神的一滴1 小时前
深入理解回归损失函数:MSE、L1 与 Smooth L1 的设计哲学
人工智能·python·程序人生·算法·机器学习·数据挖掘·回归
ZC跨境爬虫1 小时前
Python Django开发者转向微信小程序:从架构理解到第一行代码的完整准备指南
开发语言·python·ui·微信小程序·django
绘梨衣5471 小时前
django-elasticsearch-dsl-drf 搜索服务搭建教学文档
python·elasticsearch·django
测试员周周2 小时前
【AI测试系统】第6篇:需求扔进去,3 分钟出测试用例?AI测试系统的 RAG 知识增强实战
人工智能·python·功能测试·测试工具·测试用例
iCxhust2 小时前
在 emu8086 中可以直接编译运行的完整汇编程序,演示数组的定义、遍历、求和、求最大值。
开发语言·前端·javascript·汇编·单片机·嵌入式硬件·算法
JianZhen✓2 小时前
2026前端高频面试题总结(Vue/JS/网络/Webpack/性能优化/手写)
前端·javascript·vue.js
AI玫瑰助手2 小时前
Python入门:Windows/macOS/Linux系统安装Python教程
windows·python·macos