第九章 用户登录与注册(1/2)



一 、用户注册

非关键代码、重复的代码,直接复制并解释。

1.1 建立用户系统

赠送此书的业务逻辑,应该怎么完成?

赠送此书的业务逻辑要通过下面的视图函数来编写。

同时该业务逻辑又涉及到用户user这个模型。因此,应该建立一套用户系统。即常见的登录、注册、找回密码、修改密码等机制。

1.2 编写注册页面

在视图函数app|web|auth.py中:

pyfrom flask import render_template
from . import web


@web.route('/register')
def register():
    return render_template('auth/register.html')

-->
❌sqlalchemy.exc.ArgumentError: Mapper mapped class Base->base could not assemble any primary key columns for mapped table 'base'                # 基类模板在创建base这个数据表时,没有找到主键primary key

这是由于默认下,app|models下面的每一个类均会创建一个数据表。不让该类创建数据表即可。

改正:

app|models|base.py中:

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


db = SQLAlchemy()

class Base(db.Model):                            # 让base是一个基类,同时不去创建表
    __abstract__ = True                       # 告诉SQL alchemy,我不想创建一个表数据
    status = Column(SmallInteger, default=1)   

在浏览器中访问http://localhost:81/register:注册页面访问成功。

在哪里接受和处理用户提交的三个信息?

直接在视图函数register()中处理以下两种不同的请求:

(1)返回注册页面---------------------------------------get 请求

(2)处理注册页面所提交来的用户信息-----------post 请求

from flask import render_template, request
from . import web


@web.route('/register', methods=['GET', 'POST'])             # 视图函数默认只能处理get,所以要加上一个post动词
def register():
    request.form                                            # 拿到post提交的表单信息
    return render_template('auth/register.html', form={'data':{}})

二、利用Python动态语言特性,简化赋值、统一赋值

2.1 校验post提交的表单信息

拿到post提交的表单信息之后,就要进行校验。

在验证层中,新建一个目录:

app|forms|auth.py中:

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


class RegisterForm(Form):            # 对注册的表单信息进行验证
    email = StringField(validators=[DataRequired(), Length(8, 64), Email(message='电子邮箱不符合规范')])

    nickname = StringField(validators=[DataRequired(), Length(2, 10, message='昵称至少需要两个字符,最多10个字符')])

    password = PasswordField(validators=[DataRequired('密码不可以为空,请输入你的密码'), Length(6, 32)])

2.2 利用基类模型,在视图函数中简化赋值、统一赋值

app|models|base.py中:

用户提交过来的信息本项目中只有3条,可以直接在视图函数register中 赋值。但是,如果以后项目中用户提交过来的信息非常多呢?显然一个一个赋值不太现实。因此,为简化赋值,统一赋值,在基类模板中新定义一个方法set_attrs()

原理则是充分利用Python动态语言的特性:相同的覆盖,不相同的保留。

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


db = SQLAlchemy()

class Base(db.Model):           
    __abstract__ = True             
    status = Column(SmallInteger, default=1)      
    
    def set_attrs(self, attrs_dict):                    # 简化赋值,统一赋值
        '''
        遍历用户注册时传入的字典数据,如果有key与数据表模型中某一个属性相同,那么就将字典的key所对应的值,赋给模型的相关属性
        '''
        for key, value in attrs_dict.items():
            if hasattr(self, key) and key != 'id':        # 判断当前的对象,是否也包含名字为key的属性   # id号不能被赋值
                setattr(self, key, value)                 # 动态赋值

在视图函数app|web|auth.py中:

from flask import render_template, request
from . import web
from ..forms.auth import RegisterForm
from ..models.user import User


@web.route('/register', methods=['GET', 'POST'])     
def register():
    form = RegisterForm(request.form)                       # 调用上面刚建好的类,并实例化一个对象   
    if request.method == 'POST' and form.validate():     # 当只有post提交信息,且通过了validate校验,才执行后续
        user = User()                               # 在数据库中,实例化一条新用户的注册信息
            user.set_attrs(form.data)                 # 调用上面刚建好的方法set_attrs()        # form.data:包含了从客户端提交过来的所有参数

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

三、Python的属性描述符实现getter、setter

3.1 问题

user模型下面,压根儿就没有定义过属性password。同时,

用户传过来的密码是明文的,但是要保存到数据库中就必须对明文密码进行加密。

所以,上节所写不合理。

3.2 加密用户提交明文密码的两种方案

方案1:

机械呆板,不合理。因为数据库中不能以明文的形式保存密码。即使是后续对其进行了加密并覆盖的操作,也不行。

app|models|user.py中:

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


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的属性
    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))

在视图函数app|web|auth.py中: 完成先加密,再覆盖的操作。

from flask import render_template, request
from . import web
from ..forms.auth import RegisterForm
from ..models.user import User
from werkzeug.security import generate_password_hash                   # 导入这个函数,用于加密密码


@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)         # 将用户提交来的表单信息,全部赋值给数据表模型,此时密码是明文的!
        user.password = generate_password_hash(form.password.data)      # 对密码属性先加密,再覆盖上面

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

方案2:

其他用户提交过来的信息,都统一存储到了user模型中。对有特殊要求的个别数据:

(1)将属性的名字改成_password。因为属性变量名已改,所以在统一动态赋值的时候,用户提交过来的密码明文是不会存储到user模型中去的。

(2)为了对个别数据进行多一步操作,新定义以下两个函数用于预处理,即加密。

app|models|user.py中:

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

class User(Base):                                 
    # __tablename__ ='user1'                  
    id = Column(Integer, primary_key=True)            
    nickname = Column(String(24), nullable=False)      
    phone_number = Column(String(18), unique=True)
    _password = Column('password', String(128))              # 将下面刚定义好、已预先处理的的属性传进来
    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                                          # 将函数变为默认的只读属性 getter
    def password(self):
        return self._password                        # 最终,返回下面已加密过的_password

    @password.setter                                # 首先,将password属性变为只写属性setter
    def password(self, raw):                          # 将原始密码传进来
        self._password = generate_password_hash(raw)

在视图函数app|web|auth.py中:

就不用在视图函数中操作先加密,再覆盖了。密码加密的操作已经封装到了上面的user模型中了。视图函数表面上仍然风轻云淡、没有变化。

from flask import render_template, request
from . import web
from ..forms.auth import RegisterForm
from ..models.user import User
from werkzeug.security import generate_password_hash                  


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

两种方案的比较

方案1方案2
操作方式先将用户提交来的表单信息,全部赋值给数据表模型,
然后在视图函数中对password个别属性加密并覆盖掉上面
将数据表模型的属性名字改成_password
先对个别数据进行预处理加密
然后将结果同其他大部队一道,赋值给_password
思维先整体,后特殊先特殊,后整体

如果想让类下面的某一个变量,是只读的,或者是只写的,那么就可以用上面提到的setter、getter。

四、用ORM的方式保存模型

4.1 简单的对象-关系映射ORM

1.用户的注册信息最终要添加到数据库中去。但是,如何通过模型的方式实现呢?

在视图函数app|web|auth.py中:

from flask import render_template, request
from . import web
from ..forms.auth import RegisterForm
from ..models.user import User
from werkzeug.security import generate_password_hash                  


@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)                        # 将user模型,添加到数据库
        db.session.commit()                         # 提交
        
    return render_template('auth/register.html', form={'data':{}})

上面就是用ORM的思想操作数据库:即整个过程,不和数据库打交道,而是通过一个个的模型来完成数据库的操作。

ORM:"对象-关系映射"(Object/Relational Mapping) 的缩写。

2.小故障:Install 'email_validator' for email validation support.

课程没这个问题,是因为WTForms 3.0.1版本问题,从 WTForms 2.3.0开始,电子邮件验证器 Email() 由email-validator的外部库处理。

所以,需要再安装一下email-validator:

3.用户的注册信息成功添加到了数据库中去

在浏览器中:

在Navicat可视化数据表中:

4.2 解决输入非法的邮箱格式时出现的两个问题

1.当输入非法的邮箱格式时,出现两个问题:

  • 没有显示邮箱格式异常的错误提示
  • 跳转后的页面中,用户输入的信息全部丢失

因为报错的数据、用户填写的信息,都是放在了form里。

所以,要解决上述问题,就要把form也当作要渲染的数据,传入到render_template()函数中。

from flask import render_template, request
from . import web
from ..forms.auth import RegisterForm
from ..models.user import User
from werkzeug.security import generate_password_hash                  


@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=form)                # 将form传入进去

2.在浏览器中,什么也不填写:

成功出现了错误提示

3.在浏览器中,只填写前两个栏目:

刷新之后,用户上次提交的信息还在、没有丢失,成功保存了上次的信息。

五、用自定义验证器校验是否重名

5.1 在具体的业务逻辑中校验参数

问题:

参数校验时,只校验了参数的字面意思,并没有把参数放到具体的业务逻辑中来校验。

例如:Email虽然符合邮箱的规范,但是数据库中已经存在了一个同名的Email呢?nickname同理。

上述业务性质的校验,应该放在验证层里统一校验。

因此,需要自己编写一些验证器,即自定义验证器。

5.2 如何用模型做数据库的查询

db.session来查询。另外,还有更为便捷的方式:User.query.filter_by()

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


class RegisterForm(Form):
    email = StringField(validators=[DataRequired(), Length(8, 64), Email(message='电子邮箱不符合规范')])
    nickname = StringField(validators=[DataRequired(), Length(2, 10, message='昵称至少需要两个字符,最多10个字符')])
    password = PasswordField(validators=[DataRequired('密码不可以为空,请输入你的密码'), Length(6, 32)])

    def validate_email(self, field):                            # field:代表从客户端传过来的email参数
        '''
        自定义一个验证器,验证邮箱是否重名
        '''
        if User.query.filter_by(email=field.data).first():      # 查询条件  # 用first来触发这个查询:不管查询出多少条,只返回一条
            raise ValidationError('电子邮件已被注册')             # 特定的异常

同名邮箱被报出错误提示:

除了对Email做是否重名的业务逻辑校验,对nickname也要做:同理。

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


class RegisterForm(Form):
    email = StringField(validators=[DataRequired(), Length(8, 64), Email(message='电子邮箱不符合规范')])
    nickname = StringField(validators=[DataRequired(), Length(2, 10, message='昵称至少需要两个字符,最多10个字符')])
    password = PasswordField(validators=[DataRequired('密码不可以为空,请输入你的密码'), Length(6, 32)])

    def validate_email(self, field):                            
        '''
        自定义一个验证器,验证邮箱是否重名
        '''
        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('昵称已存在')            

六、redirect重定向

在用户成功注册了之后,应该跳转到哪个页面去?

注册失败,还是停留在注册页面;

注册成功,应该跳转到登录页面,要求用户再次输入账户、密码,进行登录。这是主流的方式。

在视图函数app|web|auth.py中:

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


@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()                 

        redirect(url_for('web.login'))                        # 重定向,跳转到下面刚定义的登录页面视图函数

    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():
        pass
    return render_template('auth/login.html', form=form)

在验证层app|forms|auth.py中:

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


class RegisterForm(Form):
    email = StringField(validators=[DataRequired(), Length(8, 64), Email(message='电子邮箱不符合规范')])
    nickname = StringField(validators=[DataRequired(), Length(2, 10, message='昵称至少需要两个字符,最多10个字符')])
    password = PasswordField(validators=[DataRequired('密码不可以为空,请输入你的密码'), Length(6, 32)])

    def validate_email(self, field):                           
        '''
        自定义一个验证器,验证邮箱是否重名
        '''
        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('昵称已存在')


class LoginForm(Form):                            # 新建一个登录的验证层,供在上面的视图函数中实例化使用
    email = StringField(validators=[DataRequired(), Length(8, 64), Email(message='电子邮箱不符合规范')])
    password = PasswordField(validators=[DataRequired('密码不可以为空,请输入你的密码'), Length(6, 32)])

声明:Jerry's Blog|版权所有,违者必究|如未注明,均为原创|本网站采用BY-NC-SA协议进行授权

转载:转载请注明原文链接 - 第九章 用户登录与注册(1/2)


Stop chasing money, and start chasing the solutions to the problem.