flask-login和flask-wtf

一、表单验证
前言
在Flask项目开发中针对提交表单的校验,可以使用Flask-WTF扩展库进行快速的字段校验,也可以进行页面快速渲染,并提供跨站请求伪造的保护功能。

1. 安装Flask-WTF

pip install flask-wtf

注意:
可以新建一个requirement.txt文档,里面写入将要导入的包

flask
flask-script
flask-blueprint
flask-sqlalchemy
pymysql
redis
flask-session

在Terminal中输入:pip install -r requirement.txt

2. 实现注册功能

2.1 注册表单模型定义
在定义的表单类中定义需要验证的username、password和password2字段,并实现如下校验:

校验密码password2和password相等
校验用户名是否存在
校验用户名的长度是否符合规范

users->forms.py
“`
# 导入扩展类
from flask_wtf import FlaskForm
# 导入验证字段
from wtforms import StringField, SubmitField, ValidationError
# 导入表单验证
from wtforms.validators import DataRequired, EqualTo

 from user.models import User

 class UserForm(FlaskForm):
     """
     登录注册表单验证
     """
     username = StringField('用户名', validators=[DataRequired()])
     password = StringField('密码', validators=[DataRequired()])
     password2 = StringField('确认密码', validators=[DataRequired(),
                                                 EqualTo('password', '密码不一致')]
                             )
     submit = SubmitField('提交')

     def validate_username(self, field):
         # 验证用户名是否重复
         if User.query.filter(User.username == field.data).first():
             raise ValidationError('用户名已存在')

         # 对用户名的长度进行判断
         if len(field.data) < 3:
             raise ValidationError('用户名长度不能少于3个字符')

         if len(field.data) > 6:
             raise ValidationError('用户名长度不能大于6个字符')

```

注意: 验证字段的方法名为: validate_字段(self, field)

2.2 定义注册视图函数
当HTTP请求为GET时,将表单验证对象返回给页面。
当HTTP请求为POST时,通过方法validate_on_submit()方法进行字段校验和提交判断,如果校验失败,则可以从form.errors中获取错误信息。
如果验证通过,则从form.字段.data中获取到字段的值。
users->views

@blue.route('/register/', methods=['GET', 'POST'])
def register():
    form = UserForm()
    if request.method == 'GET':
        return render_template('register.html', form=form)

    if request.method == 'POST':
        # 判断表单中的数据是否通过验证
        if form.validate_on_submit():
            # 获取验证通过后的数据
            username = form.username.data
            password = form.password.data
            # 保存
            user = User()
            user.username = username
            #加密 generate_password_hash(password)
            user.password = generate_password_hash(password)
            user.save()
            return redirect(url_for('user.login'))
        return render_template('register.html', form=form)

2.3 模板展示
注册模板采用继承父模板base.html的形式。在register.html模压中分析如下:

1\. 定义字段名: {{ form.字段.label }}

2\. 定义input输入框: {{ form.字段 }}

3\. 展示错误信息: {{ form.errors.字段 }}

4\. 跨站请求伪造: {{ form.csrf_token }}

注册register.html页面如下:

{% extends 'base.html' %}

{% block title %}
    注册页面
{% endblock %}

{% block content %}
    <form action="" method="post">
        {{ form.csrf_token }}
        {{ form.username.label }}:{{ form.username(style='color:red;', placeholder='请输入用户名', onblur="alert('123')") }}
        {{ form.password.label }}:{{ form.password }}
        {{ form.password2.label }}:{{ form.password2 }}
        {{ form.submit }}

        {% if form.errors %}
            姓名错误信息:{{ form.errors.username }}
            密码错误信息:{{ form.errors.password2 }}
        {% endif %}
    </form>
{% endblock %}

注意: 通过form.字段解析的input标签中可以自定义样式,如{{ form.字段(class=’xxx’, style=’color:red’) }}

3. 常见字段类型

字段类型    说明
StringField 普通文本字段
PasswordField   密码文本字段
SubmitField 提交按钮
HiddenField 隐藏文本字段
TextAreaField   多行文本字段
DateField   文本字段,datetime.date格式
DateTimeField   文本字段,datetime.datetime格式
IntegerField    文本字段,整数类型
FloatField  文本字段,小数类型
BooleanField    复选框,值为True或False
RadioField  单选框
SelectField 下拉列表
FileField   文件上传字段

4. 验证器

验证器 说明
DataRequired    确保字段有值(并且if判断为真)
Email   邮箱地址
IPAddress   IPv4的IP地址
Length  规定字符长度
NumberRange 输入数值的范围
EqualTo 验证两个字段的一致性
URL 有效的URL
Regexp  正则验证

二、登录注册
前言
在flask中如何快速的实现登录注册注销功能,以及登录状态验证等功能? flask的扩展库中有Flask-Login库就可快速的实现以上的功能,实现起来是非常的便利。

1. 安装Flask-Login

pip install flask-login

2. 实现登录功能

2.1 定义login.html模板

{% extends 'base.html' %}

{% block title %}
    登录页面
{% endblock %}

{% block content %}
    <form action="" method="post">
        姓名:<input type="text" name="username">
        密码:<input type="text" name="password">
        <input type="submit" value="提交">
    </form>
{% endblock %}

2.2 实现登录功能
登录方法中定义被login_manager.user_loader装饰的回调函数,回调函数在如下两个地方被调用:
1)该函数表明当前用户登录成功时调用login_user()方法时,会被回调的函数。回调函数实现的功能是向会话上下文session中存储最为中间的键值对,key为user_id, value为当前登录用户的ID值。
2)回调函数在访问任何一个路由地址时也会被调用。
注意: 因为请求上下文在每次建立连接时,都需要获取当前登录用户并将当前登录用户设置为全局上下文current_user,因此回调函数返回的是当前登录系统的用户对象。
users->views.py

from flask_login import LoginManager, login_required, login_user, logout_user,current_user

# 获取登录管理对象
login_manager = LoginManager()  

@login_manager.user_loader
def load_user(user_id):
    # 必须编写一个函数用于从数据库加载用户。
    # 这个函数在login_user(user)存储当前登录用户到session中时,会被调用
    # 在每次访问地址的时候都被被调用,用于向请求上下文中绑定当前登录的用户信息
    return User.query.get(user_id)

@blue.route('/login/', methods=['GET', 'POST'])
def login():
    if request.method == 'GET':
        return render_template('login.html')

    if request.method == 'POST':
        username = request.form.get('username')
        password = request.form.get('password')
        # 校验用户名和密码是否填写完成
        if not all([username, password]):
            return render_template('login.html')
        # 通过用户名获取用户对象
        user = User.query.filter_by(username=username).first()
        # 校验密码是否正确
        if check_password_hash(user.password, password):
            # 实现登录
            # login_user()能够将已登录并通过load_user()的用户对应的User对象保存在session中
            # 在session中会创建一个键值对,key为user_id,value为当前登录用户的id值
            # 如果希望应用记住用户的登录状态, 只需要为 login_user()的形参 remember 传入 True 实参就可以了.
            login_user(user)
            return redirect(url_for('user.index'))
        else:
            flash('用户名或者密码错误')

        return redirect(url_for('user.index'))

2.3 启动文件进行配置
session_protection: 设置存储用户登录状态的安全级别
login_view: 设置登录验证失败的跳转地址

from user.views import login_manager

app.config['SECRET_KEY'] = os.urandom(24)

# 登录管理,初始化app
# 可以设置None,'basic','strong'以提供不同的安全等级,一般设置strong,如果发现异常会登出用户
# session_protection 能够更好的防止恶意用户篡改 cookies, 当发现 cookies 被篡改时, 该用户的 session 对象会被立即删除, 导致强制重新登录。
login_manager.session_protection='strong'

# 当登录认证不通过,则跳转到该地址
login_manager.login_view='user.login'
login_manager.init_app(app)

2.4 访问首页,登录校验
使用装饰器login_required()进行登录校验。
核心思想: 校验session中是否存在key为user_id的键值对。如果校验成功,则继续访问被装饰的函数。如果校验失败,则跳转到启动文件中定义的login_manager.login_view定义的视图函数。

@blue.route('/index/')
@login_required
def index():
    return render_template('index.html')

如果登录校验成功,则渲染index.html首页,在页面中可以解析全局变量current_user参数。

{% extends 'base.html' %}

{% block title %}
    首页页面
{% endblock %}

{% block content %}
    <p>我是首页</p>
    <p>当前登录系统用户为: {{ current_user.username }}</p>
{% endblock %}

2.5 注销
使用logout_user()方法实现注销,核心功能就是删除当前会话上下文session中的user_id键值对。
# 退出

@blue.route('/logout/', methods=['GET'])
@login_required
def logout():
    logout_user()
    return redirect(url_for('user.login'))

三、文件上传
1、配置路径
utils->settings.py

import os

# 基础路径
BASE_DIR=os.path.dirname(os.path.dirname(os.path.abspath(__file__)))

# static路径
STATIC_DIR=os.path.join(BASE_DIR,'static')

#templates路径
TEMPLATES_DIR=os.path.join(BASE_DIR,'templates')

# 上传路径
UPLOAD_DIR=os.path.join(os.path.join(STATIC_DIR,'media'),'upload')

2、保存文件

       # 获取图片
        icons = request.files.get('icons')
        #保存save(path)
        #绝对路径
        file_path=os.path.join(UPLOAD_DIR,icons.filename)
        #保存图片到绝对路径中
        icons.save(file_path)
        # 保存user对象
        user=current_user
        #将文件保存到数据库中
        user.icons=os.path.join('upload',icons.filename)
        db.session.add(user)
        db.session.commit()

3、html文件

{% extends 'base.html' %}
{% block title %}
首页

{% endblock %}
{% block content %}
首页
<p>当前用户{{current_user.username}}e</p>
<img src="/static/media/{{current_user.icons}}" alt="">
<form action="" method="post" enctype="multipart/form-data">
    头像:<input type="file" name="icons">
    <input type="submit" value="上传">
</form>

{% endblock %}

四、完整的登录注册代码
manager.py

from flask import Flask
from flask_script import Manager

from users.models import db
from users.views import users_blue, login_manager

app = Flask(__name__)

# 注册蓝图
app.register_blueprint(blueprint=users_blue, url_prefix='/users')

# 定义密钥
app.config['SECRET_KEY'] = 'secret_key'

# 配置数据库
app.config['SQLALCHEMY_DATABASE_URI'] = 'mysql+pymysql://root:123456@127.0.0.1:3306/f_login'
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False

# 没有登录跳转地址(装饰器)
login_manager.login_view='users.user_login'

# 绑定
db.init_app(app)
# 绑定flask-login(登录验证)
login_manager.init_app(app)

# 定义manager管理
manager = Manager(app=app)

if __name__ == '__main__':
    manager.run()

users->views.py

import os

from flask import Blueprint, request, render_template, redirect, url_for
from flask_login import login_user, LoginManager, current_user, login_required, logout_user
from werkzeug.security import generate_password_hash, check_password_hash

from users.forms import UsersRegisterForm, UsersLoginForm

from users.models import db, User
from utils.settings import UPLOAD_DIR

# current_user是全局变量,可以输出user的说有信息

# 定义蓝图
users_blue = Blueprint('users', __name__)

login_manager = LoginManager()


@users_blue.route('/register/', methods=['GET', 'POST'])
def register():
    form = UsersRegisterForm()
    if request.method == 'GET':
        return render_template('register.html', form=form)
    if request.method == 'POST':
        # 验证提交的字段信息
        if form.validate_on_submit():
            username=form.username.data
            password=form.password.data
            #实现注册,保存用户信息到User模型中
            user = User()
            user.username = username
            user.password = generate_password_hash(password)
            db.session.add(user)
            db.session.commit()
            return redirect(url_for('users.user_login'))
        else:
            # 验证失败,form.errors中存在错误信息
            return render_template('register.html', form=form)


@users_blue.route('/create_table/')
def create_table():
    db.create_all()
    return '创建成功'


# 有点类似中间件
@login_manager.user_loader
def load_user(user_id):
    return User.query.get(user_id)


@users_blue.route('/login/', methods=['GET', 'POST'])
def login():
    form = UsersLoginForm()
    if request.method == 'GET':
        return render_template('login.html', form=form)
    if request.method == 'POST':
        if form.validate_on_submit():
            username=form.username.data
            password=form.password.data
            user = User.query.filter(User.username == username).first()
            # 校验用户名和密码是否填写完整
            if user:
                # 获取用户,进行密码判断
                if check_password_hash(user.password,password):
                    #密码正确
                    #实现功能
                    login_user(user)
                    return redirect(url_for('users.index'))
                else:
                    error_password = '密码错误'
                    return render_template('login.html', error_password=error_password,form=form)
            else:
                # 获取不到用户,返回错误信息给页面
                error_username = '该用户没有注册,请去注册'
                return render_template('login.html',error_username=error_username,form=form)
        else:
            return render_template('login.html', form=form)


# @users_blue.route('/user_login/', methods=['GET', 'POST'])
# def user_login():
#     if request.method == 'GET':
#         return render_template('login.html')
#     if request.method == 'POST':
#         username = request.form.get('username')
#         password = request.form.get('password')
#         user = User.query.filter(User.username == username).first()
#         # 校验用户名和密码是否填写完整
#         if not all([username,password]):
#             return render_template('login.html')
#         if user:
#             # 获取用户,进行密码判断
#             if check_password_hash(user.password,password):
#                 #密码正确
#                 #实现功能
#                 login_user(user)
#                 return redirect(url_for('users.index'))
#         else:
#             # 获取不到用户,返回错误信息给页面
#             error = '该用户没有注册,请去注册'
#             return render_template('login.html',error=error)


@users_blue.route('/index/',methods=['GET', 'POST'])
@login_required
def index():
    if request.method == 'GET':
        return render_template('index.html')
    if request.method == 'POST':
        # 获取图片
        icons = request.files.get('icons')
        #保存save(path)
        # 作业:只保存jpg,png。。。 实现中间件,
        # @user_blueprint.before_request
        # @user_blueprint.after_request
        # @user_blueprint.teardown_request
        file_path=os.path.join(UPLOAD_DIR,icons.filename)
        icons.save(file_path)
        # 保存user对象
        user=current_user
        user.icons=os.path.join('upload',icons.filename)
        db.session.add(user)
        db.session.commit()
        return redirect(url_for('users.index'))


@users_blue.route('/logout/')
@login_required
def logout():
    logout_user()
    return redirect(url_for('users.user_login'))

users->models.py

from flask_login import UserMixin
from flask_sqlalchemy import SQLAlchemy

db = SQLAlchemy()


class User(UserMixin,db.Model):
    id = db.Column(db.Integer,primary_key=True,autoincrement=True)
    username = db.Column(db.String(20),unique=True,nullable=False)
    password = db.Column(db.String(130),nullable=False)
    icons = db.Column(db.String(100),nullable=True)

    __tablename__='user'

users->forms.py

from flask_wtf import FlaskForm
from wtforms import StringField, SubmitField, PasswordField
from wtforms.validators import DataRequired, EqualTo, ValidationError

from users.models import User


class UsersRegisterForm(FlaskForm):
    # 定义用户名和密码都是必填项
    username = StringField('姓 名', validators=[DataRequired()])
    password = PasswordField('密 码', validators=[DataRequired()])
    password2 = PasswordField('确认密码',
                              validators=[DataRequired(), EqualTo('password', '密码不一致')])
    submit = SubmitField('注册')

    def validate_username(self, field):
        user = User.query.filter(User.username == field.data).first()
        if len(field.data) < 3:
            raise ValidationError('用户名不能少于3个字符')
        if len(field.data) > 20:
            raise ValidationError('用户名不能大于20个字符')
        if user:
            raise ValidationError('用户名已存在')

    def validate_password(self, field):
        if len(field.data) < 6:
            raise ValidationError('密码不能少于6个字符')

    def validate_password2(self, field):
        if len(field.data) < 6:
            raise ValidationError('确认密码不能少于6个字符')


class UsersLoginForm(FlaskForm):
    username=StringField('用户名',validators=[DataRequired()])
    password = PasswordField('密码', validators=[DataRequired()])
    submit=SubmitField('登录')

    def validate_username(self, field):
        if len(field.data) < 3:
            raise ValidationError('用户名不能少于3个字符')
        if len(field.data) > 20:
            raise ValidationError('用户名不能大于20个字符')

    def validate_password(self, field):
        if len(field.data) < 6:
            raise ValidationError('密码不能少于6个字符')

utils->settings.py

import os

# 基础路径
BASE_DIR=os.path.dirname(os.path.dirname(os.path.abspath(__file__)))

# static路径
STATIC_DIR=os.path.join(BASE_DIR,'static')

#templates路径
TEMPLATES_DIR=os.path.join(BASE_DIR,'templates')

# 上传路径
UPLOAD_DIR=os.path.join(os.path.join(STATIC_DIR,'media'),'upload')

templates->base.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>{% block title %}{% endblock %}</title>
    {% block css %}
    {% endblock %}

    {% block js %}
    {% endblock %}
</head>
<body>
{% block content %}
{% endblock %}
</body>
</html>

templates->base_main.html

{% extends 'base.html' %}
{% block js %}
    <script src="http://ajax.googleapis.com/ajax/libs/jquery/1.3.2/jquery.min.js"type="text/javascript"></script>
{% endblock %}

templates->register.html

{% extends 'base_main.html' %}
{% block title %}
注册
{% endblock %}
{% block content %}
<form action="" method="post">
    {{ form.csrf_token }}

    {% if form.errors.username %}
        {{ form.errors.username.0 }}
    {% endif %}
    <p>{{ form.username.label }}: {{ form.username }}</p>
    {% if form.errors.password %}
        {{ form.errors.password.0 }}
    {% endif %}
    <p>{{ form.password.label }}: {{ form.password }}</p>
    {% if form.errors.password2 %}
        {{ form.errors.password2.0 }}
    {% endif %}
    <p>{{ form.password2.label }}: {{ form.password2 }}</p>
    {{ form.submit() }}
</form>

{% endblock %}

templates->login.html

{% extends 'base_main.html' %}
{% block title %}
登录
{% endblock %}
{% block content %}
<form action="" method="post">
    {{ form.csrf_token }}

    {% if form.errors.username %}
    {{ form.errors.username.0 }}
    {% endif %}

    {% if error_username %}
    {{ error_username }}
    {% endif %}

    <p>{{ form.username.label }}: {{ form.username }}</p>

    {% if form.errors.password %}
    {{ form.errors.password.0 }}
    {% endif %}

    {% if error_password %}
    {{ error_password }}
    {% endif %}

    <p>{{ form.password.label }}:{{ form.password }}</p>

    {{ form.submit() }}
    <!--<p>姓名:<input type="text" name="username"></p>-->
    <!--<p>密码:<input type="password" name="password"></p>-->
    <!--<input type="submit" value="登录">-->
</form>

{% endblock %}

templates->index.html

{% extends 'base.html' %}
{% block title %}
首页

{% endblock %}
{% block content %}
首页
<p>当前用户{{current_user.username}}e</p>
<img src="/static/media/{{current_user.icons}}" alt="">
<form action="" method="post" enctype="multipart/form-data">
    头像:<input type="file" name="icons">
    <input type="submit" value="上传">
</form>

{% endblock %}

作者:晓晓的忍儿
链接:https://www.jianshu.com/p/9e343d10f7a2
来源:简书
简书著作权归作者所有,任何形式的转载都请联系作者获得授权并注明出处。

发表评论

电子邮件地址不会被公开。 必填项已用*标注