8.用户登录与注册

一. viewmodel意义体现和filter函数的巧妙应用

我们想要在搜索页面上每一本书都显示作者/出版社/价格


我们在app/view_models/book.py的BookViewModel中已经定义了self.author,self.publisher,self.price, 我们需要将他们用'/'连接起来显示。在Jinja2模版中,做这样的操作比较困难, 我们选择在app/view_models/book.py的BookViewModel中实现:

class BookViewModel:
    def __init__(self, book):
        self.title = book['title']
        self.publisher = book['publisher']
        self.author = ''.join(book['author'])
        self.image = book['image']
        self.price = book['price']
        self.summary = book['summary']
        self.pages = book['pages']

    @property
    def intro(self):
        intros = filter(lambda x: True if x else False, [self.author, self.publisher, self.price])
        # filter(func, iterable) 是比较好用的python内置函数
        return '/'.join(intros)

只要在Jinja2模版中{{ book.intro }}就可以显示了(search_result.html 28行)


二. 书籍详情页面业务逻辑分析

详情页的业务逻辑: 


可以访问网站查看逻辑:http://www.yushu.im/book/9787806579060/detail


三. 实现书籍详情页面

在鱼书搜索结果点击想看的书, 会跳转到详情页面。即book_detail视图函数对应的template页面:
app/web/book.py新增book_detail视图函数:

@web.route('/book/<isbn>/detail')
def book_detail(isbn):
    yushu_book = YuShuBook()
    yushu_book.search_by_isbn(isbn)
    book = BookViewModel(yushu_book.books[0])
    return render_template('book_detail.html', book=book, wishes=[], gifts=[])

这里yushu_book.books[0]对于别人去调用你的代码, 可读性不好。
修改YuShuBook类, 新增first方法:

  @property
    def first(self):
        return self.books[0] if self.total >= 1 else None

yushu_book.books[0]改为yushu_book.first


四. 模型与模型的关系

实现在详情页中 赠送此书的 逻辑。 


我们用新的模型gift来定义要赠送的书籍, user定义用户
我们先修改原来的models文件夹的结构:
新增app/models/base.py:


from flask_sqlalchemy import SQLAlchemy

db = SQLAlchemy()

让book.py的db从base.py导入:

from app.models.base import db

gift.py, user.py也从base.py导入db
user.py:

from app.models.base import db
from sqlalchemy import Column, Integer, String, Boolean, Float


class User(db.Model):
    id = Column(Integer, primary_key=True)
    nickname = Column(String(24), nullable=False)
    phone_number = Column(String(18), unique=True)
    email = Column(String(50), unique=True, nullable=False)
    confirmed = Column(Boolean, default=False)
    beans = Column(Float, default=0)
    send_counter = Column(Integer, default=0)
    receive_counter = Column(Integer, default=0)
    wx_open_id = Column(String(50))  # 将来开发小程序使用的
    wx_name = Column(String(32))

gift.py:

from app.models.base import db
from sqlalchemy import Column, Integer, String, Boolean, ForeignKey
from sqlalchemy.orm import relationship


class Gift(db.model):
    id = Column(Integer, primary_key=True)
    launched = Column(Boolean, default=False)  # 是否送出去了
    user = relationship('User')
    uid = Column(Integer, ForeignKey('user.id'))
    isbn = Column(String(15), nullable=False)

五. 自定义基类模型

数据库存在假删除, 在数据库模型中增加status项,来判断是否删除。而不是直接物理删除。 这样的好处是: 当我们要分析用户行为时,就能派上用场.
在app/models/base.py中新增:

from flask_sqlalchemy import SQLAlchemy
from sqlalchemy import Column, Integer, SmallInteger

db = SQLAlchemy()


class Base(db.Model):
    status = Column(SmallInteger)    # 是否删除


在book.py, gift.py, user.py中, 统一从继承db.Model改为Base


但是运行会报错:
sqlalchemy.exc.ArgumentError: Mapper Mapper|Base|base could not assemble any primary key columns for mapped table 'base'

sqlalchemy会尝试创建Base表, 但我们只想让他作为基类:
修改base.py:

from flask_sqlalchemy import SQLAlchemy
from sqlalchemy import Column, Integer, SmallInteger

db = SQLAlchemy()


class Base(db.Model):
    __abstract__ = True    # 这样sqlalchemy就不会去创建Base的表了
    status = Column(SmallInteger)



六. 用户注册

我们新增用户系统 


统一写在app/web/auth.py中:

from .blueprint import web
from flask import render_template, request


__author__ = 'cannon'


@web.route('/register')
def register():
    return render_template('auth/register.html', form={'data': {}})

运行flask, 访问register的url, 我们可以看到: 


我们只是用http的get方法获取了网页, 接下来需要使用post方法, 对昵称、邮箱、密码提交进入数据库。


七.Python的动态赋值

我们在app/forms下新建auth.py:

from wtforms import Form, StringField, IntegerField, PasswordField
from wtforms.validators import Length, NumberRange, DataRequired, Email


class RegisterForm(Form):
    nickname = StringField(validators=[DataRequired(), Length(2, 10, message='昵称至少两个字符,最多10个字符')])
    email = StringField(validators=[DataRequired(), Length(min=8, max=64), Email(message='电子邮件格式错误')])
    password = PasswordField(validators=[Length(min=6, max=32), DataRequired(message='密码不能为空')])

修改视图函数app/web/auth.py的register函数:

@web.route('/register', methods=['GET', 'POST'])
def register():
    form = RegisterForm(request.form)
    if request.method == 'POST' and form.validate():   # 通过验证,则存入数据库
        user = User()
        user.nickname = form.nickname.data
        user.password = form.password.data
        user.email = form.email.data
    return render_template('auth/register.html', form={'data': {}})

如果user和form有很多属性需要赋值,这样的代码显然不合适。
修改app/models/base.py,新增set_attrs方法:

class Base(db.Model):
    __abstract__ = True
    status = Column(SmallInteger)

    def set_attrs(self, attrs_dict):
        for key, value in attrs_dict.items():
            if hasattr(self, key) and key != 'id':
                setattr(self, key, value)

利用set_attrs方法,优化register函数:

@web.route('/register', methods=['GET', 'POST'])
def register():
    form = RegisterForm(request.form)
    if request.method == 'POST' and form.validate():   # 通过验证,则存入数据库
        user = User()
        user.set_attrs(form.data)   # form.data得到form所有参数数据的dict形式
    return render_template('auth/register.html', form={'data': {}})

发现一个问题:

form中有password,但User模型中没有。 
我们可以直接在User中增加password吗?

答案是: 不可以. 我们数据库中保存的密码不能为明文。写入前需要加密


八. password加密处理

我们在app/models/user.py中,对password进行加密,而register视图函数不做修改:
app/models/user.py:

from app.models.base import Base
from sqlalchemy import Column, Integer, String, Boolean, Float
from werkzeug.security import generate_password_hash, check_password_hash


class User(Base):
    id = Column(Integer, primary_key=True)
    nickname = Column(String(24), nullable=False)
    phone_number = Column(String(18), unique=True)
    _password = Column('password', String(128), nullable=False)  # 数据库表的字段名默认为为_password 这里手动设置为password
    email = Column(String(50), unique=True, nullable=False)
    confirmed = Column(Boolean, default=False)
    beans = Column(Float, default=0)
    send_counter = Column(Integer, default=0)
    receive_counter = Column(Integer, default=0)
    wx_open_id = Column(String(50))  # 将来开发小程序使用的
    wx_name = Column(String(32))

    @property
    def password(self):
        return self._password

    @password.setter
    def password(self, raw):
        self._password = generate_password_hash(raw)

    def check_password(self, raw):   # 检查传入密码是否正确
        return check_password_hash(self._password, raw)



九. ORM的方式保存模型

在app/web/auth.py的register视图函数中,orm的方式把数据存入数据库:
并且我们得把form传给模版,让页面把用户输入的错误信息显示出来

@web.route('/register', methods=['GET', 'POST'])
def register():
    form = RegisterForm(request.form)
    if request.method == 'POST' and form.validate():   # 通过验证,则存入数据库
        user = User()
        user.set_attrs(form.data)   # form.data得到form所有参数数据的dict形式
        db.session.add(user)   # 将数据保存进数据库
        db.session.commit()
    return render_template('auth/register.html', form=form)  # form传给模版


十. 自定义验证器

数据库中已有email, 以后昵称等 验证情况flask并没有内置, 我们需要自定义
修改app/forms/auth.py:

from wtforms import Form, StringField, IntegerField, PasswordField
from wtforms.validators import Length, NumberRange, DataRequired, Email, ValidationError
from app.models.user import User


class RegisterForm(Form):
    nickname = StringField(validators=[DataRequired(), Length(2, 10, message='昵称至少两个字符,最多10个字符')])
    email = StringField(validators=[DataRequired(), Length(min=8, max=64), Email(message='电子邮件格式错误')])
    password = PasswordField(validators=[Length(min=6, max=32), DataRequired(message='密码不能为空')])

    # 自定义验证器
    def validate_email(self, field):
        '''
        validate_email函数名中的email,会帮助定位到email,表名这个验证器是email的验证器
        :param field:  field代表传入的email参数
        :return:
        '''
        if User.query.filter_by(email=field).first():   # filter_by可以传入一组查询条件
            raise ValidationError('电子邮件已被注册')  # validators特有的异常
        
    def validate_nickname(self, field):
       
        if User.query.filter_by(nickname=field).first():   
            raise ValidationError('该昵称已被注册')  


十一. redirect重定向

在注册完成后,需要自动跳转到login登录页面。 我们可以在视图函数中使用redirect重定向

app/forms/auth.py:

from .blueprint import web
from flask import render_template, request, redirect, url_for
from app.forms.auth import RegisterForm, LoginForm
from app.models.user import User
from app.models.base import db


__author__ = 'cannon'


@web.route('/register', methods=['GET', 'POST'])
def register():
    form = RegisterForm(request.form)
    if request.method == 'POST' and form.validate():   
        user = User()
        user.set_attrs(form.data)   
        db.session.add(user)
        db.session.commit()
        return redirect(url_for('web.login'))   # 注册完成后重定向, 原理是修改location的信息,即url_for得到的url
    return render_template('auth/register.html', form=form)


@web.route('/login', methods=['GET', 'POST'])
def login():
    form = LoginForm(request.form)
    if request.method == 'POST' and form.validate():  # 通过验证,则存入数据库
        user = User.query.filter_by(email=form.email.data).first
        if user and user.check_password(form.password.data):  # 检查密码是否正确
            pass
        else:
            flash('账号不存在或密码错误')

    return render_template('auth/login.html', form=form)

我们在app/forms/auth.py中增加LoginForm:

class LoginForm(Form):
    email = StringField(validators=[DataRequired(), Length(min=8, max=64), Email(message='电子邮件格式错误')])
    password = PasswordField(validators=[Length(min=6, max=32), DataRequired(message='密码不能为空')])


十二. cookie

用户系统的逻辑: 


‘票据’就是cookie

cookie的应用举例:

1.浏览一个需要登录的网站,保存密码后, 下次浏览该网站会自动登录
2.在京东搜索一个电饭煲, 之后浏览腾讯视频时,会给你电饭煲的推送广告。

简单使用:

response = make_response('设置cookie')
response.set_cookie('name', 'cannon', 100)  # 参数keyvalue,过期时间100s

如果不设置有效期,cookie在浏览器关闭就会消失。


十三. login_user 实现用户登录

pip install flask_login, 修改app/init.py:

from flask import Flask
from app.models.base import db
from flask_login import LoginManager   # 导入login

login_manager = LoginManager()


def create_app():
    app = Flask(__name__)
    app.config.from_object('app.secure')
    app.config.from_object('app.setting')
    register_blueprint(app)  

    db.init_app(app) 
    db.create_all(app=app)  

    login_manager.init_app(app)  # 初始化login_manager
    return app


def register_blueprint(app):  
    from app.web.blueprint import web
    app.register_blueprint(web)

修改app/web/auth.py login视图函数:

from flask_login import login_user

@web.route('/login', methods=['GET', 'POST'])
def login():
    form = LoginForm(request.form)
    if request.method == 'POST' and form.validate():  # 通过验证,则存入数据库
        user = User.query.filter_by(email=form.email.data).first
        if user and user.check_password(form.password.data):
            login_user(user, remember=True) # 设置remember=True则默认保存365天, 没有remember则是一次性cookie   
            # 实质依然是把 user数据保存在cookie  
        else:
            flash('账号不存在或密码错误')

    return render_template('auth/login.html', form=form)

我们可以才配置文件中对cookie进行设置(可以访问www.pythondoc.com/flask-login查阅): 


user是我们自定义的数据结构, login_user是如何判断选择数据来存入cookie呢?
我们需要在User模型中定义get_id函数

class User(Base):
    id = Column(Integer, primary_key=True)
    nickname = Column(String(24), nullable=False)
    phone_number = Column(String(18), unique=True)
    _password = Column('password', String(128), nullable=False)  
    email = Column(String(50), unique=True, nullable=False)
    confirmed = Column(Boolean, default=False)
    beans = Column(Float, default=0)
    send_counter = Column(Integer, default=0)
    receive_counter = Column(Integer, default=0)
    wx_open_id = Column(String(50))  
    wx_name = Column(String(32))

    @property
    def password(self):
        return self._password

    @password.setter
    def password(self, raw):
        self._password = generate_password_hash(raw)

    def check_password(self, raw): 
        return check_password_hash(self._password, raw)

    def get_id(self):  # flask_login插件使用
        return self.id

除了get_id以外, flask_login模块还有很多有用需要在model中定义的特定名称的函数, 如果要一一写一遍会很麻烦。有个简单方法: 使用UserMixin
app/models/user.py:

from app.models.base import Base
from sqlalchemy import Column, Integer, String, Boolean, Float
from werkzeug.security import generate_password_hash, check_password_hash
from flask_login import UserMixin   # 导入


class User(UserMixin, Base):  #继承UserMixin
    id = Column(Integer, primary_key=True)
    nickname = Column(String(24), nullable=False)
    phone_number = Column(String(18), unique=True)
    _password = Column('password', String(128), nullable=False)  
    email = Column(String(50), unique=True, nullable=False)
    confirmed = Column(Boolean, default=False)
    beans = Column(Float, default=0)
    send_counter = Column(Integer, default=0)
    receive_counter = Column(Integer, default=0)
    wx_open_id = Column(String(50)) 
    wx_name = Column(String(32))

    @property
    def password(self):
        return self._password

    @password.setter
    def password(self, raw):
        self._password = generate_password_hash(raw)

    def check_password(self, raw):  # 检查传入密码是否正确
        return check_password_hash(self._password, raw)



假如你想让cookie不是通过id验证身份, 依然要重写get_id函数,get_id函数默认使用的是id来做身份数据的。


十四. 登录才能访问

如何对一些视图函数做权限控制,比如需要用户登录才能访问的视图函数。 app/web/gift.py:

from .blueprint import web
from flask_login import login_required   # 使用login_required

__author__ = 'cannon'


@web.route('/my/gifts')
@login_required
def my_gifts():
    pass

同时需要在user模型中增加必要的代码:
app/models/user.py:

from app.models.base import Base
from sqlalchemy import Column, Integer, String, Boolean, Float
from werkzeug.security import generate_password_hash, check_password_hash
from flask_login import UserMixin   
from app import login_manager       # 导入之前在create_app中注册的login_manager


class User(UserMixin, Base):  
    id = Column(Integer, primary_key=True)
    nickname = Column(String(24), nullable=False)
    phone_number = Column(String(18), unique=True)
    _password = Column('password', String(128), nullable=False)  
    email = Column(String(50), unique=True, nullable=False)
    confirmed = Column(Boolean, default=False)
    beans = Column(Float, default=0)
    send_counter = Column(Integer, default=0)
    receive_counter = Column(Integer, default=0)
    wx_open_id = Column(String(50)) 
    wx_name = Column(String(32))

    @property
    def password(self):
        return self._password

    @password.setter
    def password(self, raw):
        self._password = generate_password_hash(raw)

    def check_password(self, raw):  
        return check_password_hash(self._password, raw)


@login_manager.user_loader
def get_user(uid):  # flask_login login_required用得
    return User.query.get(int(uid))  # 通过id返回用户模型


十五. 重定向需求

需求一:

现在在没有登录的情况下,访问@login_required的视图函数会报错。
这不是我们想要的,我们想要的是自动跳转到登录页面。
我们可以在create_app中增加login_manager.login_view = 'web.login'来实现。: app/init.py:

from flask import Flask
from app.models.base import db
from flask_login import LoginManager

login_manager = LoginManager()


def create_app():
    app = Flask(__name__)
    app.config.from_object('app.secure')
    app.config.from_object('app.setting')
    register_blueprint(app)  

    db.init_app(app)  
    db.create_all(app=app)  

    login_manager.init_app(app)
    login_manager.login_view = 'web.login'   # 定义 没登录情况下,访问@login_required的视图函数 的重定向url
    return app


def register_blueprint(app):  
    from app.web.blueprint import web
    app.register_blueprint(web)

现在没有登录的情况下访问 my_gifts视图函数, 会跳转到login界面: 


可以看到,在跳转后,flask_login会自动闪现Please log in to access this page., 我们不想要英文提示, 所以得自定义flash的内容。 
同样需要在create_app函数中增加内容:

def create_app():
    app = Flask(__name__)
    app.config.from_object('app.secure')
    app.config.from_object('app.setting')
    register_blueprint(app)  

    db.init_app(app) 
    db.create_all(app=app)  

    login_manager.init_app(app)
    login_manager.login_view = 'web.login'   
    login_manager.login_message = '请先登录或注册'  # 重定义flash的内容
    return app

需求二:

跳转到login页面, 登录完成后, 我们需要再自动跳转到 login之前的页面。这要才有较好的用户体验。
修改login视图函数:

@web.route('/login', methods=['GET', 'POST'])
def login():
    form = LoginForm(request.form)
    if request.method == 'POST' and form.validate():  # 通过验证,则存入数据库
        user = User.query.filter_by(email=form.email.data).first
        if user and user.check_password(form.password.data):
            login_user(user)  
            next = request.args.get('next')   # 获取之前页面的url
            if not next:                    # 如果没有之前页面的情况得考虑到
                next = url_for('web.index')
            return redirect(next)
        else:
            flash('账号不存在或密码错误')

    return render_template('auth/login.html', form=form)

十六. 重定向攻击

如果人为的给login的url添加next参数,比如:localhost:5000/login?next=http://www.qq.com, 
我们在login界面点击登录后, 会自动跳转到腾讯的网站。这是很危险的。
解决办法也很简答, 依然通过修改login视图函数实现:

@web.route('/login', methods=['GET', 'POST'])
def login():
    form = LoginForm(request.form)
    if request.method == 'POST' and form.validate():  
        user = User.query.filter_by(email=form.email.data).first
        if user and user.check_password(form.password.data):
            login_user(user)  
            next = request.args.get('next')
            if not next or next.startwith('/'):   # next不是以'/'开头, 就强行跳转到index主页面
                next = url_for('web.index')
            return redirect(next)
        else:
            flash('账号不存在或密码错误')

    return render_template('auth/login.html', form=form)

猜你喜欢

转载自blog.csdn.net/weixin_41207499/article/details/80750143