The Flask Mega-Tutorial 之 Chapter 5: User Logins

小引

  • 完成两个需求:用户的登录login)及新用户的注册register
  • 为完成 login,引入Flask-Login
  • Flask-Login可通过存储用户的 unique identifierkeep tracking
  • Flask-Login 提供多种 features,如 UserMixin, @login.user_loader, current_user, login_user, logout_user, login_required ( with login.login_view),request

Pashword Hashing

常规的login,需要验证 usernamepassword
Model User 中之所以设置 password_hash 而非直接的 password,是为安全起见,db 不直接存储 password,而是存储 password 的 hash 值。
Werkzeug,作为 Flask 的 core dependency之一,其中 security 模块有两个函数用于hashing: generate_password_hash, check_password_hash

  • app / models.py: Password hashing and verification
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)

Intro to Flask-Login

(venv) $ pip install flask-login

  • This extension manages the user logged-in state, so that for example users can log in to the application and then navigate to different pages while the application “remembers” that the user is logged in.
  • It also provides the “remember me” functionality that allows users to remain logged in even after closing the browser window.

初始化:每次引入新的扩展,往往需要初始化 (app/__init__.py),从 flask_login 引入 LoginManager,对 app instance 进行 login 的初始化。

# ...
from flask_login import LoginManager

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

# ...

Prepare The User Model for Flask-Login

Requirements: Flask-Login works with app’s User Model, and expects certain properties / methods to be implemented in it.

Four required items:

  • is_authenticated: property; True if user has valid credentials, False otherwise.
  • is_active: property; True if user’s account is active, False otherwise.
  • is_anonymous: property; False for regular users, True for a special, anonymous user.
  • get_id(): method; returns a unique identifier for the user as a string (unicode for Python 2).

UserMixin:Since these implementations are rather generic, we can use a mixin class from Flask-Login

app / models.py: Flask-Login user mixin class

# ...
from flask_login import UserMixin

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

User Loader Function

原理:任何一个 user 连接到 application 时,Flask 都会给其分配一个 user session (storage space),Flask-Login 会将每个 user 的 unique identifier 存储到相应的 user session 中。每当 logged-in user 浏览至新的网页时,Flask-Login 就会从其对应的 user session 中提取此 user 的 ID, 并将此 user 载入memory。

Flask-Login keeps track of the logged in user by storing its unique identifier in Flask’s user session, a storage space assigned to each user who connects to the application. Each time the logged-in user navigates to a new page, Flask-Login retrieves the ID of the user from the session, and then loads that user into memory.

由于 Flask-Login 不涉及数据库,在载入 user 的时候需要 application 的辅助 ,为此配置一个 user loader function, 在其被调用时能够基于给定的 ID 来载入 user。

Because Flask-Login knows nothing about databases, it needs the application’s help in loading a user. For that reason, the extension expects that the application will configure a user loader function, that can be called to load a user given the ID. This function can be added in the app/models.py module:

app/models.py: Flask-Login user loader function

from app import login
# ...

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

The id that Flask-Login passes to the function as an argument is going to be a string, so databases that use numeric IDs need to convert the string to integer as above.


Logging Users In

  • 回顾:之前 login view function 试过 fake login 并 flash 出一条信息 (Chapter 3: Web Forms):
@app.route('/login', methods=['GET', 'POST'])
def login():
    form = LoginForm()
    if form.validate_on_submit():
        flash('Login requested for user {}, remember_me={}'.format(
            form.username.data, form.remember_me.data))
        return redirect(url_for('index'))
    return render_template('login.html',  title='Sign In', form=form)

现在 application 已可以连接 db,且知道如何生成及验证 password_hash,时机成熟,可以将此 login view func 完善。

  • 准备工作:初始化 Flask-Login, 更改 User Model(添加 UserMixin),配置 user_loader (app / models.py 中) 。

  • 继续完善
    app /routes.py: Login view function logic

# ...
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)
  1. current_user 系Flask-Login自带变量 ,任何时候想获得请求客户端(client of the request)的 用户(user object)时,都可调用此变量。
    此变量值分两种:如果已登录(logged-in),则为Flask-Login 通过 user loader 返回获得的 db 中的 一个 user object;如未登录,则为一个特殊的匿名用户(a special anonymous user object)。

    The current_user variable comes from Flask-Login and can be used at any time during the handling to obtain the user object that represents the client of the request. The value of this variable can be a user object from the database (which Flask-Login reads through the user loader callback I provided above), or a special anonymous user object if the user did not log in yet.

  2. 一旦 user (尚未logged-in)通过 username / password 的双重验证,则调用 login_user() 函数进行logging。此后,无论此用户浏览任何网页,网页都会将 current_user 变量设定为此 user。

    If the username and password are both correct, then I call the login_user() function, which comes from Flask-Login. This function will register the user as logged in, so that means that any future pages the user navigates to will have the current_user variable set to that user.

  3. 最后,所有登录成功的(已登录 / 重新登录),均被 redirect 到 ‘index’ 。


Logging Users Out

  • app / routes.py: Logout view function
# ...
from flask_login import logout_user

# ...

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

:一旦 logout,则跳转到 ‘index’。但由于 ‘index’ 设定了 login_required(见下文),所以会重新跳转到 ‘login’(通过__init__.py 中的 login.login_view='login' 设定)。

  • 对登录后的用户(logged-in user),希望页面上方的导航条中的 Login 改为 Logout

app / templates/base.html: Conditional login and logout links

    <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>

反向判断:未登录(anonymous),则显示 Login;其他,则显示 Logout

(is_anonymous,见 User Model 中的 UserMixin )


Requiring Users To Login

  • 应用场景:如果某网页设置为保户状态(login_required),则未登录用户无法浏览,此时会被 redirect 至 ‘login’;用户进行登录验证后,会被 redirect 至 之前尚无权限浏览的原网页。
  • Requirements: Flask-Login needs to know what is the view function that handles logins.

1.app/__init__.py: 定义 login.login_view

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

这里的 ‘login’,类似url_for(view_function_name)的用法,指的是 view function.

当未登录用户访问某路由时,如果此路由被 @login_required 修饰过,则会被重定向至 login.login_view = 'login' 定义的 ‘login’ ,以便进行登录验证。

2.app/routes.py: @login_required decorator 修饰需要被保护的 view_func。

from flask_login import login_required

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

@login_required 修饰 ‘index’,即 home page。
未登录用户访问 /index 时,其request 会被 @login_required 截获,并重定向至 /login;同时,原 URL 被重定向为 /login?next=/index
此 next 请求字符参数,被设定为 源 URL, 所以application 可在用户登录后据此重定向回来。

If the user navigates to /index, for example, the @login_required decorator will intercept the request and respond with a redirect to /login, but it will add a query string argument to this URL, making the complete redirect URL /login?next=/index.
The next query string argument is set to the original URL, so the application can use that to redirect back after login.

3.app / routes.py: Redirect to “next” page

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)
    # ...

login_user() 登录用户后,通过 flask 中的 request 变量 来获取 next 参数,即next_page = request.args.get('next')

flask 的 request 变量,包含客户端发起的请求中的所有信息,并通过 request.args 属性以 dict 方式展现请求字符串的内容 (contents of the query string),所以可以采用 request.args.get('next') 的方式获取 next 参数。

Flask provides a request variable that contains all the information that the client sent with the request. In particular, the request.args attribute exposes the contents of the query string in a friendly dictionary format.


获得的 next_page,分三种情况:
(1) 不存在,则直接重定向至主页的 ‘index’ (通过url_for())。
(2) 存在,但为绝对路径(包含域名),则为安全起见,直接定向为 ‘index’。
(3) 存在,且为 相对路径(不含域名),形如 ‘/index’,则重定向至 next_page。注意,直接用 redirect(next_page), 而不涉及 url_for(),因 next_page 是完备的 相对路径,且尚不知对应的 view func。

:To determine if the URL is relative or absolute, we parse it with Werkzeug’s url_parse() function and then check if the netloc component is set or not.


Showing The Logged In User in Templates

  • 之前 chapter 2 中,写 home page (index.html)时,由于 用户系统(user subsystem)尚未搭建,所以采用了 fake user,如下:
@app.route('/')
@app.route('/index')
def index():
    user = {'username': 'Miguel'}
    posts = [
        {
            'author': {'username': 'John'},
            'body': 'Beautiful day in Portland!'
        },
        {
            'author': {'username': 'Susan'},
            'body': 'The Avengers movie was so cool!'
        }
    ]
    return render_template('index.html', title='Home', user=user, posts=posts)

对应的 index. html

{% extends "base.html" %}

{% block content %}
    <h1>Hi, {{ user.username }}!</h1>
    {% for post in posts %}
    <div><p>{{ post.author.username }} says: <b>{{ post.body }}</b></p></div>
    {% endfor %}
{% endblock %}
  • 现在已经搭建 用户系统 (User Model),可以去掉 fake user,如下:

app / templates / index.html: Pass current user to template

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

user 调为 current_user

app / routes.py: not pass user to template anymore

@app.route('/')
@app.route('/index')
def index():
    # ...
    return render_template("index.html", title='Home Page', posts=posts)


  • 测试

因为尚无注册用户,所以只能通过 shell 环境创建用户,然后写入 db。

>>> u = User(username='susan', email='susan@example.com')
>>> u.set_password('cat')
>>> db.session.add(u)
>>> db.session.commit()

启动 app,输入 http://localhost:5000/ 或者 http://localhost:5000/index, 将被直接重定向至 login 页面。
用刚刚创建的上述用户名及密码登录后,将被定向至原网页,即 /index 页。


User Registration

  • 创建注册样表
    app / forms.py: User registration form
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.')

RegistrationForm 中涉及 邮箱、二次密码等,故自 wtforms.validators 中新引入验证函数: Email (用户验证邮箱)、EqualTo(二次验证password)以及 ValidationError(验证报错)。

采用 validate_<field_name> 模式自行设置两个 method。
WTForms would invoke the custom validators in addition to the stock validators, in case of ValidationException.

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

email 的form 字段长度设置为 64,大于其他的 32。

  • app / templates / login.html: login页面 添加 register 链接
<form>
...
</form>
<p>New User? <a href="{{ url_for('register') }}">Click to Register!</a></p>


  • app / routes.py: register 路由

因为注册后的用户信息,须写入 db,所以引入 db。(上文测试时,手动在shell中添加用户并写入db)

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)

(1) 已登录用户,定向至 /index。
(2) 初次 GET 请求,返回 register.html。
(3) 填写完毕并POST,利用 user.set_password(form.password.data) 设置 self.password_hash
(4) 提交修改,写入 db。
(5) flash 成功消息,并定向至 ‘login’。

这里写图片描述


小结

  • 引入 Flask-Login —-> 初始化 login —->更改 User Model (UserMixin) —-> 设置 user loader —-> 更新 login 路由(最重要的 login_user())—-> logout 路由
  • 如果页面有 login_required 需求 —-> 初始化设置 login.login_view —-> 涉及两次重定向 (如 /login?next=/index) —-> 重新更新 login 路由,以便登录后重定向源网页 (–> 更新 index.html —-> 更新 index 路由)

  • 路由中,往往需要涉及判断是否是登录用户(current_user.is_authenticated)

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
智慧校园的建设目标是通过数据整合、全面共享,实现校园内教学、科研、管理、服务流程的数字化、信息化、智能化和多媒体化,以提高资源利用率和管理效率,确保校园安全。 智慧校园的建设思路包括构建统一支撑平台、建立完善管理体系、大数据辅助决策和建设校园智慧环境。通过云架构的数据中心与智慧的学习、办公环境,实现日常教学活动、资源建设情况、学业水平情况的全面统计和分析,为决策提供辅助。此外,智慧校园还涵盖了多媒体教学、智慧录播、电子图书馆、VR教室等多种教学模式,以及校园网络、智慧班牌、校园广播等教务管理功能,旨在提升教学品质和管理水平。 智慧校园的详细方案设计进一步细化了教学、教务、安防和运维等多个方面的应用。例如,在智慧教学领域,通过多媒体教学、智慧录播、电子图书馆等技术,实现教学资源的共享和教学模式的创新。在智慧教务方面,校园网络、考场监控、智慧班牌等系统为校园管理提供了便捷和高效。智慧安防系统包括视频监控、一键报警、阳光厨房等,确保校园安全。智慧运维则通过综合管理平台、设备管理、能效管理和资产管理,实现校园设施的智能化管理。 智慧校园的优势和价值体现在个性化互动的智慧教学、协同高效的校园管理、无处不在的校园学习、全面感知的校园环境和轻松便捷的校园生活等方面。通过智慧校园的建设,可以促进教育资源的均衡化,提高教育质量和管理效率,同时保障校园安全和提升师生的学习体验。 总之,智慧校园解决方案通过整合现代信息技术,如云计算、大数据、物联网和人工智能,为教育行业带来了革命性的变革。它不仅提高了教育的质量和效率,还为师生创造了一个更加安全、便捷和富有智慧的学习与生活环境。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值