第九章 用户认证

作者: 藕丝空间 | 来源:发表于2017-11-06 18:34 被阅读31次

    大多数程序都需要进行用户跟踪。用户链接程序时需要进行身份认证,通过这一过程,让程序知道自己的身份。程序知道用户是谁后,就能提供有针对性的个性化体验。最常用的认证方法要求用户提供一个身份证明(用户的电子邮件、电话或用户名)和一个密码。

    Flask的认证扩展

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

    使用pip安装Flask-Login:

    (flask)$ pip install flask-login
    

    密码安全性

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

    使用Werkzeug实现密码散列

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

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

    修改app/models.py, 在在User模型中加入密码散列

    from werkzeug.security import generate_password_hash, check_password_hash
    class User(db.Model):
        __tablename__ = 'users'
        id = db.Column(db.Integer, primary_key=True)
        email = db.Column(db.String(64), unique=True, index=True)
        username = db.Column(db.String(64), unique=True, index=True)
        role_id = db.Column(db.Integer, db.ForeignKey('roles.id'))
        password_hash = db.Column(db.String(128))
        @property
        def password(self):
            raise ArithmeticError('非明文密码,不可读。')
        @password.setter
        def password(self, password):
            self.password_hash = generate_password_hash(password=password)
        def verify_password(self, password):
            return check_password_hash(self.password_hash, password=password)
        def __repr__(self):
            return '<User %r>' % self.username
    

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

    verify_password方法接受一个参数(即密码),将其传给Werkzeug提供的check_password_hash()函数,和存储在User模型中的密码散列值进行比对。如果这个方法返回True,就表明密码是正确的。

    准备用于登录的用户模型

    要想使用Flask-Login扩展,程序的User模型必须实现几个方法。需要实现的方法如下表:

    方法 说明
    is_authenticated() 如果用户已经登录,必须返回True,否则返回False
    is_active() 如果允许用户登录,必须返回True,否则返回False。如果要禁用账户,可以返回False
    is_anonymous() 对普通用户必须返回False
    get_id() 必须返回用户的唯一标示符,使用Unicode编码字符串

    这四个方法可以在模型类中作为方法直接实现,不过还有一种更简单的替代方法。Flask-Login提供了一个UserMixin类,其中包含这些方法的默认实现,且能满足大多数需求。在app/models.py中修改User模型,支持用户登录

    from flask.ext.login import UserMixin, AnonymousUserMixin
    class User(UserMixin, db.Model):
        __tablename__ = 'users'
        id = db.Column(db.Integer, primary_key=True)
        email = db.Column(db.String(64), unique=True, index=True)
        username = db.Column(db.String(64), unique=True, index=True)
        role_id = db.Column(db.Integer, db.ForeignKey('roles.id'))
        password_hash = db.Column(db.String(128))
    

    修改app/__init__.py, 初始化Flask-Login

    from flask.ext.login import LoginManager
    login_manager = LoginManager()
    login_manager.session_protection = 'strong'
    login_manager.login_view = 'auth.login'
    def create_app(config_name):
        """ 使用工厂函数初始化程序实例"""
        app = Flask(__name__)
        app.config.from_object(config[config_name])
        config[config_name].init_app(app=app)
        mail.init_app(app=app)
        moment.init_app(app=app)
        db.init_app(app=app)
        login_manager.init_app(app=app)
        # 注册蓝本 main
        from .main import main as main_blueprint
        app.register_blueprint(main_blueprint)
        # 注册蓝本 auth
        from .auth import auth as auth_blueprint
        app.register_blueprint(auth_blueprint, url_prefix='/auth')
        return app
    

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

    最后,Flask-Login要求程序实现一个回调函数,使用指定的标示符加载用户
    修改app/models.py,加载用户的回调函数

    from .import login_manager
    @login_manager.user_loader
    def load_user(user_id):
        return User.query.get(int(user_id))
    

    加载用户的回调函数接受以Unicode字符串形式表示的用户标示符。如果能找到用户,这个函数必须返回用户对象;否在应该返回None。

    添加登录表单

    呈现给用户的登录表单中包含一个用户输入电子邮件地址的文本字段、一个密码字段、一个“记住我”复选框和提交按钮。这个表单使用Flask-WTF类。
    修改app/auth/forms.py登录表单

    from flask.ext.wtf import Form
    from wtforms import StringField, PasswordField, BooleanField, SubmitField
    from wtforms.validators import DataRequired, Length, Email 
    
    class LoginForm(Form):
        email = StringField(u'邮箱', validators=[DataRequired(), Length(6, 64, message=u'邮件长度要在6和64之间'),
                            Email(message=u'邮件格式不正确!')])
        password = PasswordField(u'密码', validators=[DataRequired()])
        remember_me = BooleanField(label=u'记住我', default=False)
        submit = SubmitField(u'登 录')
    

    电子邮件字段用到了WTForms提供的Length()Email()验证函数。PasswordeField类表示属性为type=”password”<input>元素。BooleanField类表示复选框。

    登录页面使用的模板保存在app/templates/auth/login.html文件中。

    {% extends 'main/common/base.html' %}
    {% block title %}
        {{ super() }}
        登录
    {% endblock %}
    {% block content %}
        <div class="log-reg">
            {% include 'common/alert-wrong.html' %}<!-- 错误信息flash提示 end -->
            <!-- 错误信息form提示 -->
            {% for field_name, field_errors in loginForm.errors|dictsort if field_errors %}
                {% for error in field_errors %}
                    <div class="error">
                        <div class="alert alert-danger alert-dismissible" role="alert">
                            <button type="button" class="close" data-dismiss="alert" aria-label="Close"><span aria-hidden="true">&times;</span></button>
                            <strong>{{ loginForm[field_name].label }}错误:</strong> {{ error }}
                        </div>
                    </div>
                {% endfor %}
            {% endfor %}
            <!-- 错误信息form提示 end -->
            <form method="post" role="form">
                {{ loginForm.hidden_tag() }}
                <div class="input-group input-group-lg">
                    <span class="input-group-addon"><i class="glyphicon glyphicon-envelope"></i> </span>
                    {{ loginForm.email(class="form-control", placeholder="邮箱",required="", autofocus="") }}
                </div>
                <div class="input-group input-group-lg">
                    <span class="input-group-addon"><i class="glyphicon glyphicon-lock"></i> </span>
                    {{ loginForm.password(class="form-control", placeholder="密 码", required="") }}
                </div>
                <div class="well-lg">
                    <div class="row pull-left">
                        {{ loginForm.remember_me() }} {{ loginForm.remember_me.label }}
                    </div>
                </div>
                {{ loginForm.submit(class="btn btn-lg btn-primary pull-right") }}
                <input class="btn btn-lg btn-primary pull-right" type="reset" value="重 置">
            </form>
        </div>
    {% endblock %}
    

    登入用户

    app/auth/views.py, 视图函数login()的实现

    from flask import render_template, request, redirect, url_for, flash
    from flask.ext.login import login_user
    from ..models import User
    from .forms import LoginForm
    from .import main
    @auth.route('/login', methods=['GET', 'POST'])
    def login():
        login_form = LoginForm(prefix='login')
        if login_form.validate_on_submit():
            user = User.query.filter_by(email=login_form.email.data.strip()).first()       
            if user is not None and user.verify_password(login_form.password.data.strip()):
                login_user(user=user, remember=login_form.remember_me.data)
                return redirect(request.args.get('next') or url_for('main.index'))
            elif user is None:
                flash(u'邮箱未注册!')
            elif not user.verify_password(login_form.password.data.strip()):
                flash(u'密码不正确!')
        return render_template('main/login.html', loginForm=login_form)
    

    登出用户

    app/main/views.py 退出路由

    from flask.ext.login import logout_user, login_required
    @auth .route('/logout')
    @login_required
    def logout():
        logout_user()
        return redirect(url_for('main.index'))
    

    这个函数中调用Flask-Login中的logout_user()函数,删除并重设用户会话。随后重定向到首页,这个操作就完成。其中,这个函数用到了保护路由,Flash-Login提供的一个login_required修饰器,如果未认证的用户访问这个路由,Flash-Login会拦截请求,把用户发往登录页面。

    测试登录

    为了验证登录功能是否成功,可以把自己的成果展示一下了。
    http://localhost:5000/auth/login, 打开这个URL,溜一溜成果。

    Login01Login01

    VERY GOOD!!!页面显示成功拉。

    那么输入邮箱和密码试试,到底该用什么邮箱和密码呢。对了,我们现在还没有创建用户注册功能。只有在数据库中直接创建新用户了。shell的伟大就显示出来了。

    (flask) $ python manage.py shell
     >>>u = User(email=u'eastossifrage@gmail.com', username=u'东方鹗', password=u'123456')
    >>> db.session.add(u)
    >>> db.session.commit()
    

    什么情况,怎么这么多错误,连新用户都注册不了。先不要着急,我们冷静下来想一想,好像我们仅仅创建了数据库模型(app/models.py),数据库还没有创建,好了,想到问题所在,那么我们就把相应的工具拿出来——Flask—Migrate。

    使用init子命令来创建迁移仓库:

    (flask)$ python manage.py db init
    

    创建迁移脚本:

    (flask)$ python manage.py db migrate -m “initial migration”
    

    更新数据库:

    (flask)$ python manage.py db upgrade
    

    按照上面的提示操作之后,再利用shell执行注册用户操作。新创建的用户就可以登录了。登录之后显示的首页如下图。

    Login02Login02

    优化主页

    为了能够体现出用户的状态(登录与否)。我们现在需要优化主页。如果用户已登录,则在导航条中显示用户名和登出链接,如果未登录则显示登录和注册链接。
    app/templates/common/logined.html

    <ul class="nav navbar-nav navbar-right">
        {% if current_user.is_authenticated() %}
            <li><p class="navbar-text"> <a href="{{ url_for('auth.index') }}">{{ current_user.username }}</a> | <a href="{{ url_for('main.logout') }}">注销</a></p></li>
        {% else %}
            <li><p class="navbar-text"> <a href="{{ url_for('auth.login') }}">登录</a> | <a href="{{ url_for('auth.register') }}">注册</a></p></li>
        {% endif %}
    </ul>
    

    app/templates/main/common/header.html

    <div class="navbar-wrapper">
        <div class="container">
            <div class="navbar navbar-inverse navbar-static-top" role="navigation">
                <div class="container">
                    <div class="navbar-header">
                        <button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".navbar-collapse">
                            <span class="sr-only">Toggle navigation</span>
                            <span class="icon-bar"></span>
                            <span class="icon-bar"></span>
                            <span class="icon-bar"></span>
                        </button>
                        {% include 'common/brand.html' %}
                    </div>
                        <div class="navbar-collapse collapse">                    
                        {% include 'common/logined.html' %}
                        {% include 'common/search.html' %}
                    </div>
                </div>
            </div>
        </div>
    </div>
    

    注册新用户

    处理用户注册的过程没有什么难以理解。提交注册表单,通过验证后,系统就使用填写的信息在数据库中添加一个新用户。

    app/templates/main/register.html 注册新用户视图页面

    {% extends 'main/common/base.html' %}
    {% block title %}
        {{ super() }}
        注册
    {% endblock %}
    {% block content %}
        <div class="log-reg">
            {% for field_name, field_errors in registerForm.errors|dictsort if field_errors %}
            {% for error in field_errors %}
                <div class="alert alert-danger alert-dismissible" role="alert">
                    <button type="button" class="close" data-dismiss="alert" aria-label="Close"><span aria-hidden="true">&times;</span></button>
                    <strong>{{ registerForm[field_name].label }}错误:</strong> {{ error }}
                </div>
            {% endfor %}
        {% endfor %}
            <form method="post" role="form">
                <div class="input-group input-group-lg">
                    <span class="input-group-addon"><i class="glyphicon glyphicon-envelope"></i> </span>
                    {{ registerForm.email(class="form-control", placeholder="邮 箱", required="", autofocus="", title="邮箱正确格式:xxx@xxx.xxx") }}
                </div>
                <div class="input-group input-group-lg">
                    <span class="input-group-addon"><i class="glyphicon glyphicon-user"></i> </span>
                    {{ registerForm.username(class="form-control", placeholder="用户名", required="") }}
                </div>
                <div class="input-group input-group-lg">
                    <span class="input-group-addon" ><i class="glyphicon glyphicon-lock"></i> </span>
                    {{ registerForm.password(class="form-control", placeholder="密 码", required="") }}
                </div>
                <div class="input-group input-group-lg">
                    <span class="input-group-addon"><i class="glyphicon glyphicon-lock"></i> </span>
                    {{ registerForm.password2(class="form-control", placeholder="重新输入密码", required="") }}
                </div>
                {{ registerForm.submit(class="btn btn-lg btn-primary pull-right") }}
                <input class="btn btn-lg btn-primary pull-right" type="reset" value="重 置">
            </form>
        </div>
    {% endblock %}
    

    app/auth/views.py 注册新用户路由

    @auth.route('/register', methods=['GET', 'POST'])
    def register():
        register_form = RegisterForm(prefix='register')
        if register_form.validate_on_submit():
            user = User(email=register_form.email.data.strip(),
                        username=register_form.username.data.strip(),
                        password=register_form.password.data.strip())
            db.session.add(user)
            db.session.commit()        
            token = user.generate_confirmation_token()
            send_email(to=user.email, subject=u'请求确认你的账户', template='main/email/confirm', user=user, token=token)
            flash(message=u'一封确认邮件已发至您的邮箱')
            login_user(user=user)
            return redirect(url_for('main.confirming'))
        return render_template('main/register.html', registerForm=register_form)
    

    通过以上程序,你就可以实现新用户的注册功能了。但是以上程序并不能正确运行,因为示例5-12中有一部分是理由邮箱确认账户的功能。请继续学习下面的内容。

    确认账户

    为了验证电子邮件地址,用户注册后,程序会立即发送一封确认邮件。新账户先被标记成待确认状态,用户按照邮件中的说面操作后,曾你证明自己可以被联系上。账户确认过程中,往往会要求用户点击一个包含确认令牌的特殊URL链接。

    使用itsdangerous生成确认令牌

    itsdangerous提供了多种生成令牌的方法。其中,TimeJSONWebSignatureSerializer类生成具有过期时间的JSON Web签名(JSON Web Signatures, JWS)。这个类的构造函数接受的参数是一个密钥,在Flask程序中可使用SECRET_KEY设置。

    dumps()方法为指定的数据生曾一个加密签名,然后再对数据和签名进行序列化,生成令牌字符串。expires_in参数设置令牌的过期时间,单位为秒。

    为了解码令牌,序列化对象提供了loads()方法,其唯一的参数是令牌字符串。这个方法会检验签名和过期时间,如果通过,返回原始数据。如果提供给loads()方法的令牌不正确或过期了,则抛出异常。

    我们将这种生成和检验令牌的功能可添加到User模型中。

    app/models.py 确认用户账户

    from . import db
    from flask.ext.login import UserMixin, AnonymousUserMixin
    from flask import current_app
    class User(UserMixin, db.Model):
        # ...
        confirmed = db.Column(db.Boolean, default=False)
        
        def generate_confirmation_token(self, expiration=3600):
            s = Serializer(current_app.config['SECRET_KEY'], expires_in=expiration)
            return s.dumps({'confirm': self.id})
        def confirm(self, token):
            s = Serializer(current_app.config['SECRET_KEY'])
            try:
                data = s.loads(token)
            except:
                return False
            if data.get('confirm') != self.id:
                return False
            self.confirmed = True
            db.session.add(self)
            return True
        def __repr__(self):
            return '<User %r>' % self.username
    

    generate_confirmation_token()方法生成一个令牌,有效期默认为一小时。confirm()方法检验令牌,如果检验通过,则把新添加的confirmed属性设置为True。除了检验令牌,confirm()方法还检查令牌中的id是否和存储在current_user中的已登录用户匹配。如此一来,及时恶意用户知道如何生成签名令牌,也无法确认别人的账户。

    发送确认邮件

    5-12 app/auth/views.py中的代码所示,/register路由先把新用户添加到数据库中(** 注意,即便通过config.py配置,程序已经可以在请求末尾自动提交数据库变化,这里也需要添加db.session.commit()调用。问题在于,提交数据库之后才能赋予新用户id值,而确认令牌需要用到id,所以不能延后提交 **),在重定向之前,发送确认邮件。

    电子邮件模板保存在templates/email文件夹中,以便和HTML模板区分开来。一个电子邮件需要两个模板,分别用于渲染纯文本正负和富文本正负。

    app/templates/auth/email/confirm.txt, 确认邮件的纯文本正文

    尊敬的 {{ user.username }}, 您好!
        欢迎来到藕丝空间!
        请点击下面的链接来确认您的账户:
        {{ url_for('auth.confirm', token=token, _external=True) }}
                                                    藕丝团队敬上
        注意:请不要回复该邮件!
    

    app/templates/auth/email/confirm.html, 确认邮件的富文本正文

    <p>尊敬的 <strong>{{ user.username }}</strong>, 您好!</p>
    <p>欢迎来到藕丝空间!</p>
    <p>请点击下面的链接来确认您的账户:</p>
    <p><a href="{{ url_for('auth.confirm', token=token, _external=True) }}">{{ url_for('auth.confirm', token=token, _external=True) }}</a></p>
    <p class="pull-right">                                            藕丝团队敬上</p>
    <p>注意:请不要回复该邮件!
    

    默认情况下,url_for()生成相对URL,例如url_for('auth.confirm', token='abc')返回的字符串是/auth/confirm/abc。这显然不是能够在电子邮件中发送的正确URL。相对URL在网页的上下文中可以正常使用,因为通过添加当前页面的主机名和端口号,浏览器会将其转换成绝对URL。但通过电子邮件发送的URL时,并没有这种上下文。添加到url_for()函数中的_external=True参数要求程序生成完整的URL,其中包含协议(http://或https://),主机名和端口。

    app/auth/views.py, 确认用户的账户

    from flask.ext.login import current_user
    @auth.route('/confirm/<token>')
    @login_required
    def confirm(token):
        if current_user.confirmed:
            return redirect(url_for('main.index'))
        if current_user.confirm(token):
            flash(u'您已经成功的对您的账户进行了邮件确认。非常感谢!')
        else:
            flash(u'本链接已经失效或者过期。')
            return redirect(url_for('auth.unconfirmed'))
        return redirect(url_for('auth.confirmed'))
    

    Flask-Login提供的login_required修饰器会保护这个路由,因此,用户点击确认邮件中的链接后,要先登录,然后才能执行这个视图函数。

    这个函数先检查已登录的用户是否确认国,如果确认国,则重定向到首页,因为很显然此时不用做什么操作。这样处理可以避免用户不小心多次点击确认令牌带来的额外工作。

    由于令牌确认完全在User模型中完成,所以视图函数只需要调用confirm()方法即可,然后再根据确认结果显示不同的flash消息。确认成功后,User模型中confirmed属性的值会被修改并添加到会话中,请求处理完后,这两个操作被提交到数据库。

    在确定用户账户之前,我们可以自由决定用户该进行那些操作。现在我们需要显示一个页面,要求用户在获取权限之前先确认账户。这一步可以使用Flask提供的before_request钩子完成。对于蓝本来说,before_request钩子只能应用到属于蓝本的请求上。若想在蓝本中使用针对全局请求的钩子,必须使用before_app_request修饰器。

    app/auth/views.py, 在before_aap_request处理程序中过滤未确认账户

    @auth.before_app_request
    def before_request():
        if current_user.is_authenticated():
            # current_user.ping()
            if not current_user.confirmed \
                    and request.endpoint[:5] != 'auth.' \
                    and request.endpoint != 'static':
                return redirect(url_for('auth.unconfirmed'))
    
    @auth.route('/unconfirmed')
    def unconfirmed():
        if current_user.is_anonymous() or current_user.confirmed:
            return redirect(url_for('main.index'))
        return render_template('auth/email/unconfirmed.html')
    

    同时满足一下3个条件,before_app_request处理程序会拦截请求。

    • 用户已经登录(current_user.is_authenticated()必须返回True)。
    • 用户账户还未确认。
    • 请求的端点(使用request.endpoint获取)不再认证蓝本中。访问认证路由要获取权限,因为这些路由的作用是让用户确认账户或执行其他账户管理操作。

    如果请求满足以上3个条件,则会被重定向到/auth/unconfirmed路由,显示一个确认账户相关信息页面。

    为了防止之前的邮件丢失。我们需要重新发送确认邮件的功能。

    5-18 app/auth/views.py, 重新发送账户确认邮件

    @auth.route('/confirm')
    @login_required
    def resend_confirmation():
        token = current_user.generate_confirmation_token()
        send_email(to=current_user.email, subject=u'请求确认你的账户',
                   template='auth/email/confirm', user=current_user, token=token)
        flash(message=u'一封注册确认邮件已发至您的邮箱')
        return redirect(url_for('auth.confirming'))
    

    这个路由为current_user(即已登录的用户,也是目标用户)重做了一遍注册路由中的操作。这个路由也用login_required保护,确保访问时程序知道请求再次发送邮件的哪个用户。

    管理账户

    拥有程序账户的用户有时可能需要修改账户信息。

    修改密码

    安全意识强的用户可能希望定期修改密码。这是一个很容易实现的功能,只要用户处于登录状态,就可以放心显示一个表单,要求用户输入旧密码和替换的新密码。

    app/auth/forms.py,修改密码表单

    class ChangePasswordForm(Form):
        old_password = PasswordField(u'旧密码', validators=[DataRequired()])
        password = PasswordField(u'密码', validators=[DataRequired(), EqualTo(u'password2', message=u'密码必须一致!')])
        password2 = PasswordField(u'重输密码', validators=[DataRequired()])
        submit = SubmitField(u'更新密码')
    

    app/templates/auth/config/changer_password.html, 修改密码页面

    {% extends 'auth/common/base.html' %}
    {% block title %}
        {{ super() }}
        修改密码
    {% endblock %}
    {% block content %}
        <h1 class="page-header">修改密码</h1>
        <div class="center-auth">
            {% include 'common/alert.html' %}<!-- flash提示 end -->
            <!-- 错误信息changePasswordForm提示 -->
            {% for field_name, field_errors in changePasswordForm.errors|dictsort if field_errors %}
                {% for error in field_errors %}
                    <div class="error">
                        <div class="alert alert-danger alert-dismissible" role="alert" style="margin-top: 20px">
                            <button type="button" class="close" data-dismiss="alert" aria-label="Close"><span aria-hidden="true">&times;</span></button>
                            <strong>{{ changePasswordForm[field_name].label }}错误:</strong> {{ error }}
                        </div>
                    </div>
                {% endfor %}
            {% endfor %}
            <!-- 错误信息changePasswordForm提示 end -->
            <form method="post" role="form">
                {{ changePasswordForm.hidden_tag() }}
                    <div class="input-group">
                        <span class="input-group-addon"><i class="glyphicon glyphicon-lock"></i> </span>
                        {{ changePasswordForm.old_password(class="form-control", maxlength="64", placeholder="旧密码", required="") }}
                    </div>
                    <div class="input-group">
                        <span class="input-group-addon"><i class="glyphicon glyphicon-lock"></i> </span>
                        {{ changePasswordForm.password(class="form-control", maxlength="64", placeholder="密码", required="") }}
                    </div>
                    <div class="input-group">
                        <span class="input-group-addon"><i class="glyphicon glyphicon-lock"></i> </span>
                        {{ changePasswordForm.password2(class="form-control", maxlength="64", placeholder="重新输入密码", required="") }}
                    </div>
                    {{ changePasswordForm.submit(class="btn btn-primary pull-right") }}
            </form>
        </div>
    {% endblock %}
    

    app/auth/views.py, 修改密码路由

    @auth.route('/change_password', methods=['GET', 'POST'])
    @login_required
    def change_password():
        change_password_form = ChangePasswordForm(prefix='change_password')
        if change_password_form.validate_on_submit():
            if current_user.verify_password(change_password_form.old_password.data.strip()):
                current_user.password = change_password_form.password.data.strip()
                db.session.add(current_user)
                flash({'success': u'您的账户密码已修改成功!'})
            else:
                flash({'error': u'无效的旧密码!'})
        return render_template('auth/config/change_password.html', changePasswordForm=change_password_form)
    

    重设密码

    为了避免用户忘记密码无法登入的情况,程序可以提供重设密码功能。安全起见,有必要使用类似于确认账户时用到的令牌。用户请求重设密码后,程序会向用户注册时提供的电子邮件地址发送一封包含重设令牌的邮件。用户点击邮件中的链接,令牌验证后,会显示一个用户输入密码的表单。

    app/auth/forms.py, 重置密码表单

    class PasswordResetRequestForm(Form):
        email = StringField(u'邮箱', validators=[DataRequired(), Length(6, 64, message=u'邮件长度要在6和64之间'),
                            Email(message=u'邮件格式不正确!')])
        submit = SubmitField(u'发送')
    
    class PasswordResetForm(Form):
        email = StringField(u'邮箱', validators=[DataRequired(), Length(6, 64, message=u'邮件长度要在6和64之间'),
                            Email(message=u'邮件格式不正确!')])
        password = PasswordField(u'密码', validators=[DataRequired(), EqualTo(u'password2', message=u'密码必须一致!')])
        password2 = PasswordField(u'重输密码', validators=[DataRequired()])
        submit = SubmitField(u'确认')
        def validate_email(self, field):
            if User.query.filter_by(email=field.data).first() is None:
                raise ValidationError(u'邮箱未注册!')
    

    app/templates/auth/password/password_reset_.html, 忘记密码页面(输入注册邮箱,程序会往注册邮箱里发送一封包含重设令牌的邮件)

    {% extends 'main/common/base.html' %}
    {% block title %}
        {{ super() }}
        重置密码
    {% endblock %}
    {% block content %}
        <div class="log-reg">
            {% include 'common/alert.html' %}<!-- flash提示 end -->
            <!-- 错误信息form提示 -->
            {% for field_name, field_errors in passwordResetRequestForm.errors|dictsort if field_errors %}
                {% for error in field_errors %}
                    <div class="error">
                        <div class="alert alert-danger alert-dismissible" role="alert">
                            <button type="button" class="close" data-dismiss="alert" aria-label="Close"><span aria-hidden="true">&times;</span></button>
                            <strong>{{ passwordResetRequestForm[field_name].label }}错误:</strong> {{ error }}
                        </div>
                    </div>
                {% endfor %}
            {% endfor %}
            <!-- 错误信息form提示 end -->
            <!-- Modal -->
            <form method="post" role="form">
                {{ passwordResetRequestForm.hidden_tag() }}
                <label>填写您所注册的邮箱</label>
                <div class="input-group input-group-lg">
                    <span class="input-group-addon"><i class="glyphicon glyphicon-envelope"></i> </span>
                    {{ passwordResetRequestForm.email(class="form-control", placeholder="邮箱", required="", autofocus="") }}
                </div>
                {{ passwordResetRequestForm.submit(class="btn btn-lg btn-primary pull-right") }}
                <input type="reset" class="btn btn-lg btn-default pull-right">
            </form>
        </div>
    {% endblock %}
    

    app/templates/auth/password/password_reset.html, 重置密码页面

    {% extends 'main/common/base.html' %}
    {% block title %}
        {{ super() }}
        重置密码
    {% endblock %}
    {% block content %}
        <div class="log-reg">
            {% include 'common/alert.html' %}<!-- flash提示 end -->
            <!-- 错误信息form提示 -->
            {% for field_name, field_errors in passwordResetForm.errors|dictsort if field_errors %}
                {% for error in field_errors %}
                    <div class="error">
                        <div class="alert alert-danger alert-dismissible" role="alert">
                            <button type="button" class="close" data-dismiss="alert" aria-label="Close"><span aria-hidden="true">&times;</span></button>
                            <strong>{{ passwordResetForm[field_name].label }}错误:</strong> {{ error }}
                        </div>
                    </div>
                {% endfor %}
            {% endfor %}
            <!-- 错误信息form提示 end -->
            <!-- Modal -->
            <form method="post" role="form">
                {{ passwordResetForm.hidden_tag() }}
                    <div class="input-group">
                        <span class="input-group-addon"><i class="glyphicon glyphicon-envelope"></i> </span>
                        {{ passwordResetForm.email(class="form-control", placeholder="邮箱", maxlength="64", required="", autofocus="") }}
                    </div>
                    <div class="input-group">
                        <span class="input-group-addon"><i class="glyphicon glyphicon-lock"></i> </span>
                        {{ passwordResetForm.password(class="form-control", placeholder="密码", maxlength="64", required="") }}
                    </div>
                    <div class="input-group">
                        <span class="input-group-addon"><i class="glyphicon glyphicon-lock"></i> </span>
                        {{ passwordResetForm.password2(class="form-control", placeholder="重新输入密码", maxlength="64", required="") }}
                    </div>
                    {{ passwordResetForm.submit(class="btn btn-lg btn-primary pull-right") }}
                    <input type="reset" class="btn btn-lg btn-default pull-right">
            </form>
        </div>
    {% endblock %}
    

    包含带有令牌的重设密码的提示页面以及重设密码成功的页面,请发挥想象力,自己完成。

    app/auth/views.py, 重置密码路由

    @auth.route('/reset', methods=['GET', 'POST'])
    def password_reset_request():
        ''' 往注册邮箱里发送一封包含令牌的重设密码邮件 '''
        if not current_user.is_anonymous():
            return redirect(url_for('main.index'))
        password_reset_request_form = PasswordResetRequestForm()
        if password_reset_request_form.validate_on_submit():
            user = User.query.filter_by(email=password_reset_request_form.email.data.strip()).first()
            if user:
                token = user.generate_reset_token()
                send_email(to=user.email, subject=u'重置密码',
                           user=user, token=token, template='auth/password/reset_password',
                           next=request.args.get('next'))
                flash(user.username)
                flash(u'一封重置密码的确认邮件已发至您的邮箱')
                flash(user.email)
                return redirect(url_for('auth.password_reset_confirming'))
            else:
                flash({'error':u'邮箱未注册!'})
        return render_template('auth/password/password_reset_.html', passwordResetRequestForm=password_reset_request_form)
    
    @auth.route('/reset/<token>', methods=['GET', 'POST'])
    def password_reset(token):
        ''' 重设密码路由 '''
        if not current_user.is_anonymous():
            return redirect(url_for('main.index'))
        password_reset_form = PasswordResetForm()
        if password_reset_form.validate_on_submit():
            user = User.query.filter_by(email=password_reset_form.email.data.strip()).first()
            if user is None:
                return redirect(url_for('main.index'))
            if user.reset_password(token, password_reset_form.password.data):
                flash(user.username)
                flash(u'您的账户密码已重置,请使用新密码登录!')
                return redirect(url_for('auth.password_reset_confirmed'))
            else:
                return redirect(url_for('main.index'))
        return render_template('auth/password/password_reset.html', passwordResetForm=password_reset_form)
    
    @auth.route('/password/confirming')
    def password_reset_confirming():
        ''' 包含一份带有令牌的重设密码页面的路由 '''
        return render_template('auth/password/reset_password_confirming.html')
    
    @auth.route('/password/confirmed')
    def password_reset_confirmed():
        ''' 重设密码成功页面的路由 '''
        return render_template('auth/password/reset_password_confirmed.html')
    

    修改用户昵称

    程序可以提供修改用户昵称的功能,不过修改用户昵称的操作,必须使用密码进行权限确认。

    app/auth/forms.py, 修改用户昵称表单

    class ChangeUsernameForm(Form):
        username = StringField(u'用户名', validators=[DataRequired(), Length(1, 64, message=u'用户名长度要在1和64之间'),
                               Regexp(ur'^[\u4E00-\u9FFF]+$', flags=0, message=u'用户名必须为中文')])
        password = PasswordField('Password', validators=[DataRequired()])
        submit = SubmitField(u'更新昵称')
        def validate_username(self, field):
            if User.query.filter_by(username=field.data).first():
                raise ValidationError(u'用户名已被注册!')
    

    app/templates/auth/config/change_username.html, 修改用户昵称页面

    {% extends 'auth/common/base.html' %}
    {% block title %}
        {{ super() }}
        修改昵称
    {% endblock %}
    {% block content %}
        <h1 class="page-header">修改昵称</h1>
        <div class="center-auth">
            {% include 'common/alert.html' %}<!-- flash提示 end -->
            <!-- 错误信息changeUsernameForm提示 -->
            {% for field_name, field_errors in changeUsernameForm.errors|dictsort if field_errors %}
                {% for error in field_errors %}
                    <div class="error">
                        <div class="alert alert-danger alert-dismissible" role="alert" style="margin-top: 20px">
                            <button type="button" class="close" data-dismiss="alert" aria-label="Close"><span aria-hidden="true">&times;</span></button>
                            <strong>{{ changeUsernameForm[field_name].label }}错误:</strong> {{ error }}
                        </div>
                    </div>
                {% endfor %}
            {% endfor %}
            <!-- 错误信息changeUsernameForm提示 end -->
            <form method="post" role="form">
                {{ changeUsernameForm.hidden_tag() }}
                <div class="input-group">
                    <span class="input-group-addon"><i class="glyphicon glyphicon-lock"></i> </span>
                    {{ changeUsernameForm.password(class="form-control", maxlength="64", placeholder="当前密码", required="") }}
                </div>
                <div class="input-group">
                    <span class="input-group-addon"><i class="glyphicon glyphicon-user"></i> </span>
                    {{ changeUsernameForm.username(class="form-control", maxlength="64", value=current_user.username, required="") }}
                </div>
                {{ changeUsernameForm.submit(class="btn btn-primary pull-right") }}
            </form>
        </div>
    {% endblock %}
    

    app/auth/views.py, 重设用户昵称路由

    @auth.route('/change-username', methods=['GET', 'POST'])
    @login_required
    def change_username():
        change_username_form = ChangeUsernameForm(prefix='change_username')
        if change_username_form.validate_on_submit():
            if current_user.verify_password(change_username_form.password.data):
                current_user.username = change_username_form.username.data.strip()
                db.session.add(current_user)
                flash({'success': u'昵称更新成功!'})
            else:
                flash({'error': u'密码错误!'})
        return render_template('auth/config/change_username.html', changeUsernameForm=change_username_form)
    

    修改电子邮件地址

    程序可以提供修改电子邮件的功能,不过接受新昵称之前,必须使用确认邮件进行验证。
    由于设计思路也是基于令牌的邮件验证,本课件将不在展示MVC各阶段的代码,如有需要,请下载源码进行阅读。

    <code class="btn btn-primary pull-right">ousi373_login.rar</code>

    相关文章

      网友评论

        本文标题:第九章 用户认证

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