结合作者的教程,也是加了个人的总结与精炼,源作者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 第一次整理结构
随着代码量的增加,当项目初步具备一些功能时,就需要考虑代码之间的解耦。用一个好的文件结构是不错的选择。像这样整理结构。
把路由,数据库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">×</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)
1 | MAIL_SERVER电子邮件服务器的名称/IP地址 |
---|---|
2 | MAIL_PORT使用的服务器的端口号 |
3 | MAIL_USE_TLS启用/禁用传输安全层加密 |
4 | MAIL_USE_SSL启用/禁用安全套接字层加密 |
5 | MAIL_DEBUG调试支持。默认值是Flask应用程序的调试状态 |
6 | MAIL_USERNAME发件人的用户名 |
7 | MAIL_PASSWORD发件人的密码 |
8 | MAIL_DEFAULT_SENDER设置默认发件人 |
9 | MAIL_MAX_EMAILS设置要发送的最大邮件数 |
10 | MAIL_SUPPRESS_SEND如果app.testing设置为true,则发送被抑制 |
11 | MAIL_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. 结构大变动
这就是最终结构,不得不说,这样的结构真的使得项目的结构很专业,比起一堆文件堆在一起,确实是好。分为了用户模块,文章模块,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