第九章 用戶認(rèn)證

大多數(shù)程序都需要進(jìn)行用戶跟蹤。用戶鏈接程序時需要進(jìn)行身份認(rèn)證肌稻,通過這一過程,讓程序知道自己的身份伤塌。程序知道用戶是誰后灯萍,就能提供有針對性的個性化體驗。最常用的認(rèn)證方法要求用戶提供一個身份證明(用戶的電子郵件每聪、電話或用戶名)和一個密碼旦棉。

Flask的認(rèn)證擴(kuò)展

  • Flask-Login:管理已登錄用戶的用戶會話(session)。
  • Werkzeug:計算密碼散列值并進(jìn)行核對药薯。
  • itsdangerous:生成并核對加密安全令牌绑洛。

使用pip安裝Flask-Login:

(flask)$ pip install flask-login

密碼安全性

若要保證數(shù)據(jù)庫中用戶密碼的安全,關(guān)鍵在于不能存儲密碼本身童本,而要存儲密碼的散列值(非明文)真屯。計算密碼散列值的函數(shù)接手密碼作為輸入,使用一種或多種加密算法轉(zhuǎn)換密碼穷娱,最終得到一個和原始密碼沒有關(guān)系的字符序列绑蔫。核對密碼時,密碼散列值可以替代原始密碼泵额,因為計算散列值的函數(shù)是可以復(fù)現(xiàn)的:只要輸入一樣配深,結(jié)果就一樣。

使用Werkzeug實現(xiàn)密碼散列

Werkzeug中的security模塊能夠很方便地實現(xiàn)密碼散列值的計算嫁盲。這一功能的實現(xiàn)只需要兩個函數(shù)篓叶,分別用在注冊用戶和驗證用戶階段。

  • generate_password_hash(password, method=pbkdf2:sha1, salt_length=8):這個函數(shù)將原始密碼作為輸入羞秤,以字符串形式輸出密碼的散列值缸托,輸出的值可保存在用戶數(shù)據(jù)庫中,method和salt_length的默認(rèn)值就能滿足大多數(shù)需求瘾蛋。
  • check_password_hash(hash, password):這個函數(shù)的參數(shù)是從數(shù)據(jù)庫中取回的密碼散列值和用戶輸入的密碼俐镐。返回值為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

計算密碼散列值的函數(shù)通過名為password的只寫屬性實現(xiàn)。設(shè)定這個函數(shù)的值時匹摇,賦值方法會調(diào)用Werkzeug提供的generate_password_hash()函數(shù),并把得到的結(jié)果賦值給password_hash字段甲葬。如果試圖讀取password屬性的值廊勃,則會返回錯誤,原因很明顯经窖,因為生曾散列值后就無法還原成原來的密碼了坡垫。

verify_password方法接受一個參數(shù)(即密碼),將其傳給Werkzeug提供的check_password_hash()函數(shù)画侣,和存儲在User模型中的密碼散列值進(jìn)行比對冰悠。如果這個方法返回True,就表明密碼是正確的配乱。

準(zhǔn)備用于登錄的用戶模型

要想使用Flask-Login擴(kuò)展溉卓,程序的User模型必須實現(xiàn)幾個方法。需要實現(xiàn)的方法如下表:

方法 說明
is_authenticated() 如果用戶已經(jīng)登錄搬泥,必須返回True桑寨,否則返回False
is_active() 如果允許用戶登錄,必須返回True忿檩,否則返回False尉尾。如果要禁用賬戶,可以返回False
is_anonymous() 對普通用戶必須返回False
get_id() 必須返回用戶的唯一標(biāo)示符燥透,使用Unicode編碼字符串

這四個方法可以在模型類中作為方法直接實現(xiàn)沙咏,不過還有一種更簡單的替代方法。Flask-Login提供了一個UserMixin類班套,其中包含這些方法的默認(rèn)實現(xiàn)肢藐,且能滿足大多數(shù)需求。在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):
    """ 使用工廠函數(shù)初始化程序?qū)嵗?""
    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)
    # 注冊藍(lán)本 main
    from .main import main as main_blueprint
    app.register_blueprint(main_blueprint)
    # 注冊藍(lán)本 auth
    from .auth import auth as auth_blueprint
    app.register_blueprint(auth_blueprint, url_prefix='/auth')
    return app

LoginManager對象的session_protection屬性可以設(shè)為None、'basic'或'strong'杉女,以提供不同的安全等級防止用戶會話遭篡改瞻讽。設(shè)置為'strong'時,F(xiàn)lask-Login會記錄客戶端IP地址和瀏覽器的用戶代理信息熏挎,如果發(fā)現(xiàn)移動就登出用戶速勇。login_view屬性設(shè)置登錄頁面的端點,前面需要加上藍(lán)本的名字坎拐。

最后烦磁,F(xiàn)lask-Login要求程序?qū)崿F(xiàn)一個回調(diào)函數(shù)养匈,使用指定的標(biāo)示符加載用戶
修改app/models.py,加載用戶的回調(diào)函數(shù)

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

加載用戶的回調(diào)函數(shù)接受以Unicode字符串形式表示的用戶標(biāo)示符都伪。如果能找到用戶呕乎,這個函數(shù)必須返回用戶對象;否在應(yīng)該返回None陨晶。

添加登錄表單

呈現(xiàn)給用戶的登錄表單中包含一個用戶輸入電子郵件地址的文本字段猬仁、一個密碼字段、一個“記住我”復(fù)選框和提交按鈕先誉。這個表單使用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()驗證函數(shù)褐耳。PasswordeField類表示屬性為type=”password”<input>元素诈闺。BooleanField類表示復(fù)選框。

登錄頁面使用的模板保存在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, 視圖函數(shù)login()的實現(xiàn)

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'))

這個函數(shù)中調(diào)用Flask-Login中的logout_user()函數(shù)杨帽,刪除并重設(shè)用戶會話漓穿。隨后重定向到首頁,這個操作就完成注盈。其中晃危,這個函數(shù)用到了保護(hù)路由,F(xiàn)lash-Login提供的一個login_required修飾器老客,如果未認(rèn)證的用戶訪問這個路由僚饭,F(xiàn)lash-Login會攔截請求,把用戶發(fā)往登錄頁面胧砰。

測試登錄

為了驗證登錄功能是否成功鳍鸵,可以把自己的成果展示一下了。
http://localhost:5000/auth/login尉间, 打開這個URL偿乖,溜一溜成果。

Login01
Login01

VERY GOODU艹啊L靶健!頁面顯示成功拉眠副。

那么輸入郵箱和密碼試試画切,到底該用什么郵箱和密碼呢。對了囱怕,我們現(xiàn)在還沒有創(chuàng)建用戶注冊功能霍弹。只有在數(shù)據(jù)庫中直接創(chuàng)建新用戶了毫别。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()

什么情況典格,怎么這么多錯誤岛宦,連新用戶都注冊不了。先不要著急耍缴,我們冷靜下來想一想恋博,好像我們僅僅創(chuàng)建了數(shù)據(jù)庫模型(app/models.py),數(shù)據(jù)庫還沒有創(chuàng)建私恬,好了,想到問題所在炼吴,那么我們就把相應(yīng)的工具拿出來——Flask—Migrate本鸣。

使用init子命令來創(chuàng)建遷移倉庫:

(flask)$ python manage.py db init

創(chuàng)建遷移腳本:

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

更新數(shù)據(jù)庫:

(flask)$ python manage.py db upgrade

按照上面的提示操作之后,再利用shell執(zhí)行注冊用戶操作硅蹦。新創(chuàng)建的用戶就可以登錄了荣德。登錄之后顯示的首頁如下圖。

Login02
Login02

優(yōu)化主頁

為了能夠體現(xiàn)出用戶的狀態(tài)(登錄與否)童芹。我們現(xiàn)在需要優(yōu)化主頁涮瞻。如果用戶已登錄,則在導(dǎo)航條中顯示用戶名和登出鏈接假褪,如果未登錄則顯示登錄和注冊鏈接署咽。
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>

注冊新用戶

處理用戶注冊的過程沒有什么難以理解。提交注冊表單生音,通過驗證后宁否,系統(tǒng)就使用填寫的信息在數(shù)據(jù)庫中添加一個新用戶。

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'請求確認(rèn)你的賬戶', template='main/email/confirm', user=user, token=token)
        flash(message=u'一封確認(rèn)郵件已發(fā)至您的郵箱')
        login_user(user=user)
        return redirect(url_for('main.confirming'))
    return render_template('main/register.html', registerForm=register_form)

通過以上程序缀遍,你就可以實現(xiàn)新用戶的注冊功能了慕匠。但是以上程序并不能正確運行,因為示例5-12中有一部分是理由郵箱確認(rèn)賬戶的功能域醇。請繼續(xù)學(xué)習(xí)下面的內(nèi)容台谊。

確認(rèn)賬戶

為了驗證電子郵件地址,用戶注冊后譬挚,程序會立即發(fā)送一封確認(rèn)郵件锅铅。新賬戶先被標(biāo)記成待確認(rèn)狀態(tài),用戶按照郵件中的說面操作后殴瘦,曾你證明自己可以被聯(lián)系上狠角。賬戶確認(rèn)過程中,往往會要求用戶點擊一個包含確認(rèn)令牌的特殊URL鏈接蚪腋。

使用itsdangerous生成確認(rèn)令牌

itsdangerous提供了多種生成令牌的方法丰歌。其中姨蟋,TimeJSONWebSignatureSerializer類生成具有過期時間的JSON Web簽名(JSON Web Signatures, JWS)。這個類的構(gòu)造函數(shù)接受的參數(shù)是一個密鑰立帖,在Flask程序中可使用SECRET_KEY設(shè)置眼溶。

dumps()方法為指定的數(shù)據(jù)生曾一個加密簽名,然后再對數(shù)據(jù)和簽名進(jìn)行序列化晓勇,生成令牌字符串堂飞。expires_in參數(shù)設(shè)置令牌的過期時間,單位為秒绑咱。

為了解碼令牌绰筛,序列化對象提供了loads()方法,其唯一的參數(shù)是令牌字符串描融。這個方法會檢驗簽名和過期時間铝噩,如果通過,返回原始數(shù)據(jù)窿克。如果提供給loads()方法的令牌不正確或過期了骏庸,則拋出異常。

我們將這種生成和檢驗令牌的功能可添加到User模型中年叮。

app/models.py 確認(rèn)用戶賬戶

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()方法生成一個令牌具被,有效期默認(rèn)為一小時。confirm()方法檢驗令牌只损,如果檢驗通過一姿,則把新添加的confirmed屬性設(shè)置為True。除了檢驗令牌跃惫,confirm()方法還檢查令牌中的id是否和存儲在current_user中的已登錄用戶匹配啸蜜。如此一來,及時惡意用戶知道如何生成簽名令牌辈挂,也無法確認(rèn)別人的賬戶衬横。

發(fā)送確認(rèn)郵件

5-12 app/auth/views.py中的代碼所示,/register路由先把新用戶添加到數(shù)據(jù)庫中(** 注意终蒂,即便通過config.py配置蜂林,程序已經(jīng)可以在請求末尾自動提交數(shù)據(jù)庫變化,這里也需要添加db.session.commit()調(diào)用拇泣。問題在于噪叙,提交數(shù)據(jù)庫之后才能賦予新用戶id值,而確認(rèn)令牌需要用到id霉翔,所以不能延后提交 **)睁蕾,在重定向之前,發(fā)送確認(rèn)郵件。

電子郵件模板保存在templates/email文件夾中子眶,以便和HTML模板區(qū)分開來瀑凝。一個電子郵件需要兩個模板,分別用于渲染純文本正負(fù)和富文本正負(fù)臭杰。

app/templates/auth/email/confirm.txt, 確認(rèn)郵件的純文本正文

尊敬的 {{ user.username }}, 您好粤咪!
    歡迎來到藕絲空間!
    請點擊下面的鏈接來確認(rèn)您的賬戶:
    {{ url_for('auth.confirm', token=token, _external=True) }}
                                                藕絲團(tuán)隊敬上
    注意:請不要回復(fù)該郵件渴杆!

app/templates/auth/email/confirm.html, 確認(rèn)郵件的富文本正文

<p>尊敬的 <strong>{{ user.username }}</strong>, 您好寥枝!</p>
<p>歡迎來到藕絲空間!</p>
<p>請點擊下面的鏈接來確認(rèn)您的賬戶:</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">                                            藕絲團(tuán)隊敬上</p>
<p>注意:請不要回復(fù)該郵件磁奖!

默認(rèn)情況下囊拜,url_for()生成相對URL,例如url_for('auth.confirm', token='abc')返回的字符串是/auth/confirm/abc比搭。這顯然不是能夠在電子郵件中發(fā)送的正確URL艾疟。相對URL在網(wǎng)頁的上下文中可以正常使用,因為通過添加當(dāng)前頁面的主機(jī)名和端口號敢辩,瀏覽器會將其轉(zhuǎn)換成絕對URL。但通過電子郵件發(fā)送的URL時弟疆,并沒有這種上下文戚长。添加到url_for()函數(shù)中的_external=True參數(shù)要求程序生成完整的URL,其中包含協(xié)議(http://或https://)怠苔,主機(jī)名和端口同廉。

app/auth/views.py, 確認(rèn)用戶的賬戶

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'您已經(jīng)成功的對您的賬戶進(jìn)行了郵件確認(rèn)。非常感謝柑司!')
    else:
        flash(u'本鏈接已經(jīng)失效或者過期迫肖。')
        return redirect(url_for('auth.unconfirmed'))
    return redirect(url_for('auth.confirmed'))

Flask-Login提供的login_required修飾器會保護(hù)這個路由,因此攒驰,用戶點擊確認(rèn)郵件中的鏈接后蟆湖,要先登錄,然后才能執(zhí)行這個視圖函數(shù)玻粪。

這個函數(shù)先檢查已登錄的用戶是否確認(rèn)國隅津,如果確認(rèn)國,則重定向到首頁劲室,因為很顯然此時不用做什么操作伦仍。這樣處理可以避免用戶不小心多次點擊確認(rèn)令牌帶來的額外工作。

由于令牌確認(rèn)完全在User模型中完成很洋,所以視圖函數(shù)只需要調(diào)用confirm()方法即可充蓝,然后再根據(jù)確認(rèn)結(jié)果顯示不同的flash消息。確認(rèn)成功后,User模型中confirmed屬性的值會被修改并添加到會話中谓苟,請求處理完后官脓,這兩個操作被提交到數(shù)據(jù)庫。

在確定用戶賬戶之前娜谊,我們可以自由決定用戶該進(jìn)行那些操作∪仿颍現(xiàn)在我們需要顯示一個頁面,要求用戶在獲取權(quán)限之前先確認(rèn)賬戶纱皆。這一步可以使用Flask提供的before_request鉤子完成湾趾。對于藍(lán)本來說,before_request鉤子只能應(yīng)用到屬于藍(lán)本的請求上派草。若想在藍(lán)本中使用針對全局請求的鉤子搀缠,必須使用before_app_request修飾器。

app/auth/views.py, 在before_aap_request處理程序中過濾未確認(rèn)賬戶

@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處理程序會攔截請求艺普。

  • 用戶已經(jīng)登錄(current_user.is_authenticated()必須返回True)。
  • 用戶賬戶還未確認(rèn)鉴竭。
  • 請求的端點(使用request.endpoint獲绕缙)不再認(rèn)證藍(lán)本中。訪問認(rèn)證路由要獲取權(quán)限搏存,因為這些路由的作用是讓用戶確認(rèn)賬戶或執(zhí)行其他賬戶管理操作瑰步。

如果請求滿足以上3個條件,則會被重定向到/auth/unconfirmed路由璧眠,顯示一個確認(rèn)賬戶相關(guān)信息頁面缩焦。

為了防止之前的郵件丟失。我們需要重新發(fā)送確認(rèn)郵件的功能责静。

5-18 app/auth/views.py, 重新發(fā)送賬戶確認(rèn)郵件

@auth.route('/confirm')
@login_required
def resend_confirmation():
    token = current_user.generate_confirmation_token()
    send_email(to=current_user.email, subject=u'請求確認(rèn)你的賬戶',
               template='auth/email/confirm', user=current_user, token=token)
    flash(message=u'一封注冊確認(rèn)郵件已發(fā)至您的郵箱')
    return redirect(url_for('auth.confirming'))

這個路由為current_user(即已登錄的用戶袁滥,也是目標(biāo)用戶)重做了一遍注冊路由中的操作。這個路由也用login_required保護(hù)灾螃,確保訪問時程序知道請求再次發(fā)送郵件的哪個用戶题翻。

管理賬戶

擁有程序賬戶的用戶有時可能需要修改賬戶信息。

修改密碼

安全意識強(qiáng)的用戶可能希望定期修改密碼腰鬼。這是一個很容易實現(xiàn)的功能藐握,只要用戶處于登錄狀態(tài),就可以放心顯示一個表單垃喊,要求用戶輸入舊密碼和替換的新密碼猾普。

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)

重設(shè)密碼

為了避免用戶忘記密碼無法登入的情況,程序可以提供重設(shè)密碼功能。安全起見溜在,有必要使用類似于確認(rèn)賬戶時用到的令牌陌知。用戶請求重設(shè)密碼后,程序會向用戶注冊時提供的電子郵件地址發(fā)送一封包含重設(shè)令牌的郵件掖肋。用戶點擊郵件中的鏈接仆葡,令牌驗證后,會顯示一個用戶輸入密碼的表單志笼。

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'發(fā)送')

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'確認(rèn)')
    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, 忘記密碼頁面(輸入注冊郵箱,程序會往注冊郵箱里發(fā)送一封包含重設(shè)令牌的郵件)

{% 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 %}

包含帶有令牌的重設(shè)密碼的提示頁面以及重設(shè)密碼成功的頁面紊浩,請發(fā)揮想象力窖铡,自己完成。

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

@auth.route('/reset', methods=['GET', 'POST'])
def password_reset_request():
    ''' 往注冊郵箱里發(fā)送一封包含令牌的重設(shè)密碼郵件 '''
    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'一封重置密碼的確認(rèn)郵件已發(fā)至您的郵箱')
            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):
    ''' 重設(shè)密碼路由 '''
    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():
    ''' 包含一份帶有令牌的重設(shè)密碼頁面的路由 '''
    return render_template('auth/password/reset_password_confirming.html')

@auth.route('/password/confirmed')
def password_reset_confirmed():
    ''' 重設(shè)密碼成功頁面的路由 '''
    return render_template('auth/password/reset_password_confirmed.html')

修改用戶昵稱

程序可以提供修改用戶昵稱的功能,不過修改用戶昵稱的操作口芍,必須使用密碼進(jìn)行權(quán)限確認(rèn)箍铲。

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="當(dāng)前密碼", 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, 重設(shè)用戶昵稱路由

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

修改電子郵件地址

程序可以提供修改電子郵件的功能聋庵,不過接受新昵稱之前膘融,必須使用確認(rèn)郵件進(jìn)行驗證。
由于設(shè)計思路也是基于令牌的郵件驗證祭玉,本課件將不在展示MVC各階段的代碼氧映,如有需要,請下載源碼進(jìn)行閱讀脱货。

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

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末岛都,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子振峻,更是在濱河造成了極大的恐慌臼疫,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,126評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件扣孟,死亡現(xiàn)場離奇詭異烫堤,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,254評論 2 382
  • 文/潘曉璐 我一進(jìn)店門鸽斟,熙熙樓的掌柜王于貴愁眉苦臉地迎上來拔创,“玉大人,你說我怎么就攤上這事富蓄∈T铮” “怎么了?”我有些...
    開封第一講書人閱讀 152,445評論 0 341
  • 文/不壞的土叔 我叫張陵,是天一觀的道長违柏。 經(jīng)常有香客問我叉钥,道長,這世上最難降的妖魔是什么比伏? 我笑而不...
    開封第一講書人閱讀 55,185評論 1 278
  • 正文 為了忘掉前任,我火速辦了婚禮疆导,結(jié)果婚禮上赁项,老公的妹妹穿的比我還像新娘。我一直安慰自己澈段,他們只是感情好悠菜,可當(dāng)我...
    茶點故事閱讀 64,178評論 5 371
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著败富,像睡著了一般悔醋。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上兽叮,一...
    開封第一講書人閱讀 48,970評論 1 284
  • 那天芬骄,我揣著相機(jī)與錄音,去河邊找鬼鹦聪。 笑死账阻,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的泽本。 我是一名探鬼主播淘太,決...
    沈念sama閱讀 38,276評論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼规丽!你這毒婦竟也來了蒲牧?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 36,927評論 0 259
  • 序言:老撾萬榮一對情侶失蹤赌莺,失蹤者是張志新(化名)和其女友劉穎冰抢,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體艘狭,經(jīng)...
    沈念sama閱讀 43,400評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡晒屎,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 35,883評論 2 323
  • 正文 我和宋清朗相戀三年喘蟆,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片鼓鲁。...
    茶點故事閱讀 37,997評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡蕴轨,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出骇吭,到底是詐尸還是另有隱情橙弱,我是刑警寧澤,帶...
    沈念sama閱讀 33,646評論 4 322
  • 正文 年R本政府宣布燥狰,位于F島的核電站棘脐,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏龙致。R本人自食惡果不足惜蛀缝,卻給世界環(huán)境...
    茶點故事閱讀 39,213評論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望目代。 院中可真熱鬧屈梁,春花似錦、人聲如沸榛了。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,204評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽霜大。三九已至构哺,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間战坤,已是汗流浹背曙强。 一陣腳步聲響...
    開封第一講書人閱讀 31,423評論 1 260
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留途茫,地道東北人碟嘴。 一個月前我還...
    沈念sama閱讀 45,423評論 2 352
  • 正文 我出身青樓,卻偏偏與公主長得像慈省,于是被迫代替她去往敵國和親臀防。 傳聞我的和親對象是個殘疾皇子眠菇,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 42,722評論 2 345

推薦閱讀更多精彩內(nèi)容

  • 22年12月更新:個人網(wǎng)站關(guān)停边败,如果仍舊對舊教程有興趣參考 Github 的markdown內(nèi)容[https://...
    tangyefei閱讀 35,160評論 22 257
  • 第二部分 Blog例子 第八章 用戶驗證 大部分程序需要追蹤用戶身份。當(dāng)用戶連接到程序捎废,通過一系列步驟使自己的身份...
    易木成華閱讀 1,281評論 0 4
  • 最近在學(xué)習(xí)flask笑窜,用到flask-login,發(fā)現(xiàn)網(wǎng)上只有0.1版本的中文文檔登疗,看了官方已經(jīng)0.4了排截,并且添加...
    ZZES_ZCDC閱讀 5,934評論 3 24
  • 4 創(chuàng)建一個社交網(wǎng)站 在上一章中嫌蚤,你學(xué)習(xí)了如何創(chuàng)建站點地圖和訂閱,并且為博客應(yīng)用構(gòu)建了一個搜索引擎断傲。在這一章中脱吱,你...
    lakerszhy閱讀 2,161評論 0 7
  • 一個現(xiàn)代 web 應(yīng)用程序需要做的最常見的事情就是處理用戶。擁有基本賬號功能的一個應(yīng)用程序需要處理很多的事情认罩,像注...
    邪惡的Sheldon閱讀 476評論 0 0