【Flask/跟着学习】Flask大型教程项目#04:用户登陆

跟着学习(新版):https://blog.miguelgrinberg.com/post/the-flask-mega-tutorial-part-v-user-logins
跟着学习(旧版):http://www.pythondoc.com/flask-mega-tutorial/userlogin.html
回顾上一章:https://blog.csdn.net/weixin_41263513/article/details/85010558

正片前的碎碎念

实际上上面两个链接跟着学习(新版旧版)都是出于同一位作者之手Miguel Grinberg,赞美大神!虽然旧版的代码可能有点问题,但好在是中文版啊!!而且下载的文件其实是属于新版的代码,所以下载之后会发现跟网页(旧版)里的对不上,现在要换一个网页学习啦,新版的我大概阅览了一下,能学到比旧版多的东西,但比较尴尬的是新版的都是英文,不过也不差啦,里面除了专业名词,其实过了四级的人我觉得都能看懂的呢!之后我的学习记录也将按照新版的编写,up我擦线过六级,啃完这一篇全英的,下年努把力,争取六级过500!之前的博客找个时间也翻新一下吧,就酱~

本章内容

  • 密码安全性
  • 用Flask-Login验证用户身份
  • 需要用户登陆才能访问页面
  • 在模板中显示登陆用户
  • 用户注册

密码安全性

若想保证数据库中用户密码的安全,关键在于不存储密码本身,而是存储密码的散列值。计算密码散列值的函数接收密码作为输入,添加随即内容(盐值)之后,使用多种单向加密算法转换密码,最终得到一个和原始密码没有关系的字符序列,而且无法还原成原始密码,核对密码时,密码散列值可代替原始密码,因为计算散列值的函数时可复现的:只要输入(密码和盐值)一样,结果就一样

我们用Werkzeug中的security模块实现密码散列值的计算
在这里插入图片描述
在这里插入图片描述
更新一下我们的模型,添加两个方法
文件:/app/models.py

#......
from werkzeug.security import generate_password_hash, check_password_hash


class User(UserMixin, db.Model):
	#......
    def set_password(self, password):
        self.password_hash = generate_password_hash(password)

    def check_password(self, password):
        return check_password_hash(self.password_hash, password)

实现一下
在这里插入图片描述

用Flask-Login验证用户身份

flask有一个非常好用的扩展Flask-login,这个扩展可用于管理用户的登陆状态,让应用程序“记住”用户,登陆时可以导航到不同页面,即便在关闭浏览器后,用户也能保持登陆状态

先初始化这个扩展
文件:/app/–init–.py

#......
from flask_login import LoginManager

app = Flask(__name__)
#......
login = LoginManager(app)
#......

要使用Flask-Login扩展,应用的User模型必须实现以下几个属性和方法

属性/方法 说明
is_authenticated 如果用户提供的登陆凭据有效,必须返回True,否则返回False
is_active 如果允许用户登陆,必须返回True,否则返回False,如果想禁用账户,可以返回false
is_anonymous 对普通用户必须始终返回false,如果是表示匿名用户的特殊用户对象,应该返回true
get_id() 必须返回用户的威一标识符,使用Unicode编码字符串

虽然可以很容易的实现上面四个属性/方法,但Flask-Login还提供了一个名为UserMixin的mixin类,包含了适用于大多数用户模型类的通用实现,以下是将mixin类添加到模型中

#......
from flask_login import UserMixin

class User(UserMixin, db.Model):
#......

加载用户的方法
因为Flask-Login对数据库一无所知,所以需要通过数据库先找到用户

我们使用@ login.user_loader装饰器向Flask-Login注册用户加载程序。 Flask-Login作为参数传递给函数的id将是一个字符串,因此使用数字ID的数据库需要将字符串转换为整数。

from app import login
# ...

@login.user_loader
def load_user(id):
    return User.query.get(int(id))

登入用户
让我们完成视图函数

# ...
from flask_login import current_user, login_user
from app.models import User

# ...

@app.route('/login', methods=['GET', 'POST'])
def login():
    if current_user.is_authenticated:
        return redirect(url_for('index'))
    form = LoginForm()
    if form.validate_on_submit():
        user = User.query.filter_by(username=form.username.data).first()
        if user is None or not user.check_password(form.password.data):
            flash('Invalid username or password')
            return redirect(url_for('login'))
        login_user(user, remember=form.remember_me.data)
        return redirect(url_for('index'))
    return render_template('login.html', title='Sign In', form=form)

login()函数中的前两行是为了处理一个情况:倘若你是一个已经登陆了的用户,用户却导航到了应用程序中/login的URL,这是一个乌龙行为,不能让这样的情况发生,所以用Flask-Login中的属性is_authenticated来检查用户是否登陆,若用户已经登陆,重定向到index页面。

使用SQLAlchemy查询对象的filter_by()方法。 filter_by()的结果是仅包含具有匹配用户名的对象的查询。因为我知道只有一个或零个结果,所以我通过调用first()来完成查询,如果它存在则返回用户对象,如果不存在则返回None。当您在查询中调用all()方法时,查询将执行,您将获得与该查询匹配的所有结果的列表。当您只需要一个结果时,first()方法是另一种常用的执行查询的方法。

如果用户名和密码都正确,那么我调用login_user()函数,该函数来自Flask-Login。此功能将在登录时注册用户,这意味着用户导航到的任何将来页面都将current_user变量设置为该用户。

要完成登录过程,我只需将新登录的用户重定向到index页面。

登出用户
有登入就有登出嘛
文件:/app/routes.py

# ...
from flask_login import logout_user

# ...

@app.route('/logout')
def logout():
    logout_user()
    return redirect(url_for('index'))

登录登出链接都需要公开在导航栏中,我们可以在用户登陆后使导航栏中的“登陆”链接自动切换成“注销”,通过改变base.html来完成:
文件:/app/templates/base.html

<div>
        Microblog:
        <a href="{{ url_for('index') }}">Home</a>
        {% if current_user.is_anonymous %}
        <a href="{{ url_for('login') }}">Login</a>
        {% else %}
        <a href="{{ url_for('logout') }}">Logout</a>
        {% endif %}
    </div>

is_anonymous属性是Flask-Login通过UserMixin类添加到用户对象的属性之一。 仅当用户未登录时,current_user.is_anonymous表达式才会为True。

需要用户登陆才能访问页面

Flask-Login提供了一个非常有用的功能,强制用户在查看应用程序的某些页面之前登录。 如果未登录的用户尝试查看受保护的页面,Flask-Login将自动将用户重定向到登录表单,并仅在登录过程完成后重定向回用户想要查看的页面。

要实现此功能,Flask-Login需要知道处理登录的视图函数是什么
文件:/app/–init–.py

# ...
login = LoginManager(app)
login.login_view = 'login'

其实吧,我对login.login_view = 'login’这一行不太懂,作者的意思是:
上面的“login”值是登录视图的功能名称(或端点)。 换句话说,您将会在url_for()调用这个login名称来获取URL。(渣翻译)
原文:The ‘login’ value above is the function (or endpoint) name for the login view. In other words, the name you would use in a url_for() call to get the URL.
反正实现的效果是指未登录时,会跳转过去的login页面

除此之外,我们还需要在routes文件里面做文章
文件:/app/routes.py

from flask_login import login_required

@app.route('/')
@app.route('/index')
@login_required
def index():
    # ...

当您将@login_required装饰器添加到Flask的@ app.route装饰器下面的视图函数时,该函数将受到保护,并且不允许访问未经过身份验证的用户
结合上面两个代码一起理解就是:如果用户导航到/ index,则@login_required装饰器将拦截请求并使用重定向响应导航到/ login,但它会向此URL添加查询字符串参数,下一个查询字符串参数设置为原始URL,因此应用程序可以使用该参数在登录后重定向。
在这里插入图片描述
下面是一段代码,展示了如何读取和处理下一个查询字符串参数
文件:/app/routes.py

from flask import request
from werkzeug.urls import url_parse

@app.route('/login', methods=['GET', 'POST'])
def login():
    # ...
    if form.validate_on_submit():
        user = User.query.filter_by(username=form.username.data).first()
        if user is None or not user.check_password(form.password.data):
            flash('Invalid username or password')
            return redirect(url_for('login'))
        login_user(user, remember=form.remember_me.data)
        next_page = request.args.get('next')
        if not next_page or url_parse(next_page).netloc != '':
            next_page = url_for('index')
        return redirect(next_page)
    # ...

在用户调用Flask-Login的login_user()函数登录后,将获取下一个查询字符串参数的值(上图)。 Flask提供了一个请求变量,其中包含客户端随请求发送的所有信息。特别是,request.args属性以友好的字典格式公开查询字符串的内容。实际上,在成功登录后,确实需要考虑三种可能的情况来确定重定向的位置:

  1. 如果登录URL没有下一个参数,则会将用户重定向到索引页面。
  2. 如果登录URL包含设置为相对路径的下一个参数(或者换句话说,没有域部分的URL),则将用户重定向到该URL。
  3. 如果登录URL包含设置为包含域名的完整URL的下一个参数,则会将用户重定向到索引页面。

第一和第二个案例是不言自明的。第三种情况是为了使应用程序更安全。攻击者可以在下一个参数中向恶意站点插入URL,因此应用程序仅在URL为相对时重定向,这可确保重定向与应用程序保持在同一站点内。要确定URL是相对的还是绝对的,我使用Werkzeug的url_parse()函数解析它,然后检查是否设置了netloc组件。
我自己也看的不太懂,总而言之,一切都是为了安全,亲测,没有上面这段代码依然可以运行!不过为了安全性各位还是加上吧,谁知道以后你们的网站会不会遭受攻击呢!

在模板中显示登陆用户

之前创建了一个假用户来帮助我在用户子系统到位之前设计应用程序的主页,那该应用程序现在有真正的用户,所以我现在可以删除假用户并开始与真实用户合作。 我可以在模板中使用Flask-Login的current_user而不是假用户:
文件:/app/templates/index.html

{% extends "base.html" %}

{% block content %}
    <h1>Hi, {{ current_user.username }}!</h1>
    {% for post in posts %}
    <div><p>{{ post.author.username }} says: <b>{{ post.body }}</b></p></div>
    {% endfor %}
{% endblock %}

用户注册

最后一个功能,注册表单!可以让用户通过web表单进行注册
先创建web表单类:
文件:/app/forms.py

from flask_wtf import FlaskForm
from wtforms import StringField, PasswordField, BooleanField, SubmitField
from wtforms.validators import ValidationError, DataRequired, Email, EqualTo
from app.models import User

# ...

class RegistrationForm(FlaskForm):
    username = StringField('Username', validators=[DataRequired()])
    email = StringField('Email', validators=[DataRequired(), Email()])
    password = PasswordField('Password', validators=[DataRequired()])
    password2 = PasswordField(
        'Repeat Password', validators=[DataRequired(), EqualTo('password')])
    submit = SubmitField('Register')

    def validate_username(self, username):
        user = User.query.filter_by(username=username.data).first()
        if user is not None:
            raise ValidationError('Please use a different username.')

    def validate_email(self, email):
        user = User.query.filter_by(email=email.data).first()
        if user is not None:
            raise ValidationError('Please use a different email address.')

这个新表格中有一些有趣的东西。首先,对于email字段,我在DataRequired之后添加了第二个验证器,Emaiil()。这是WTForms附带的另一个验证器,它将确保用户在此字段中键入的内容与电子邮件地址的结构相匹配。

由于这是注册表,因此通常要求用户输入密码两次以减少拼写错误的风险。出于这个原因,我有password和password2字段。password2字段使用另一个名为EqualTo的验证器,它将确保其值与第一个密码字段的值相同。

我还为这个类添加了两个方法,validate_username()和validate_email()。都非常好理解

要在网页上显示此表单,我需要一个HTML模板:
文件:/app/templates/register.html

{% extends "base.html" %}

{% block content %}
    <h1>Register</h1>
    <form action="" method="post">
        {{ form.hidden_tag() }}
        <p>
            {{ form.username.label }}<br>
            {{ form.username(size=32) }}<br>
            {% for error in form.username.errors %}
            <span style="color: red;">[{{ error }}]</span>
            {% endfor %}
        </p>
        <p>
            {{ form.email.label }}<br>
            {{ form.email(size=64) }}<br>
            {% for error in form.email.errors %}
            <span style="color: red;">[{{ error }}]</span>
            {% endfor %}
        </p>
        <p>
            {{ form.password.label }}<br>
            {{ form.password(size=32) }}<br>
            {% for error in form.password.errors %}
            <span style="color: red;">[{{ error }}]</span>
            {% endfor %}
        </p>
        <p>
            {{ form.password2.label }}<br>
            {{ form.password2(size=32) }}<br>
            {% for error in form.password2.errors %}
            <span style="color: red;">[{{ error }}]</span>
            {% endfor %}
        </p>
        <p>{{ form.submit() }}</p>
    </form>
{% endblock %}

登录表单模板需要一个链接,将新用户发送到表单下方的注册表单
文件:/app/templates/login.html

 <p>New User? <a href="{{ url_for('register') }}">Click to Register!</a></p>

最后的最后,不要忘了,新建的html一定要在routes.py中处理:
文件:/app/routes.py

from app import db
from app.forms import RegistrationForm

# ...

@app.route('/register', methods=['GET', 'POST'])
def register():
    if current_user.is_authenticated:
        return redirect(url_for('index'))
    form = RegistrationForm()
    if form.validate_on_submit():
        user = User(username=form.username.data, email=form.email.data)
        user.set_password(form.password.data)
        db.session.add(user)
        db.session.commit()
        flash('Congratulations, you are now a registered user!')
        return redirect(url_for('login'))
    return render_template('register.html', title='Register', form=form)

终于终于结束啦!!看看效果吧
在这里插入图片描述
在这里插入图片描述

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章