vlambda博客
学习文章列表

Flask发送注册邮件

Flask发送注册邮件

1.导读

前面已经简单的学习了Flask上下文,以及Flask启动流程,现在结合Flask Mail,(还有Python相关线程进程的操作).结合这些,简单的实现一下Flask注册邮箱,并发送验证邮件.类似如下:

graph LR;
A(登录页面)-->B(发送验证邮件)-->C(邮箱中验证)

2.搭建基本架构

❯ tree
.
├── app.py         # 主app
├── bpModel    #  Python包文件,主要存放蓝图模板
│   ├── __init__.py
│   └── users.py   # 主路由文件
├── config.py     # 主配置文件
├── exts.py          # 解决双向引入的问题
├── formModel.py    # form模板
├── mailModel.py      # 发送邮件模板
├── manager.py          # 主要基本管理
├── migrations            # 数据库版本迁移文件
│   ├── alembic.ini
│   ├── env.py
│   ├── README
│   ├── script.py.mako
│   └── versions
│       ├── c15ed05a9a50_.py
├── sqlModel.py        # 数据库模板
├── static                   # 静态文件CSS/JS/Image目录
└── templates           # 静态模板
    ├── activate.html      # 激活
    ├── login.html             # 登录
    ├── register.html          # 注册
    └── verification.html     # 注册后返回的页面

以下,从开头的思路出发,一步一步实现.

1.app.py

app文件中,我们应该尽量减少路由,把核心的路由全部布置在bpModel

from flask import Flask
import config
from bpModel import users

app = Flask(__name__)
app.config.from_object(config)


@app.route('/')
def index():
    # 测试路由
    return  'index page'

# 注册blueprint
app.register_blueprint(users.bp)

if __name__ == '__main__':
    app.run()

配置文件config.py

DEBUG = True
TEMPLATES_AUTO_RELOAD = True

蓝图文件bpModel/users.py

from flask import  Blueprint, render_template, redirect, url_for, flash, current_app

bp = Blueprint('users', __name__, url_prefix='/users')

# 设置路由
@bp.route('/register/', methods=['GET', 'POST'])
def register():
        return  render_template('register.html')


@bp.route('/login/', methods=['GET', 'POST'])
def login():
     return  render_template('login.html')

@bp.route('/verification/')
def verification():
      return  render_template('verification.html')

在这一步主要用来实现能访问127.0.0.1:5000/users/register/,即实现基本的路由正常.

2.对接数据库

先写register.html,注意,这里我们使用的表单验证是Flask_form,它是提供SECRET_KEYCSRF保护的,如果已经遗忘的,可以温习https://wenyan.online/2020/10/18/flask-csrfd/.

register.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>register</title>
</head>
<body>
    <form action="" method="post">
        {# 这里实现了一个隐藏的csrf_token,它的写法是固定的  #}
        <input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
        <div><label for="">Username:</label><input type="text" name="username"></div>
        <div><label for="">Email:</label><input type="text" name="email"></div>
        <div><label for="">Password:</label><input type="password" name="password"></div>
        <div><label for="">Password(again):</label><input type="password" name="passwordR"></div>
        <div><input type="checkbox" name="agree">I agree the policy.</div>
        <div><input type="submit" value="Register" name="submit1"></div>
    </form>
</body>
</html>

exts.py

from flask_sqlalchemy import  SQLAlchemy
from flask_wtf.csrf import CSRFProtect

db = SQLAlchemy()
csrf = CSRFProtect()

app.py

# 添加
from exts import db,  csrf

db.init_app(app)
csrf.init_app(app)

表单验证比较简单formModel.py

from flask_wtf import FlaskForm
from wtforms import StringField, PasswordField, BooleanField
from wtforms.validators import Length, Email, EqualTo


class RegisterForm(FlaskForm):
    username = StringField(validators=[Length(620'长度不够')])
    email = StringField(validators=[Email()])
    password = PasswordField(validators=[Length(620'长度不够')])
    passwordR = PasswordField(validators=[EqualTo('password')])
    agree = BooleanField(validators=[]) # 验证为空,默认是False, 选择checkbox 变成True

做好以上的基本准备后,就可以着手路由和数据库的对应.


首先,我们来设计一下数据库,它基本满足以下几个内容即可.

id username passowrd email verification create_time age phone
序列号 用户名 密码 邮箱 是否激活 创建事件 年龄 电话

为了简便,只是截取前几项

sqlModel.py

from exts import db

class Users(db.Model):
    __tablename__ = 'users'

    id = db.Column(db.Integer, primary_key=True, nullable=False, autoincrement=True)
    username = db.Column(db.String(45), nullable=False)
    email = db.Column(db.String(45), nullable=False)
    password = db.Column(db.String(45), nullable=False)
    verification = db.Column(db.Boolean, default=0# flag,标识是否注册激活

有了数据库的对应,基本可以创建数据库连接.

config.py

# 添加
# mysql
# 数据库支持
msg = "mysql+pymysql://root:[email protected]:3306/flask_mail_demo"
SQLALCHEMY_DATABASE_URI = msg
SQLALCHEMY_TRACK_MODIFICATIONS = False # 关闭追

manage.py

from flask_script import Manager
from exts import db
from flask_migrate import MigrateCommand, Migrate
from app import app


# 导入Manager并绑定app
manager = Manager(app)

# 导入flaks_migrate
# Migrate 绑定app,db
Migrate(app, db)
# MigrateCommand 可以使用Alembic的命令
#

manager.add_command('db', MigrateCommand) # db是别名


if __name__  == '__main__':
    manager.run()

然后使用版本控制工具migrate

$ python manager.py db --help   # 测试数据库连接
$ python manager.py db  init   # 初始化
$ python manager.py db migratge  # 生成版本
$ python manager.py db upgrade  # 升级到最新版本

3.生成路由

生成注册路由.

dbModel/users.py

# 设置路由
@bp.route('/register/', methods=['GET', 'POST'])
def register():
    form = RegisterForm()
    if form.validate_on_submit():
        # 如果表单验证成功
        # 添加到数据库中
        username = form.username.data
        email = form.email.data
        password = form.password.data
        user = Users(username=username, email=email, password=password)
        db.session.add(user)
        db.session.commit()
        return redirect(url_for('users.verification', name=username))
    else:
        print(form.errors)
        return  render_template('register.html')

以上,是基本的验证表单,并添加到数据库中的过程,但是我们希望的是,再添加到数据库的同时,一并发送邮件给注册账户的邮箱.

4.邮箱设置

修改config.py,设置基本的邮箱信息

# 配置Mail
MAIL_SERVER = 'smtp.qq.com'
MAIL_PORT = 465
MAIL_USERNAME = '[email protected]'
MAIL_PASSWORD = "imkzuyoomc"
MAIL_USE_SSL = True
MAIL_USE_TLS = False
MAIL_DEFAULT_SENDER = '[email protected]'

注意,这里使用了MAIL_DEFAULT_SENDER.

exts.py文件中引入Flask_mail,并在app.py中绑定app

# app.py
from flask import Flask, render_template
from bpModel import users
import config
from exts import db, mail, csrf


app = Flask(__name__)
app.config.from_object(config)
db.init_app(app)
mail.init_app(app)
csrf.init_app(app)

mailModel.py

from flask_mail import Message
from flask import render_template, current_app
from threading import Thread
from exts import mail

# 异步发送邮件
def async_send_mail(app, msg):
    # 要求在Flask的一次访问中发送邮件,下面代码中新建的线程中并
    # 不包含 上下文结构,手动推送
    with app.app_context():
        mail.send(msg)


def sendMail(to, subject, template, **kwargs):
    try:
        # 创建邮件
        msg = Message(subject, recipients=[to])
        # 回传浏览器
        msg.html = render_template(template + '.html', **kwargs)
        # 创建一个新线程,发送邮件
        # 根据flask上下文,如果不再同一个 app 中,将无法发送邮件
        app = current_app._get_current_object()
        thread = Thread(target=async_send_mail, args=[app, msg])
        thread.start()
        return thread
    except Exception as e:
        print(e)

这里需要注意,我们是在bpModel/users.py下的register中发送邮件,根据flask上下文,它是一个application context(current_app).如果不在同一个,将不能发送邮件,所以这里手动推送了current_app.它的使用:

app_context()
Binds the application only. For as long as the application is bound to the current context the flask.current_app points to that application. An application context is automatically created when a request context is pushed if necessary.

Example usage:

with app.app_context():
    ...

再次修改register路由,发送邮件

# 设置路由
@bp.route('/register/', methods=['GET', 'POST'])
def register():
    form = RegisterForm()
    if form.validate_on_submit():
        # 如果表单验证成功
        # 添加到数据库中
        username = form.username.data
        email = form.email.data
        password = form.password.data
        user = Users(username=username, email=email, password=password)
        db.session.add(user)
        db.session.commit()

        # 生成激活校验的token
        token = user.generate_active_token()
        print(token)
        # 发送激活邮件到注册邮箱
        sendMail(user.email, '账户激活''activate', username=user.username, token=token)
        flash('注册成功,请到你邮箱中点击激活!!!')
        return redirect(url_for('users.verification', name=username))
    else:
        print(form.errors)
        return  render_template('register.html')

注意生成的token,这个token是在sqlModel.py中定义好的.

from exts import db
# 一种加密方式
from itsdangerous import TimedJSONWebSignatureSerializer as Serializer
from flask import current_app

class Users(db.Model):
    __tablename__ = 'users'

    id = db.Column(db.Integer, primary_key=True, nullable=False, autoincrement=True)
    username = db.Column(db.String(45), nullable=False)
    email = db.Column(db.String(45), nullable=False)
    password = db.Column(db.String(45), nullable=False)
    verification = db.Column(db.Boolean, default=0# flag,标识是否注册激活

    # 生成账户激活的token
    def generate_active_token(self, expires_in=3600):
        s = Serializer(current_app._get_current_object().config['SECRET_KEY'], expires_in=expires_in)
        print(s)
        print(current_app._get_current_object().config['SECRET_KEY'])
        return s.dumps({'id':self.id})

itsdangerous主要用来签名和序列化,这个可以后面说,这里简单的理解就是用它来提供一个加密的序列化.

因为,激活邮件的形式:http://127.0.0.1:5000/users/activate/eyJhbGciOiJI/,我希望它是一堆乱码,只有我自己能够加密解密序列化.所以这里使用了SECRET_KEY.

同样的,在Flask上下文中,我们使用current_app获取当前使用的app.


activate.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Mail</title>
</head>
<body>
    <h1>Hello {{ username }}</h1>
    <p>激活请点击右边链接,<a href="{{ url_for('users.activate', token=token, _external=True) }}">激活</a></p>
</body>
</html>

5.账户激活

users.py中设计激活路由

@bp.route('/activate/<token>/')
def activate(token):
    if Users.check_activate_token(token):
        flash('激活成功')
        return redirect(url_for('users.login'))
    else:
        flash('激活失败')
        return redirect(url_for('users.register'))

注意check_activate_token,是已经设计好的解密token

sqlModel.py

from exts import db
# 一种加密方式
from itsdangerous import TimedJSONWebSignatureSerializer as Serializer
from flask import current_app

class Users(db.Model):
    __tablename__ = 'users'

    id = db.Column(db.Integer, primary_key=True, nullable=False, autoincrement=True)
    username = db.Column(db.String(45), nullable=False)
    email = db.Column(db.String(45), nullable=False)
    password = db.Column(db.String(45), nullable=False)
    verification = db.Column(db.Boolean, default=0# flag,标识是否注册激活

    # 生成账户激活的token
    def generate_active_token(self, expires_in=3600):
        s = Serializer(current_app._get_current_object().config['SECRET_KEY'], expires_in=expires_in)
        print(s)
        print(current_app._get_current_object().config['SECRET_KEY'])
        return s.dumps({'id':self.id})

    # 账户激活
    @staticmethod
    def check_activate_token(token):
        s = Serializer(current_app._get_current_object().config['SECRET_KEY'])
        print(s)
        print(current_app._get_current_object().config['SECRET_KEY'])
        try:
            data = s.loads(token)
        except:
            return False
        # 得到用户
        u = Users.query.get(data['id'])
        if not u:
            # 用户不存在
            return False
        if not u.verification:
            # 用户没有激活
            u.verification = 1
            db.session.add(u)
            db.session.commit()
            return True

它的逻辑也很简单,在我生成token的时候,已经包含了id在内,所以,反向激活的时候,只要验证有没有id在,如果有就把flag变成1,用来标记已经激活过了.需要注意的是一定要用同一个SECRET_KEY.(一个dumps,一个loads).

6.注册并验证

运行后,注册,并查看注册邮箱中有无激活邮件,并尝试激活邮件.

点击运行后,能正常访问到login页面.

7.代码参考

https://github.com/ningwenyan/demo_code/tree/master/flask_demo_code/T24

- END -