Flask--微博客

这篇博客介绍了如何使用Python的Flask框架构建一个微博客应用,包括登录、表单验证、数据库模型、模板、分页、密码重置等功能。作者详细讲解了每个部分的实现,如使用LoginForm处理登录,定义数据库模型User和Post,以及如何实现分页和密码更改的邮箱确认过程。此外,还讨论了项目结构的组织方式,如使用blueprint进行模块化管理,并提到了Serializer在处理token令牌中的作用。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

结合作者的教程,也是加了个人的总结与精炼,源作者github:https://github.com/CoreyMSchafer/code_snippets/tree/master/Python/Flask_Blog/03-Forms-and-Validation

#01

@app.route("/login", methods=['GET', 'POST'])
def login():
    form = LoginForm()
    if form.validate_on_submit():
        if form.email.data == 'admin@blog.com' and form.password.data == 'password':
            flash('You have been logged in!', 'success')
            return redirect(url_for('home'))
        else:
            flash('Login Unsuccessful. Please check username and password', 'danger')
    return render_template('login.html', title='Login', form=form)

@app.route("/login", methods=[‘GET’, ‘POST’]),表示请求方法的路径及允许的方法,下面定义了一个名叫login的方法。form = LoginForm(),表示生成一个loginform类的实例,也就是一张含有登录数据的表单。

紧接着,来一个判断,如果提交了并且邮箱和密码都符合我写死的数据,则输出一个信息 flash(‘You have been logged in!’, ‘success’),然后,重定向到home页面;如果,数据不对,那就也返回一个错误的信息。

#02

posts = [
    {
        'author': 'Corey Schafer',
        'title': 'Blog Post 1',
        'content': 'First post content',
        'date_posted': 'April 20, 2018'
    },
    {
        'author': 'Jane Doe',
        'title': 'Blog Post 2',
        'content': 'Second post content',
        'date_posted': 'April 21, 2018'
    }
]

数据在前期可以写死,写成字典的形式,像以上这样。

#03 templates

关于templates文件夹,这个文件夹的名字是不能乱改的,里面就是我们熟知html页面。但是页面多的话要每个都老老实实的写吗?答案显然不是,这里我们定义一个页面让它成为一个模板,就是说其他页面都可以利用或者说依赖这个模板页面。

姑且叫模板页面为layout.html,其他页面如home,about等等,这些页面往模板里面注入数据,我们把这些页面叫做活页面

这是一个名叫about的活页面,第一行表示继承了模板页面layout,下面的block,endblock,里面就是填活数据的地方。

以下,就是两个活页面。

About.html:简单的。

{% extends "layout.html" %}
{% block content %}
    <h1>About Page</h1>
{% endblock content %}

login.html:较复杂,登录表单就这样写。

{% extends "layout.html" %}
{% block content %}
    <div class="content-section">
        <form method="POST" action="">
            {{ form.hidden_tag() }}
            <fieldset class="form-group">
                <legend class="border-bottom mb-4">Log In</legend>
                <div class="form-group">
                    {{ form.email.label(class="form-control-label") }}
                    {% if form.email.errors %}
                        {{ form.email(class="form-control form-control-lg is-invalid") }}
                        <div class="invalid-feedback">
                            {% for error in form.email.errors %}
                                <span>{{ error }}</span>
                            {% endfor %}
                        </div>
                    {% else %}
                        {{ form.email(class="form-control form-control-lg") }}
                    {% endif %}
                </div>
                <div class="form-group">
                    {{ form.password.label(class="form-control-label") }}
                    {% if form.password.errors %}
                        {{ form.password(class="form-control form-control-lg is-invalid") }}
                        <div class="invalid-feedback">
                            {% for error in form.password.errors %}
                                <span>{{ error }}</span>
                            {% endfor %}
                        </div>
                    {% else %}
                        {{ form.password(class="form-control form-control-lg") }}
                    {% endif %}
                </div>
                <div class="form-check">
                    {{ form.remember(class="form-check-input") }}
                    {{ form.remember.label(class="form-check-label") }}
                </div>
            </fieldset>
            <div class="form-group">
                {{ form.submit(class="btn btn-outline-info") }}
            </div>
            <small class="text-muted ml-2">
                <a href="#">Forgot Password?</a>
            </small>
        </form>
    </div>
    <div class="border-top pt-3">
        <small class="text-muted">
            Need An Account? <a class="ml-2" href="{{ url_for('register') }}">Sign Up Now</a>
        </small>
    </div>
{% endblock content %}

再来看看模板页面长啥样,就是写的最完整的页面,仔细看,里面也有一对block,endblock,这里正好与上面的一致,是巧合吗?嘿嘿,显然不是,这里就是活数据要传的地方。

如果写页面的话,只需关注不一样的地方即活数据,剩下的引用公共的模板页面即可。

<!DOCTYPE html>
<html>
<head>
    <!-- Required meta tags -->
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">

    <!-- Bootstrap CSS -->
    <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css" integrity="sha384-Gn5384xqQ1aoWXA+058RXPxPg6fy4IWvTNh0E263XmFcJlSAwiGgFAW/dAiS6JXm" crossorigin="anonymous">

    <link rel="stylesheet" type="text/css" href="{{ url_for('static', filename='main.css') }}">

    {% if title %}
        <title>Flask Blog - {{ title }}</title>
    {% else %}
        <title>Flask Blog</title>
    {% endif %}
</head>
<body>
    <header class="site-header">
      <nav class="navbar navbar-expand-md navbar-dark bg-steel fixed-top">
        <div class="container">
          <a class="navbar-brand mr-4" href="/">Flask Blog</a>
          <button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarToggle" aria-controls="navbarToggle" aria-expanded="false" aria-label="Toggle navigation">
            <span class="navbar-toggler-icon"></span>
          </button>
          <div class="collapse navbar-collapse" id="navbarToggle">
            <div class="navbar-nav mr-auto">
              <a class="nav-item nav-link" href="{{ url_for('home') }}">Home</a>
              <a class="nav-item nav-link" href="{{ url_for('about') }}">About</a>
            </div>
            <!-- Navbar Right Side -->
            <div class="navbar-nav">
              <a class="nav-item nav-link" href="{{ url_for('login') }}">Login</a>
              <a class="nav-item nav-link" href="{{ url_for('register') }}">Register</a>
            </div>
          </div>
        </div>
      </nav>
    </header>
    <main role="main" class="container">
      <div class="row">
        <div class="col-md-8">
          {% with messages = get_flashed_messages(with_categories=true) %}
            {% if messages %}
              {% for category, message in messages %}
                <div class="alert alert-{{ category }}">
                  {{ message }}
                </div>
              {% endfor %}
            {% endif %}
          {% endwith %}
          {% block content %}{% endblock %}
        </div>
        <div class="col-md-4">
          <div class="content-section">
            <h3>Our Sidebar</h3>
            <p class='text-muted'>You can put any information here you'd like.
              <ul class="list-group">
                <li class="list-group-item list-group-item-light">Latest Posts</li>
                <li class="list-group-item list-group-item-light">Announcements</li>
                <li class="list-group-item list-group-item-light">Calendars</li>
                <li class="list-group-item list-group-item-light">etc</li>
              </ul>
            </p>
          </div>
        </div>
      </div>
    </main>


    <!-- Optional JavaScript -->
    <!-- jQuery first, then Popper.js, then Bootstrap JS -->
    <script src="https://code.jquery.com/jquery-3.2.1.slim.min.js" integrity="sha384-KJ3o2DKtIkvYIK3UENzmM7KCkRr/rE9/Qpg6aAZGJwFDMVNA/GpGFF93hXpG5KkN" crossorigin="anonymous"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.12.9/umd/popper.min.js" integrity="sha384-ApNbgh9B+Y1QKtv3Rn7W3mgPxhU9K/ScQsAP7hUibX39j7fakFPskvXusvfa0b4Q" crossorigin="anonymous"></script>
    <script src="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/js/bootstrap.min.js" integrity="sha384-JZR6Spejh4U02d8jOt6vLEHfe/JQGiRRSQQxSfFWpi1MquVdAyjUar5+76PVCmYl" crossorigin="anonymous"></script>
</body>
</html>

通过观察代码,还可以看到,这个项目要用bootstrap和jquery。

利用框架的好处,就是简化我们美化页面的时间,前期练习项目都可以考虑用框架,还有许多优秀的框架都可以用,阿里的antd,semantic,element等等,都不错。

好了,这个模板已经解决布局等一些的死问题,剩下的我们就专心写我们想写的就ok。

#04 forms

专门定义表的文件:forms.py.

class LoginForm(FlaskForm):
    email = StringField('Email',
                        validators=[DataRequired(), Email()])
    password = PasswordField('Password', validators=[DataRequired()])
    remember = BooleanField('Remember Me')
    submit = SubmitField('Login')

这里只以其中一个表为例,更详细的,直接去作者的github去看就行,这里只让我们懂得简单的用法即可。

StringField,PasswordField,BooleanField,SubmitField,这些都是要定义的内容,相当于表的配置

到时候,直接在html文件里像这样,直接调用出标签,输入框,这都是flask封装好的,满足基本需求没问题。

{{ form.email.label(class=“form-control-label”) }}

{{ form.email(class=“form-control form-control-lg”) }}

#05 models

关于数据库,首先导入from flask_sqlalchemy import SQLAlchemy,这个库负责把类映射到数据库,简单点说,就是,用类的方式写数据库的表。

专业点:Object-Relational Mapping,把关系数据库的表结构映射到对象上。

导入后,就要配置,并且绑定到这个app.

app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///site.db'
db = SQLAlchemy(app)

也很简单,先看看user,post这两个表。

class User(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    username = db.Column(db.String(20), unique=True, nullable=False)
    email = db.Column(db.String(120), unique=True, nullable=False)
    image_file = db.Column(db.String(20), nullable=False, default='default.jpg')
    password = db.Column(db.String(60), nullable=False)
    posts = db.relationship('Post', backref='author', lazy=True)

    def __repr__(self):
        return f"User('{self.username}', '{self.email}', '{self.image_file}')"


class Post(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    title = db.Column(db.String(100), nullable=False)
    date_posted = db.Column(db.DateTime, nullable=False, default=datetime.utcnow)
    content = db.Column(db.Text, nullable=False)
    user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)

    def __repr__(self):
        return f"Post('{self.title}', '{self.date_posted}')"

那这两个表的关系显然是一对多,一个用户可以有多个文章,这时就需要用外键了,多的一方用外键连接少的一方简单声明即可

 //少的一方
 posts = db.relationship('Post', backref='author', lazy=True)


//多的一方
 user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)

这样就建完表,并且表的关系也完善了。

repr() 方法是类的实例化对象用来做“自我介绍”的方法,当要打印对象时,会返回一个自定义的信息。

#06 第一次整理结构

随着代码量的增加,当项目初步具备一些功能时,就需要考虑代码之间的解耦。用一个好的文件结构是不错的选择。像这样整理结构。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-zC2fuijq-1583331298522)(C:\Users\G3\AppData\Roaming\Typora\typora-user-images\1582974946168.png)]

把路由,数据库module,表单这些都分离开来。

run.py

from flaskblog import app

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

#07实现登录

接下来,先实现登录授权。

首先,注册的账号和邮箱不能重复,加一个验证器。

class RegistrationForm(FlaskForm):
    username = StringField('Username',
                           validators=[DataRequired(), Length(min=2, max=20)])
    email = StringField('Email',
                        validators=[DataRequired(), Email()])
    password = PasswordField('Password', validators=[DataRequired()])
    confirm_password = PasswordField('Confirm Password',
                                     validators=[DataRequired(), EqualTo('password')])
    submit = SubmitField('Sign Up')

    def validate_username(self, username):
        user = User.query.filter_by(username=username.data).first()
        if user:
            raise ValidationError('That username is taken. Please choose a different one.')

    def validate_email(self, email):
        user = User.query.filter_by(email=email.data).first()
        if user:
            raise ValidationError('That email is taken. Please choose a different one.')

在登陆的时候,考虑到安全问题,这里用bcrypt来加密。即当表单提交时,其中的密码需要用hash的方式处理。

@app.route("/register", methods=['GET', 'POST'])
def register():
    if current_user.is_authenticated:
        return redirect(url_for('home'))
    form = RegistrationForm()
    if form.validate_on_submit():
        hashed_password = bcrypt.generate_password_hash(form.password.data).decode('utf-8')
        user = User(username=form.username.data, email=form.email.data, password=hashed_password)
        db.session.add(user)
        db.session.commit()
        flash('Your account has been created! You are now able to log in', 'success')
        return redirect(url_for('login'))
    return render_template('register.html', title='Register', form=form)

数据映射为对象,注入到session,成功后重定向到login页面。

在init函数里,导入bcrypt,flask_login,绑定到我们的app,如下。

SECRET_KEY 配置变量是通用密钥, 可在 Flask 和多个第三方扩展中使用. 如其名所示, 加密的

强度取决于变量值的机密度. 不同的程序要使用不同的密钥, 而且要保证其他人不知道你所用的字符串.

其主要作用应该是在各种加密过程中加盐以增加安全性。在实际应用中最好将这个参数存储为系统环境变量。

from flask import Flask
from flask_sqlalchemy import SQLAlchemy
from flask_bcrypt import Bcrypt
from flask_login import LoginManager

app = Flask(__name__)
app.config['SECRET_KEY'] = '5791628bb0b13ce0c676dfde280ba245'
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///site.db'
db = SQLAlchemy(app)
bcrypt = Bcrypt(app)
login_manager = LoginManager(app)
login_manager.login_view = 'login'
login_manager.login_message_category = 'info'

from flaskblog import routes


from flask_login import login_user, current_user, logout_user, login_required

从flask_login导出处理好的四个函数,可以方便的管理用户会话:current_user为当前用户,current_user.is_authenticated判断用户是否为登录状态;

logout_user()退出用户;

@login_required必须登录后才能访问,否则弹到我们定义的login页面;

login_user(),将登录的数据和是否记住密码,封装好一并提交。

 login_user(user, remember=form.remember.data)
            next_page = request.args.get('next')
            return redirect(next_page) if next_page else redirect(url_for('home'))

#08

Flask-Login就是通过装饰器,来注册回调函数,当没有sessionID时,通过装饰器指定的函数来读取用户到session中,达到在前端模板中调用当前登录用户current_user的目的,该装饰器就是:

@login_manager.user_loader

@login_manager.user_loader
def load_user(user_id):
    return User.query.get(int(user_id))

使用flask_login进行用户的登录和登出管理,需要将我们的User模型继承flask_login的UserMixin基类:

class User(db.Model, UserMixin):
    id = db.Column(db.Integer, primary_key=True)
    username = db.Column(db.String(20), unique=True, nullable=False)
    email = db.Column(db.String(120), unique=True, nullable=False)
    image_file = db.Column(db.String(20), nullable=False, default='default.jpg')
    password = db.Column(db.String(60), nullable=False)
    posts = db.relationship('Post', backref='author', lazy=True)

    def __repr__(self):
        return f"User('{self.username}', '{self.email}', '{self.image_file}')"

#09 添加修改用户信息功能(图片)

@app.route("/account", methods=['GET', 'POST'])
@login_required
def account():
    form = UpdateAccountForm()
    if form.validate_on_submit():
        if form.picture.data:
            picture_file = save_picture(form.picture.data)
            current_user.image_file = picture_file
        current_user.username = form.username.data
        current_user.email = form.email.data
        db.session.commit()
        flash('Your account has been updated!', 'success')
        return redirect(url_for('account'))
   
//这个表示,刚进这个页面时,是以get方法进的,所以表格上的当前用户的信息(修改前)
    elif request.method == 'GET':
        form.username.data = current_user.username
        form.email.data = current_user.email
    image_file = url_for('static', filename='profile_pics/' + current_user.image_file)
    return render_template('account.html', title='Account',
                           image_file=image_file, form=form)

这个我们要自己写save_picture这个方法:i.thumbnail(output_size)限制图片大小,大的会被剪成125*125.

返回一个文件名,保存一个图片路径。

def save_picture(form_picture):
    random_hex = secrets.token_hex(8)
    _, f_ext = os.path.splitext(form_picture.filename)
    picture_fn = random_hex + f_ext
    picture_path = os.path.join(app.root_path, 'static/profile_pics', picture_fn)

    output_size = (125, 125)
    i = Image.open(form_picture)
    i.thumbnail(output_size)
    i.save(picture_path)

    return picture_fn

#10 post的增删改查

对文章的增删改查这里也有很多要讲的,如果是自己的文章的话,可以删除,可以更新;所有用户都可以,增加文章,查询文章。

@app.route("/post/new", methods=['GET', 'POST'])
@login_required
def new_post():
    form = PostForm()
    if form.validate_on_submit():
        post = Post(title=form.title.data, content=form.content.data, author=current_user)
        db.session.add(post)
        db.session.commit()
        flash('Your post has been created!', 'success')
        return redirect(url_for('home'))
    return render_template('create_post.html', title='New Post',
                           form=form, legend='New Post')


@app.route("/post/<int:post_id>")
def post(post_id):
    post = Post.query.get_or_404(post_id)
    return render_template('post.html', title=post.title, post=post)


@app.route("/post/<int:post_id>/update", methods=['GET', 'POST'])
@login_required
def update_post(post_id):
    post = Post.query.get_or_404(post_id)
    if post.author != current_user:
        abort(403)
    form = PostForm()
    if form.validate_on_submit():
        post.title = form.title.data
        post.content = form.content.data
        db.session.commit()
        flash('Your post has been updated!', 'success')
        return redirect(url_for('post', post_id=post.id))
    elif request.method == 'GET':
        form.title.data = post.title
        form.content.data = post.content
    return render_template('create_post.html', title='Update Post',
                           form=form, legend='Update Post')


@app.route("/post/<int:post_id>/delete", methods=['POST'])
@login_required
def delete_post(post_id):
    post = Post.query.get_or_404(post_id)
    if post.author != current_user:
        abort(403)
    db.session.delete(post)
    db.session.commit()
    flash('Your post has been deleted!', 'success')
    return redirect(url_for('home'))

#11. 分析详情页

先看一下,详情页的全部代码。

{% extends "layout.html" %}
{% block content %}
  <article class="media content-section">
    <img class="rounded-circle article-img" src="{{ url_for('static', filename='profile_pics/' + post.author.image_file) }}">
    <div class="media-body">
      <div class="article-metadata">
        <a class="mr-2" href="#">{{ post.author.username }}</a>
        <small class="text-muted">{{ post.date_posted.strftime('%Y-%m-%d') }}</small>
        {% if post.author == current_user %}
          <div>
            <a class="btn btn-secondary btn-sm mt-1 mb-1" href="{{ url_for('update_post', post_id=post.id) }}">Update</a>
            <button type="button" class="btn btn-danger btn-sm m-1" data-toggle="modal" data-target="#deleteModal">Delete</button>
          </div>
        {% endif %}
      </div>
      <h2 class="article-title">{{ post.title }}</h2>
      <p class="article-content">{{ post.content }}</p>
    </div>
  </article>
  <!-- Modal -->
  <div class="modal fade" id="deleteModal" tabindex="-1" role="dialog" aria-labelledby="deleteModalLabel" aria-hidden="true">
    <div class="modal-dialog" role="document">
      <div class="modal-content">
        <div class="modal-header">
          <h5 class="modal-title" id="deleteModalLabel">Delete Post?</h5>
          <button type="button" class="close" data-dismiss="modal" aria-label="Close">
            <span aria-hidden="true">&times;</span>
          </button>
        </div>
        <div class="modal-footer">
          <button type="button" class="btn btn-secondary" data-dismiss="modal">Close</button>
          <form action="{{ url_for('delete_post', post_id=post.id) }}" method="POST">
            <input class="btn btn-danger" type="submit" value="Delete">
          </form>
        </div>
      </div>
    </div>
  </div>
{% endblock content %}

后半段的代码,是直接从bootstrap上copy的,作用是当你点击删除按钮后,弹出一个对话框:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-gmIN297n-1583331298524)(C:\Users\G3\AppData\Roaming\Typora\typora-user-images\1583152432324.png)]

#12.分页

 {% for page_num in posts.iter_pages(left_edge=1, right_edge=1, left_current=1, right_current=2) %}
      {% if page_num %}
        {% if posts.page == page_num %}
          <a class="btn btn-info mb-4" href="{{ url_for('home', page=page_num) }}">{{ page_num }}</a>
        {% else %}
          <a class="btn btn-outline-info mb-4" href="{{ url_for('home', page=page_num) }}">{{ page_num }}</a>
        {% endif %}
      {% else %}
        ...
      {% endif %}
    {% endfor %}

以上代码可实现分页功能,该分页功能主要应用在home页面上。

在路由函数添加部分代码如下,意思是按发布的时间降序,也就是最新的文章在最上面,每页可容纳5篇内容。

@app.route("/")
@app.route("/home")
def home():
    page = request.args.get('page', 1, type=int)
    posts = Post.query.order_by(Post.date_posted.desc()).paginate(page=page, per_page=5)
    return render_template('home.html', posts=posts)

#13.更改密码(通过邮箱确认)

通过填写一个与账号相关的邮箱号,向该邮箱发送一个邮件,再通过邮件,到达重置密码的页面。

不妨称第一个页面为,reset_request,负责发送请求。

{% extends "layout.html" %}
{% block content %}
    <div class="content-section">
        <form method="POST" action="">
            {{ form.hidden_tag() }}
            <fieldset class="form-group">
                <legend class="border-bottom mb-4">Reset Password</legend>
                <div class="form-group">
                    {{ form.email.label(class="form-control-label") }}
                    {% if form.email.errors %}
                        {{ form.email(class="form-control form-control-lg is-invalid") }}
                        <div class="invalid-feedback">
                            {% for error in form.email.errors %}
                                <span>{{ error }}</span>
                            {% endfor %}
                        </div>
                    {% else %}
                        {{ form.email(class="form-control form-control-lg") }}
                    {% endif %}
                </div>
            </fieldset>
            <div class="form-group">
                {{ form.submit(class="btn btn-outline-info") }}
            </div>
        </form>
    </div>
{% endblock content %}

观察发现,里面只有一个填邮箱和一个发送按钮。

点击邮箱里的链接,到下面的页面,叫reset_token,里面是一个填密码和确认密码,以及一个提交按钮。当然,需要注意的是,这个页面需要做个加密,不能说是直接输个url就可以进入,这样的话,没有第一个界面的必要了,而且安全性太低。

怎么办呢?加密一个字符串,拼在url的后面,这样就好多了。

  
{% extends "layout.html" %}
{% block content %}
    <div class="content-section">
        <form method="POST" action="">
            {{ form.hidden_tag() }}
            <fieldset class="form-group">
                <legend class="border-bottom mb-4">Reset Password</legend>
                <div class="form-group">
                    {{ form.password.label(class="form-control-label") }}
                    {% if form.password.errors %}
                        {{ form.password(class="form-control form-control-lg is-invalid") }}
                        <div class="invalid-feedback">
                            {% for error in form.password.errors %}
                                <span>{{ error }}</span>
                            {% endfor %}
                        </div>
                    {% else %}
                        {{ form.password(class="form-control form-control-lg") }}
                    {% endif %}
                </div>
                <div class="form-group">
                    {{ form.confirm_password.label(class="form-control-label") }}
                    {% if form.confirm_password.errors %}
                        {{ form.confirm_password(class="form-control form-control-lg is-invalid") }}
                        <div class="invalid-feedback">
                            {% for error in form.confirm_password.errors %}
                                <span>{{ error }}</span>
                            {% endfor %}
                        </div>
                    {% else %}
                        {{ form.confirm_password(class="form-control form-control-lg") }}
                    {% endif %}
                </div>
            </fieldset>
            <div class="form-group">
                {{ form.submit(class="btn btn-outline-info") }}
            </div>
        </form>
    </div>
{% endblock content %}

当然,以上只是在前端方面做到了支持,具体的内部逻辑还需要引库与配置。

在_init_文件里:

from flask_mail import Mail

app.config['MAIL_SERVER'] = 'smtp.googlemail.com'
app.config['MAIL_PORT'] = 587
app.config['MAIL_USE_TLS'] = True
app.config['MAIL_USERNAME'] = os.environ.get('EMAIL_USER')
app.config['MAIL_PASSWORD'] = os.environ.get('EMAIL_PASS')
mail = Mail(app)

1MAIL_SERVER电子邮件服务器的名称/IP地址
2MAIL_PORT使用的服务器的端口号
3MAIL_USE_TLS启用/禁用传输安全层加密
4MAIL_USE_SSL启用/禁用安全套接字层加密
5MAIL_DEBUG调试支持。默认值是Flask应用程序的调试状态
6MAIL_USERNAME发件人的用户名
7MAIL_PASSWORD发件人的密码
8MAIL_DEFAULT_SENDER设置默认发件人
9MAIL_MAX_EMAILS设置要发送的最大邮件数
10MAIL_SUPPRESS_SEND如果app.testing设置为true,则发送被抑制
11MAIL_ASCII_ATTACHMENTS如果设置为true,则附加的文件名将转换为ASCII

在路由文件中,可以看到三个函数:先是发送请求,里面写了个send_reset_email。

@app.route("/reset_password", methods=['GET', 'POST'])
def reset_request():
    if current_user.is_authenticated:
        return redirect(url_for('home'))
    form = RequestResetForm()
    if form.validate_on_submit():
        user = User.query.filter_by(email=form.email.data).first()
        send_reset_email(user)
        flash('An email has been sent with instructions to reset your password.', 'info')
        return redirect(url_for('login'))
    return render_template('reset_request.html', title='Reset Password', form=form)

再看看send_reset_email(),看第二行, token = user.get_reset_token()生成一个token令牌,这里有点盐值(salt)的意思,就是加一个随机字符串,使得安全性大大提高。

def send_reset_email(user):
    token = user.get_reset_token()
    msg = Message('Password Reset Request',
                  sender='noreply@demo.com',
                  recipients=[user.email])
    msg.body = f'''To reset your password, visit the following link:
{url_for('reset_token', token=token, _external=True)}
If you did not make this request then simply ignore this email and no changes will be made.
'''
    mail.send(msg)

然后,带着token,来到真正重置密码的页面,完成更改密码。

@app.route("/reset_password/<token>", methods=['GET', 'POST'])
def reset_token(token):
    if current_user.is_authenticated:
        return redirect(url_for('home'))
    user = User.verify_reset_token(token)
    if user is None:
        flash('That is an invalid or expired token', 'warning')
        return redirect(url_for('reset_request'))
    form = ResetPasswordForm()
    if form.validate_on_submit():
        hashed_password = bcrypt.generate_password_hash(form.password.data).decode('utf-8')
        user.password = hashed_password
        db.session.commit()
        flash('Your password has been updated! You are now able to log in', 'success')
        return redirect(url_for('login'))
    return render_template('reset_token.html', title='Reset Password', form=form)

#14. 结构大变动

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-wPGZ3y6Z-1583331298525)(C:\Users\G3\AppData\Roaming\Typora\typora-user-images\1583324910262.png)]

这就是最终结构,不得不说,这样的结构真的使得项目的结构很专业,比起一堆文件堆在一起,确实是好。分为了用户模块,文章模块,templates模块,静态文件,自定义错误页面模块。

和用户相关的表放一个文件,相关的路由放一个文件,文章的也一样。

这里说下util文件,这个文件就是放一些工具函数,像save_picture,send_reset_email.

error里的handlers:就是对于错误的一些处理。

from flask import Blueprint, render_template

errors = Blueprint('errors', __name__)


@errors.app_errorhandler(404)
def error_404(error):
    return render_template('errors/404.html'), 404


@errors.app_errorhandler(403)
def error_403(error):
    return render_template('errors/403.html'), 403


@errors.app_errorhandler(500)
def error_500(error):
    return render_template('errors/500.html'), 500

#15.blueprint(蓝图)

这个可以说是最后的一个难点,模块化管理程序路由是它的特色,它使程序结构清晰、简单易懂。

在总的init文件里,我们要各模块的路由并且注册到蓝图中。这样在各子模块的路由文件里直接声明一下即可。

def create_app(config_class=Config):
    app = Flask(__name__)
    app.config.from_object(Config)

    db.init_app(app)
    bcrypt.init_app(app)
    login_manager.init_app(app)
    mail.init_app(app)

    from flaskblog.users.routes import users
    from flaskblog.posts.routes import posts
    from flaskblog.main.routes import main
    from flaskblog.errors.handlers import errors
    app.register_blueprint(users)
    app.register_blueprint(posts)
    app.register_blueprint(main)
    app.register_blueprint(errors)

    return app

#16.Serializer序列化

https://www.bookstack.cn/read/head-first-flask/chapter03-section3.05.md

可以参考这篇文章,下面的部分代码可以认识下,token令牌是怎么使用的。
通过dump把数据转储一下(就是转换并储存),变成token令牌;通过load,再把token逆转回去,若验证成功,则把值赋回去并返回True。
不同的秘钥生成不同的序列化对象,不同的序列化对象有不同的加密规则。

#实例化一个签名序列化对象 serializer,有效期 10 分钟
serializer = Serializer(app.config['SECRET_KEY'], expires_in=600)
users = ['john', 'susan']
# 生成 token
for user in users:
    token = serializer.dumps({'username': user})
    print('Token for {}: {}\n'.format(user, token))
# 回调函数,对 token 进行验证
@auth.verify_token
def verify_token(token):
    g.user = None
    try:
        data = serializer.loads(token)
    except:
        return False
    if 'username' in data:
        g.user = data['username']
        return True
    return False

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值