美文网首页程序员flask高级编程-鱼书FLASK入门
慕课网Flask高级编程实战-8.用户登录与注册

慕课网Flask高级编程实战-8.用户登录与注册

作者: 9c0ddf06559c | 来源:发表于2018-06-13 10:07 被阅读33次

    专题章节目录

    慕课网Flask高级编程实战-知识点思维导图

    慕课网Flask高级编程实战-1.项目准备 和 Flask入门

    慕课网Flask高级编程实战-2.搜索书籍路由编写

    慕课网Flask高级编程实战-3.蓝图、模型与CodeFirst

    慕课网Flask高级编程实战-4.flask核心机制

    慕课网Flask高级编程实战-5.书籍详情页面的构建

    慕课网Flask高级编程实战-6.书籍详情页面的构建

    慕课网Flask高级编程实战-7.静态文件、模板、消息闪现与Jinja2

    慕课网Flask高级编程实战-8.用户登录与注册

    慕课网Flask高级编程实战-9.书籍交易模型(数据库事务、重写Flask中的对象)

    慕课网Flask高级编程实战-10.鱼书业务处理

    慕课网Flask高级编程实战-11.Python与Flask的结合应用

    慕课网Flask构建可扩展的RESTful API-1. 起步与红图

    慕课网Flask构建可扩展的RESTful API-2. REST基本特征

    慕课网Flask构建可扩展的RESTful API-3. 自定义异常对象

    慕课网Flask构建可扩展的RESTful API-5. Token与HTTPBasic验证 —— 用令牌来管理用户

    慕课网Flask构建可扩展的RESTful API-6. 模型对象的序列化

    慕课网Flask构建可扩展的RESTful API-7. 权限控制

    8.1 viewmodel意义的体现与filter函数的巧妙应用

    在搜索书籍页面里,需要将每一条结果的作者,出版社,价格在一行展示,并以”/“分割。由于这三个属性还有可能为空,所以在html模板里处理不太方便。我们选择将这些数据处理的工作放在viewmodel中。

    简单粗暴一点的方法是写一段 if-else 代码,将这三个属性以及可能为空的情况全都穷举出来,但是python给我们提供了更优雅的解决方式,就是使用filter过滤器+lambda表达式

    class BookViewModel:
    
    def __init__(self, book):
    self.title = book['title'],
    self.publisher = book['publisher'],
    self.pages = book['pages'],
    self.author = '、'.join(book['author']),
    self.price = book['price'],
    self.summary = book['summary'],
    self.image = book['image']
    
    # @property注解可以让我们把一个方法当做一个属性来使用
    @property
    def intro(self):
    intros = filter(lambda x: True if x else False,
    [self.author[0], self.publisher[0], self.price[0]])
    
    return ' / '.join(intros)
    

    8.2 书籍详情页面

    1.业务逻辑梳理

    • 书籍详情页面,首先应该展示数据详情信息。
    • 书籍详情页面应该有加入心愿清单和赠送此书的功能
    • 书籍详情页面默认展示想要赠书次数的人,并且可以向他们索要书籍
    • 如果用户点击了赠送此书,那么他就成了一个赠书人,这个时候书籍详情页面会展示出想要这本书的人

    2.编写思路

    • 书籍详情页面接受一个isbn作为参数,直接访问我们之前编写的yushu_book的search_by_isbn函数即可。这需要我们在之前的BookViewModel中加入isbn属性
    • search_by_isbn返回的原始数据不应该直接返回,而应该经过裁剪加工,这里也可以复用我们之前写的BookViewModel。
    • BookViewModel需要接受一个book对象,由于search_by_isbn只会返回只有一个对象的列表,所以我们返回结果的第一个元素即可
    • 但是yushu_book.books[0]的写法并不是很好的编程规范,我们之所以可以这么写是因为我们清楚的内部结构,但是我们写的代码不一定是给我们自己用,给被人用的时候要让被人清晰易懂,所以这里,我们在yushu_book加入一个first函数返回第一个对象。

    3.实现代码

    web/book.py

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

    spider/yushu_book.py

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

    view_models/book.py

    
    class BookViewModel:
    
    def __init__(self, data):
    self.title = data['title']
    self.author = '、'.join(data['author'])
    self.binding = data['binding']
    self.publisher = data['publisher']
    self.image = data['image']
    self.price = '¥' + data['price'] if data['price'] else data['price']
    self.isbn = data['isbn']
    self.pubdate = data['pubdate']
    self.summary = data['summary']
    self.pages = data['pages']
    
    @property
    def intro(self):
    intros = filter(lambda x: True if x else False,
    [self.author, self.publisher, self.price])
    return ' / '.join(intros)
    

    8.3 模型关系

    分析业务逻辑,用户赠送书籍,需要将用户赠送书籍的数据保存到数据库中。为此我们需要建立业务模型,并通过codeFirst的原则,反向生成数据库表

    1.模型与模型关系

    • 首先我们需要一个用户User模型,来存储用户信息
    • 其次我们需要一个Book模型,来存储书籍的信息
    • 我们还需要一个Gift模型,来存储哪个用户想要赠送哪本书。从数据库的角度来看,用户和书籍是多对多的关系,多对多的关系需要第三章表。


      image.png

    2.编写建立模型代码

    models/user.py

    
    from sqlalchemy import Column
    from sqlalchemy import Integer, Float
    from sqlalchemy import String, Boolean
    
    from app.models.base import db
    
    
    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))
    

    models/gift.py

    from app.models.base import db
    from sqlalchemy import Column, String, Integer, ForeignKey, Boolean
    from sqlalchemy.orm import relationships
    
    __author__ = "gaowenfeng"
    
    
    class Gift(db.Model):
    id = Column(Integer, primary_key=True, autoincrement=True)
    # relationships表示管理关系
    user = relationships('User')
    # ForeignKey定义外键约束
    uid = Column(Integer, ForeignKey('user.id'))
    # 书籍我们记录isbn编号,因为书籍信息是从网络获取的
    isbn = Column(String(15),nullable=True)
    # 是否已经赠送出去
    launched = Column(Boolean, default=False)
    

    3.自定义基类模型

    • 每个表的信息,在删除的时候都不应该物理的从数据库里删除,而应该设置一个标志位,默认为0,如果删除了则置为1,这样可以搜索到历史的用户记录。
    • 像标志位这样的参数,每个表里都有同样的属性,我们应该建立一个基类,来存储这些共有属性

    base.py

    from flask_sqlalchemy import SQLAlchemy
    from sqlalchemy import SmallInteger, Column
    
    __author__ = "gaowenfeng"
    
    db = SQLAlchemy()
    
    
    class Base(db.Model):
    __abstract__ = True
    status = Column(SmallInteger, default=1)
    

    8.4 用户注册

    • 用户注册的界面,和注册POST请求共用同一个视图函数,兼容POST,GET请求。
    • 表单验证的结果数据,赋值到User模型里,可以在Base类里编写一个set_attrs函数,统一将属性拷贝赋值。动态赋值。
    • 验证器中还应该加入业务逻辑的校验,如email不能重复,这需要自己定义验证器,以vaildate_开头
    • 使用filter_by自定义数据库查询
    • 数据库的密码,前端传来的是明文,需要密文加密到数据库,应该给User的password提供getter/setter函数。在set值的时候,将password加密在赋值给User的_password。
    • 使用db.session,采用ORM方式将数据存储到数据库
    • 如果登录成功,则重定向到登录界面

    web/auth.py

    @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 render_template('auth/register.html', form={'data': {}})
    

    models/user.py

    class User(Base):
    id = Column(Integer, primary_key=True)
    nickname = Column(String(24), nullable=False)
    _password = Column('password', String(128))
    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))
    
    @property
    def password(self):
    return self._password
    
    @password.setter
    def password(self, raw):
    self._password = generate_password_hash(raw)
    

    models/base.py

    db = SQLAlchemy(query_class=Query)
    
    
    class Base(db.Model):
    __abstract__ = True
    status = Column(SmallInteger, default=1)
    
    def set_attrs(self, attrs_dict):
    for key, value in attrs_dict.items():
    if hasattr(self, key) and key != 'id':
    setattr(self, key, value)
    

    forms/auth.py

    class RegisterForm(Form):
    email = StringField(validators=[
    DataRequired(), Length(8, 64, message='电子邮箱不符合规范')])
    
    nickname = StringField('昵称', validators=[
    DataRequired(), Length(2, 10, message='昵称至少需要两个字符,最多10个字符')])
    
    password = PasswordField('密码', validators=[
    DataRequired(), Length(6, 20)])
    def validate_email(self, field):
    # User.query.filter_by(email=field.data).first() 等同于 select * from user where name = field.data limit 1
    if User.query.filter_by(email=field.data).first():
    raise ValidationError('电子邮件已被注册')
    
    def validate_nickname(self, field):
    if User.query.filter_by(nickname=field.data).first():
    raise ValidationError('昵称已存在')
    

    8.5 用户登录

    • 接受用户传来的参数并进行参数校验
    • 使用email查询数据库并验证密码是否正确,密码的加密校验应该放在User模型类里,这样可以使得封装性更好,外部调用更加方便
    • email和密码校验未通过,则通过消息闪现通知客户端消息
    • email和密码校验通过,则通过flask提供的插件flask_login(http://www.pythondoc.com/flask-login/) 将数据写入cookie

    web/auth.py

    ...
    ...
    @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).first()
    if user and user.check_password(user.password):
    # 使用flask-login 的 login_user间接写入cookie
    # 默认是暂时的cookie,关闭浏览器后cookie消失,如果想改成长期的需要传入关键字参数remember
    login_user(user, remember=True)
    else:
    flash("账号不存在或者密码错误")
    return render_template('auth/login.html', form=form)
    ...
    ...
    

    login_user方法并不是把user内的搜索属性全都写入cookie,login_user需要我们为user类定义几个方法,如get_id用来获取id。如果我们把他定义的方法全都编写出来,太多了,我们可以集成他提供给我们的UserMixin类,如果和他的默认配置不同,复写他的方法即可

    image.png

    models/user.py

    ...
    ...
    from flask_login import UserMixin
    
    from app.models.base import Base
    
    
    class User(UserMixin, Base):
    ...
    ...
    

    flask-login模块也需要在app中进行注册,注册方法同SQLALChemy
    app/__init__.py

    login_manager = LoginManager()
    
    
    def create_app():
    app = Flask(__name__)
    app.config.from_object("app.secure")
    app.config.from_object("app.settings")
    # 注册蓝图
    register_blueprint(app)
    
    # 注册SQLAlchemy
    db.init_app(app)
    
    # 创建所有表
    with app.app_context():
    db.create_all()
    
    # 注册LoginManager
    login_manager.init_app(app)
    return app
    
    
    def register_blueprint(app):
    from app.web import web
    app.register_blueprint(web)
    

    8.6 访问权限控制与重定向攻击

    1.在需要限制登录才能访问的试图函数上,加入@login_required装饰器

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

    2.在User模型里,编写get_user 方法用来根据id查询用户,并加入@login_manager.user_loader 装饰器(login_manager是从app/__init__.py中导入)

    @login_manager.user_loader
    def get_user(self, uid):
    # 如果是根据主键查询,不要filter_by,使用get方法即可
    return User.query.get(int(uid))
    

    3.在app/__init__.py中,配置未登录时调整到的页面和提示消息

    login_manager.login_view = 'web.login'
    login_manager.login_message = '请先登录或注册'
    

    4.登录成功以后,重定向到next页面;如果没有next页面,则跳转到首页;为了防止重定向攻击,应该判断next是否"/"开头

    @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)
    # request.form 获取表单信息;
    # request.args获取url路径?后面的信息
    next = request.args.get('next')
    # next.startswith('/')防止重定向攻击
    if not next or not next.startswith('/'):
    return redirect(url_for('web.index'))
    return redirect(next)
    else:
    flash("账号不存在或者密码错误")
    return render_template('auth/login.html', form=form)
    

    相关文章

      网友评论

      本文标题:慕课网Flask高级编程实战-8.用户登录与注册

      本文链接:https://www.haomeiwen.com/subject/pbadeftx.html