flask-用户认证(上)

来源:互联网 发布:工行td交易软件 编辑:程序博客网 时间:2024/05/21 21:01

主要用到的包:

  • Flask-Login: 管理已登录用户的用户会话。
  • Werkzeug: 计算密码散列值并进行核对(把密码变成散列值
  • itsdangerous: 生成并核对加密安全令牌(确认邮件)。

Werkzeug实现密码散列

若想保证数据库中用户密码的安全,要存储密码的散列值。计算密码散列值的函数接收密码作为输入,使用一种或多种加密算法转换密码,最终得到一个和原始密码没有关系的字符序列。核对密码时,密码散列值可代替原始密码,因为计算散列值的函数是可复现的:只要输入一样,结果就一样。

Werkzeug 中的 security 模块能很方便地实现密码散列值的计算。只需要两个函数,分别用在注册和验证用户阶段。

  • generate_password_hash(password, method=pbkdf2:sha1, salt_length=8):这个函数将原始 密码作为输入,以字符串形式输出密码的散列值, 输出的值可保存在用户数据库中。method 和 salt_length 的默认值就能满足大多数需求。
  • check_password_hash(hash, password):这个函数的参数是从数据库中取回的密码散列值和用 户输入的密码。返回值为 True 表明密码正确。

在 User 模型中加入密码散列:

from werkzeug.security import generate_password_hash, check_password_hashclass User(db.Model):    password_hash = db.Column(db.String(128))        @property   #私有    def password(self):        raise AttributeError('password is not a readable attribute')        @password.setter  #只可写不可读      def password(self, password):      self.password_hash = generate_password_hash(password)         def verify_password(self, password):        return check_password_hash(self.password_hash, password)

计算密码散列值的函数通过名为 password 的只写属性实现。设定这个属性的值时,赋值方法会调用Werkzeug 提供的generate_password_hash()函数,并把得到的结果赋值给password_hash 字段。如果试图读取 password 属性的值,则会返回错误,原因很明显,因为生成散列值后就无法还原成原 来的密码了。

verify_password 方法会把传入的密码和存储在 User 模型中的密码散列值进行比对。如果这个方法返回 True,就表明密码是正确的。

创建认证蓝本

把创建程序的过程移入工厂函数后,可以使用蓝本在全局作用域中定义路由。 对于不同的程序功能,使用不同的蓝本。

auth 蓝本的包构造文件创建蓝本对象,再从 views.py 模块中引入 路由。

【app/auth/init.py: 创建蓝本

from flask import Blueprintauth = Blueprint('auth',__name__)from . import views

app/auth/views.py 模块引入蓝本,然后使用route 修饰器定义与认证的 /login/路由。渲染同名占位模板。

【app/templates/auth/login.html】

{% extends "base.html" %}{% block title %}Flasky - Login{% endblock %}{% block page_content %}<div class="page-header">    <h1>Login</h1></div>{% endblock %}

【app/auth/views.py: 蓝本中的路由和视图函数】

from flask import render_templatefrom . import auth@auth.route('/login/')def login():    return render_template('auth/login.html')

注意:

为 render_template() 指定的模板文件保存在 auth 文件夹中。这个文件夹必须在 app/templates 中创建, Flask 认为模板的路径是相对于程序模板文件夹而言的。为了避免与 main 蓝本和后续添加的蓝本发生模板命名冲突,可以把蓝本使用的模板保存在单独的文件夹中。

auth 蓝本要在 app/init.py的create_app() 工厂函数中附加到程序上

【app/init.py: 附加蓝本】

def create_app(config_name):       from .auth import auth     app.register_blueprint(auth,url_prefix='/auth')       return app

url_prefix 是可选参数,使用后,注册后蓝本中定义的所有路由都会加上指定的前缀。例如:/login路由会注册成/auth/login, 在开发 Web 服务器中,完整的 URL 就变成了 http://localhost:5000/auth/login。

使用Flask-Login认证用户

用户登录程序后,他们的认证状态要被记录下来,这样浏览不同的页面时才能记住这个状态。

Flask-Login是个小型扩展,用来管理用户认证系统中的认证状态,且不依赖特定的认证机制

使用之前,要在虚拟环境中安装这个扩展:

(venv) $ pip install flask-login

1. 准备用于登录的用户模型

  • 第1步:Flask-Login 在程序的工厂函数中初始化:

【app/init.py: 初始化 Flask-Login】

from flask_login import LoginManagerlogin_manager = LoginManager()login_manager.session_protection = 'strong'login_manager.login_view = 'auth.login'def create_app(config_name):    login_manager.init_app(app)

LoginManager 对象的 session_protection 属性可以设为 None、 'basic' 或 'strong',以提供不 同的安全等级防止用户会话遭篡改。 设为 'strong' 时, Flask-Login 会记录客户端 IP地址和浏 览器的用户代理信息, 如果发现异动就登出用户。 login_view 属性设置登录页面的端点。登录路由在蓝本中定义,因此要在前面加上蓝本的名字。

  • 第2步:FlaskLogin 提供了一个 UserMixin 类,其中包含一些用户方法的默认实现,只需让模型继 承UserMixin 类即可。

【app/models.py: 修改 User 模型,支持用户登录】

from flask_login import UserMixinclass User(UserMixin, db.Model):
  • 第3步:Flask-Login 要求程序实现一个回调函数,使用指定的标识符加载用户
from . import login_manager@login_manager.user_loaderdef load_user(user_id):         return User.query.get(int(user_id))

2. 保护路由

为了保护路由只让认证用户访问,Flask-Login提供了一个login_required修饰器。用法演示如下:

from flask_login import login_required@auth.route('/secret/')@login_requireddef secret():    return 'Only anthenticated user are allowed.'

若没有登录,将无法访问http://127.0.0.1:5000/auth/secret/。会自动跳转到http://127.0.0.1:5000/auth/login/,因为在app/init.py里定义了login_manager.login_view = 'auth.login'

3. 添加登录表单

呈现给用户的登录表单中包含一个用于输入电子邮件地址的文本字段、一个密码字段、一个“记住 我”复选框和提交按钮。

【app/auth/forms.py:登录表单】

from flask_wtf import Formfrom wtforms import StringField,SubmitField,BooleanField,PasswordFieldfrom wtforms.validators import DataRequired,Email,Lengthclass LoginForm(Form):    email = StringField('Email',validators=[DataRequired(),Email(),Length(1,64)])    password = PasswordField('Password',validators=[DataRequired(),Length(6,20)])    remember = BooleanField('Remenber me')    submit = SubmitField('Log In')

电子邮件字段用到了 WTForms 提供的 Length() 和 Email() 验证函数。

PasswordField 类表示属 性为 type="password" 的 <input> 元素。 BooleanField 类表示复选框。

4. 登入用户

设置sign in与sign out按钮:

【app/templates/base.html:导航条中的 Sign In 和 Sign Out 链接】

<div class="navbar-collapse collapse">            <ul class="nav navbar-nav">                <li><a href="{{ url_for('main.index') }}">Home</a></li>            </ul>            <ul class="nav navbar-nav navbar-right">                {% if current_user.is_authenticated %}                <li><a href="{{ url_for('auth.logout') }}">Sign Out</a> </li>                {% else %}                <li><a href="{{ url_for('auth.login') }}">Sign IN</a> </li>                {% endif %}            </ul>        </div>

判断条件中的变量 current_user 由 Flask-Login 定义,且在视图函数和模板中自动可用。这个变 量的值是当前登录的用户, 如果用户尚未登录,则是一个匿名用户代理对象。如果是匿名用户, is_authenticated属性返回 False。所以这个方法可用来判断当前用户是否已经登录。

更新登录模板以渲染表单:

【app/templates/auth/login.html:渲染登录表单】

{% extends 'base.html' %}{% import "bootstrap/wtf.html" as wtf %}{% block title %} Flasky - Login {% endblock %}{% block page_content %}<div class="page-header">    <h1>Login</h1></div><div class="col-md-4">    {{wtf.quick_form(form)}}</div>{% endblock %}

视图函数 login() 的实现如下所示:

【app/auth/views.py:登录路由】

from flask import render_template, redirect, request, url_for, flashfrom flask_login import login_user, logout_user, login_requiredfrom . import authfrom ..models import Userfrom .forms import LoginForm@auth.route('/login', methods=['GET', 'POST'])def login():    form = LoginForm()    if form.validate_on_submit():        user = User.query.filter_by(email=form.email.data).first()        if user is not None and user.verify_password(form.password.data):            login_user(user, form.remember_me.data)            return redirect(request.args.get('next') or url_for('main.index'))        flash('Invalid username or password.')    return render_template('auth/login.html', form=form)

这个视图函数创建了一个 LoginForm 表单对象。当请求类型是 GET 时,视图函数直接渲染模板,即 显示表单。当表单在 POST 请求中提交时,Flask-WTF 中的 validate_on_submit() 函数会验证表单 数据,然后尝试登入用户。

为了登入用户, 视图函数首先使用表单中填写的 email 从数据库中加载用户。如果电子邮件地址对 应的用户存在, 再调用用户对象的 verify_password() 方法,其参数是表单中填写的密码。如果密 码正确,则调用 Flask-Login 中的 login_user() 函数,在用户会话中把用户标记为已登录。

login_user() 函数的参数是要登录的用户,以及可选的“记住我”布尔值,“记住我”也在表单中 填写。如果值为 False,那么关闭浏览器后用户会话就过期了,所以下次用户访问时要重新登录。 如果值为 True,那么会在用户浏览器中写入一个长期有效的 cookie,使用这个 cookie 可以复现用 户会话。

按照“Post/重定向/Get 模式”,提交登录密令的 POST 请求最后也做了重定向,不过目标 URL 有两种可能。用户访问未授权的 URL 时会显示登录表单, Flask-Login会把原地址保存在查询字符串 的 next 参数中,这个参数可从 request.args 字典中读取。 如果查询字符串中没有 next 参数,则重定向到首页。如果用户输入的电子邮件或密码不正确,程序 会设定一个 Flash 消息,再次渲染表单,让用户重试登录。

5. 登出用户

退出登录的实现:

@auth.route('/logout/')@login_requireddef logout():    logout_user()    flash('You have been logged out.')    return redirect(url_for('main.index'))

6. 测试登录

为验证登录功能可用,可以更新首页,使用已登录用户的名字显示一个欢迎消息。

【app/templates/index.html:为已登录的用户显示一个欢迎消息】

{% extends "base.html" %}{% block title %}Flasky{% endblock %}{% block page_content %}<div class="page-header">    <h1>Hello, {% if current_user.is_authenticated %}{{ current_user.username }}{% else %}Stranger{% endif %}!</h1></div>{% endblock %}

因为还未创建用户注册功能,所以新用户可在 shell 中注册:

(venv) $ python manage.py shell>>> u = User(email='nanfengpo@localhost', username='nanfengpo', password='abcdef')>>> db.session.add(u)>>> db.session.commit()

刚刚创建的用户现在可以登录了。

注册新用户

如果新用户想成为程序的成员,必须在程序中注册,这样程序才能识别并登入用户。程序的登录页面 中要显示一个链接, 把用户带到注册页面,让用户输入电子邮件地址、用户名和密码。

  • 1. 添加用户注册表单       注册页面使用的表单要求用户输入电子邮件地址、用户名和密码。

【app/auth/forms.py:用户注册表单】

from flask_wtf import Formfrom wtforms import StringField, PasswordField, BooleanField, SubmitFieldfrom wtforms.validators import Required, Length, Email, Regexp, EqualTofrom wtforms import ValidationErrorfrom ..models import Userclass RegistrationForm(Form):    email = StringField('Email', validators=[DataRequired(), Length(1, 64),     username = StringField('Username', validators=[        DataRequired(), Length(1, 64), Regexp('^[A-Za-z][A-Za-z0-9_.]*$', 0,                                          'Usernames must have only letters, '                                          'numbers, dots or underscores')])    password = PasswordField('Password', validators=[        DataRequired(), EqualTo('password2', message='Passwords must match.')])    password2 = PasswordField('Confirm password', validators=[DataRequired()])    submit = SubmitField('Register')    def validate_email(self, field):        if User.query.filter_by(email=field.data).first():            raise ValidationError('Email already registered.')    def validate_username(self, field):        if User.query.filter_by(username=field.data).first():            raise ValidationError('Username already in use.')

这个表单使用 WTForms 提供的 Regexp 验证函数,确保 username 字段只包含字母、数字、下划线 和点号。 这个验证函数中正则表达式后面的两个参数分别是正则表达式的旗标和验证失败时显示的 错误消息。

安全起见,密码要输入两次。此时要验证两个密码字段中的值是否一致,这种验证可使用WTForms 提 供的另一验证函数实现, 即 EqualTo。这个验证函数要附属到两个密码字段中的一个上,另一个字 段则作为参数传入。

这个表单还有两个自定义的验证函数, 以方法的形式实现。如果表单类中定义了以validate_ 开头 且后面跟着字段名的方法,这个方法就和常规的验证函数一起调用。本例分别为 email 和 username 字段定义了验证函数,确保填写的值在数据库中没出现过。自定义的验证函数要想表示验证失败,可 以抛出 ValidationError 异常,其参数就是错误消息。

显示这个表单的模板是 app/templates/auth/register.html。 和登录模板一样,这个模板也使用 wtf.quick_form() 渲染表单。

【app/templates/auth/register.html】

{% extends "base.html" %}{% import "bootstrap/wtf.html" as wtf %}{% block title %}Flasky - Register{% endblock %}{% block page_content %}<div class="page-header">    <h1>Register</h1></div><div class="col-md-4">    {{ wtf.quick_form(form) }}</div>{% endblock %}

此外,登录页面要显示一个指向注册页面的链接,让没有账户的用户能轻易找到注册页面。

【app/templates/auth/login.html:增加指向注册页面链接】

<div class="col-md-4">    {{wtf.quick_form(form)}}    <p>        new user? <a href="{{url_for('auth.register')}}">click here to register</a>.    </p></div>
  • 2. 注册新用户

提交注册表单,通过验证后,系统就使用用户填写的信息在数据库中添加一个新用户。这个步骤在视 图当中完成。

【app/auth/views.py:用户注册路由】

@auth.route('/register', methods=['GET', 'POST'])def register():    form = RegistrationForm()    if form.validate_on_submit():        user = User(email=form.email.data,                    username=form.username.data,                    password=form.password.data)        db.session.add(user)        flash('You can now login.')        return redirect(url_for('auth.login'))    return render_template('auth/register.html', form=form)
原创粉丝点击