Flask课程5-用户登录子系统

942 阅读2分钟

密码哈希

整个密码哈希逻辑可以在用户模型中实现为两个新的方法: app/models.py

from werkzeug.security import generate_password_hash, check_password_hash

# ...

class User(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

安装

$ pip install flask-login

初始化flask-login

app/__init__py

# ...
from flask_login import LoginManager

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

# ...

为Flask-Login准备用户模型

Flask-Login插件需要在用户模型上实现某些属性和方法。这种做法很棒,因为只要将这些必需项添加到模型中,Flask-Login就没有其他依赖了,它就可以与基于任何数据库系统的用户模型一起工作。

必须的四项如下:

  • is_authenticated: 一个用来表示用户是否通过登录认证的属性
  • is_active: 用户是否活跃
  • is_anonymous: 是否未特定匿名用户
  • get_id(): 返回用户唯一id

Flask-Login提供了UserMixin的mixin类来归纳这些属性,将mixin类添加到模型中:

app/models.py修改为:

from flask_login import UserMixin
#...
class User(UserMixin, db.model):
#...

用户加载函数

用户回话是Flask分配给每个连接到应用的存储空间,Flask-Login通过在用户会话中存储的唯一标识符来跟踪用户。每当已登录的用户导航到新页面时,Flask-Login将从会话中检索用户的ID,然后将该用户实例加载到内存中。

因为数据库对Flask-Login透明,所以需要应用来辅助加载用户。 基于此,插件期望应用配置一个用户加载函数,可以调用该函数来加载给定ID的用户。 该功能可以添加到app/models.py模块中:

from app import login

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

用户登入

app/routes.py

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)

用户登出

添加登出路由

from flask_login import current_user, login_user, logout_user

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

处理导航栏登录后显示登出按钮

app/templates/base.html

<div>
        Idu blog:
        <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属性是在其模型继承UserMixin类后Flask-Login添加的,表达式current_user.is_anonymous仅当用户未登录时的值是True。

要求用户登录

Flask-Login提供了一个非常有用的功能——强制用户在查看应用的特定页面之前登录。 如果未登录的用户尝试查看受保护的页面,Flask-Login将自动将用户重定向到登录表单,并且只有在登录成功后才重定向到用户想查看的页面。

为了实现这个功能,Flask-Login需要知道哪个视图函数用于处理登录认证。在app/__init__.py中添加代码如下:

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

上面的'login'值是登录视图函数(endpoint)名,换句话说该名称可用于url_for()函数的参数并返回对应的URL。

Flask-Login使用名为@login_required的装饰器来拒绝匿名用户的访问以保护某个视图函数。 当你将此装饰器添加到位于@app.route装饰器下面的视图函数上时,该函数将受到保护,不允许未经身份验证的用户访问。 以下是该装饰器如何应用于应用的主页视图函数的案例:


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

剩下的就是实现登录成功之后自定重定向回到用户之前想要访问的页面。 当一个没有登录的用户访问被@login_required装饰器保护的视图函数时,装饰器将重定向到登录页面,不过,它将在这个重定向中包含一些额外的信息以便登录后的回转。 例如,如果用户导航到*/index*,那么@login_required装饰器将拦截请求并以重定向到*/login来响应,但是它会添加一个查询字符串参数来丰富这个URL,如/login?next=/index*。 原始URL设置了next查询字符串参数后,应用就可以在登录后使用它来重定向。

简单来说就是登录前的页面记录一下,登录完成后依然回到登录前的页面。那么在登录的路由处修改下代码 app/routes.py

from flask import request
from werkzeug.urls import url_parse

@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)
        next_page = request.args.get('next')
        if not next_page or url_parse(next_page).netloc !='':#netloc判定URL是否为相对路径,
        #否则不执行,防止指向恶意站点
            next_page = url_for('index')#如果未携带nextpage,给默认的indexpage
        return redirect(next_page)
    return render_template('login.html', title='Sign In', form=form)

在模板中显示已登录的用户

移出indexpage中的模拟用户,使用flask-login中的current_user

{% 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 %}

移出index路由中模拟的user参数


@app.route('/')
@app.route('/index')
@login_required
def index():
    #...
    return render_template('index.html', title='HomePage', posts=posts)

使用flask shell注册一个用户

$ flask shell
$ from app.models import User
$ from app import db
$ u = User(username='dou', email='dou@example.com')
$ u.set_password('123')
$ db.session.add(u)
$ db.session.commit()

启动首页可以直接重定向到登录页面了,并且填入正确的用户名密码可以登录成功并返回到index页面。点击log out按钮,会退出当前登录并返回index页面,由于index页面必需登录,所以又被重定向至login.login_view页面,其值在app/__init__.py中赋值为login路由

通过web表单进行用户注册

构建表单

app/forms.py

from wtforms.validators import DataRequired, ValidationError, Email, EqualTo
from app.models import User
#...
class RegisterForm(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 diff name.')

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

其中validate_<field_name>的方法为WTForms自定义验证器,此处设定为不允许使用相同的用户名以及email注册用户,若违反raise出错误日志。

新建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)}}
            {%for error in form.email.errors%}
            <span style="color: red;">[{{error}}]</span>
            {% endfor %}
        </p>
        <p>
            {{form.password.label}}<br>
            {{form.password(size=32)}}
            {%for error in form.password.errors%}
            <span style="color: red;">[{{error}}]</span>
            {%endfor%}
        </p>
        <p>
            {{form.password2.label}}<br>
            {{form.password2(size=32)}}
            {%for error in form.password2.errors%}
            <span style="color: red;">[{{error}}]</span>
            {%endfor%}
        </p>
        <p>{{form.submit()}}</p>
    </form>
{%endblock%}

添加注册超链接

当未登录成功时显示注册超链接

app/templates/base.html

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

处理注册路由

@app.route('/register', methods=['GET', 'POST'])
def register():
    if current_user.is_authenticated:
        return redirect(url_for('index'))
    form=RegisterForm()
    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 ! register ok')
        return redirect(url_for('login'))

    return render_template('register.html', title='Register', form=form)