flask flask-login實現(xiàn)用戶登陸認(rèn)證的詳細(xì)過程(flask 53)

用戶認(rèn)證的原理
在了解使用Flask來實現(xiàn)用戶認(rèn)證之前蕴忆,我們首先要明白用戶認(rèn)證的原理。假設(shè)現(xiàn)在我們要自己去實現(xiàn)用戶認(rèn)證刨肃,需要做哪些事情呢?

首先箩帚,用戶要能夠輸入用戶名和密碼真友,所以需要網(wǎng)頁和表單,用以實現(xiàn)用戶輸入和提交的過程紧帕。
用戶提交了用戶名和密碼盔然,我們就需要比對用戶名,密碼是否正確是嗜,而要想比對愈案,首先我們的系統(tǒng)中就要有存儲用戶名,密碼的地方鹅搪,大多數(shù)后臺系統(tǒng)會通過數(shù)據(jù)庫來存儲站绪,但是實際上我們也可以簡單的存儲到文件當(dāng)中。(為簡明起見丽柿,本文將用戶信息存儲到j(luò)son文件當(dāng)中)
登錄之后恢准,我們需要維持用戶登錄狀態(tài),以便用戶在訪問特定網(wǎng)頁的時候來判斷用戶是否已經(jīng)登錄甫题,以及是否有權(quán)限訪問改網(wǎng)頁馁筐。這就需要有維護(hù)一個會話來保存用戶的登錄狀態(tài)和用戶信息。
從第三步我們也可以看出坠非,如果我們的網(wǎng)頁需要權(quán)限保護(hù)敏沉,那么當(dāng)請求到來的時候,我們就首先要檢查用戶的信息,比如是否已經(jīng)登錄盟迟,是否有權(quán)限等秋泳,如果檢查通過,那么在response的時候就會將相應(yīng)網(wǎng)頁回復(fù)給請求的用戶攒菠,但是如果檢查不通過轮锥,那么就需要返回錯誤信息。
在第二步要尔,我們知道要將用戶名和密碼存儲起來舍杜,但是如果只是簡單的用明文存儲用戶名和密碼,很容易被“有心人”盜取赵辕,從而造成用戶信息泄露既绩,那么我們實際上應(yīng)當(dāng)將用戶信息尤其是密碼做加密處理之后再存儲比較安全。
用戶登出

通過Flask以及相應(yīng)的插件來實現(xiàn)登錄過程
接下來講述如何通過Flask框架以及相應(yīng)的插件來實現(xiàn)整個登錄過程还惠,需要用到的插件如下:

flask-wtf
wtf
werkzeug
flask_login

使用flask-wtf和wtf來實現(xiàn)表單功能
flask-wtf對wtf做了一些封裝饲握,不過有些東西還是要直接用wtf,比如StringField等蚕键。flask-wtf和wtf主要是用于建立html中的元素和Python中的類的對應(yīng)關(guān)系救欧,通過在Python代碼中操作對應(yīng)的類,對象等從而控制html中的元素锣光。我們需要在python代碼中使用flask-wtf和wtf來定義前端頁面的表單(實際是定義一個表單類)笆怠,再將對應(yīng)的表單對象作為render_template函數(shù)的參數(shù),傳遞給相應(yīng)的template誊爹,之后Jinja模板引擎會將相應(yīng)的template渲染成html文本蹬刷,再作為http response返回給用戶。
定義表單類示例代碼:

forms.py

from flask_wtf import FlaskForm
from wtforms import StringField, BooleanField, PasswordField
from wtforms.validators import DataRequired

定義的表單都需要繼承自FlaskForm

class LoginForm(FlaskForm):
# 域初始化時频丘,第一個參數(shù)是設(shè)置label屬性的
username = StringField('User Name', validators=[DataRequired()])
password = PasswordField('Password', validators=[DataRequired()])
remember_me = BooleanField('remember me', default=False)

在wtf當(dāng)中办成,每個域代表就是html中的元素,比如StringField代表的是<input type="text">元素搂漠,當(dāng)然wtf的域還定義了一些特定功能迂卢,比如validators,可以通過validators來對這個域的數(shù)據(jù)做檢查桐汤,詳細(xì)請參考wtf教程而克。
對應(yīng)的html模板可能如下login.html:
{% extends "layout.html" %}
<html>
<head>
<title>Login Page</title>
</head>
<body>
<form action="{{ url_for("login") }}" method="POST">
<p>
User Name:

<input type="text" name="username" />

</p>
<p>
Password:</br>
<input type="password" name="password" />

</p>
<p>
<input type="checkbox" name="remember_me"/>Remember Me
</p>
{{ form.csrf_token }}
</form>
</body>
</html>

這里{{ form.csrf_token }}也可以使用{{ form.hidden_tag() }}來替換
同時我們也可以使用form去定義模板,跟直接用html標(biāo)簽去定義效果是相同的惊科,Jinja模板引擎會將對象拍摇、屬性轉(zhuǎn)化為對應(yīng)的html標(biāo)簽,
相對應(yīng)的template馆截,如下login.html:


{% extends "base.html" %}

{% block content %}
<h1>Sign In</h1>
<form action="{{ url_for("login") }}" method="post" name="login">
{{ form.csrf_token }}
<p>
{{ form.username.label }}

{{ form.username(size=80) }}

</p>
<p>
{{ form.password.label }}


{{ form.password(size=80) }}

</p>
<p>{{ form.remember_me }} Remember Me</p>
<p><input type="submit" value="Sign In"></p>
</form>
{% endblock %}

現(xiàn)在我們需要在view中定義相應(yīng)的路由,并將相應(yīng)的登錄界面展示給用戶。
簡單起見蜡娶,將view的相關(guān)路由定義放在主程序當(dāng)中

app.py

@app.route('/login')
def login():
form = LoginForm()
return render_template('login.html', title="Sign In", form=form)

這里簡單起見混卵,當(dāng)用戶請求'/login'路由時,直接返回login.html網(wǎng)頁窖张,注意這里的html網(wǎng)頁是經(jīng)過Jinja模板引擎將相應(yīng)的模板轉(zhuǎn)換后的html網(wǎng)頁幕随。
至此,如果我們把以上代碼整合到flask當(dāng)中宿接,就應(yīng)該能夠看到相應(yīng)的登錄界面了赘淮,那么當(dāng)用戶提交之后,我們應(yīng)當(dāng)怎樣存儲呢睦霎?這里我們暫時先不用數(shù)據(jù)庫這樣復(fù)雜的工具存儲梢卸,先簡單地存為文件。接下來就看下如何去存儲副女。
加密和存儲
我們可以首先定義一個User類蛤高,用于處理與用戶相關(guān)的操作,包括存儲和驗證等碑幅。

models.py

from werkzeug.security import generate_password_hash
from werkzeug.security import check_password_hash
from flask_login import UserMixin
import json
import uuid

define profile.json constant, the file is used to

save user name and password_hash

PROFILE_FILE = "profiles.json"

class User(UserMixin):
def init(self, username):
self.username = username
self.id = self.get_id()

@property
def password(self):
    raise AttributeError('password is not a readable attribute')

@password.setter
def password(self, password):
    """save user name, id and password hash to json file"""
    self.password_hash = generate_password_hash(password)
    with open(PROFILE_FILE, 'w+') as f:
        try:
            profiles = json.load(f)
        except ValueError:
            profiles = {}
        profiles[self.username] = [self.password_hash,
                                   self.id]
        f.write(json.dumps(profiles))

def verify_password(self, password):
    password_hash = self.get_password_hash()
    if password_hash is None:
        return False
    return check_password_hash(self.password_hash, password)

def get_password_hash(self):
    """try to get password hash from file.

    :return password_hash: if the there is corresponding user in
            the file, return password hash.
            None: if there is no corresponding user, return None.
    """
    try:
        with open(PROFILE_FILE) as f:
            user_profiles = json.load(f)
            user_info = user_profiles.get(self.username, None)
            if user_info is not None:
                return user_info[0]
    except IOError:
        return None
    except ValueError:
        return None
    return None

def get_id(self):
    """get user id from profile file, if not exist, it will
    generate a uuid for the user.
    """
    if self.username is not None:
        try:
            with open(PROFILE_FILE) as f:
                user_profiles = json.load(f)
                if self.username in user_profiles:
                    return user_profiles[self.username][1]
        except IOError:
            pass
        except ValueError:
            pass
    return unicode(uuid.uuid4())

@staticmethod
def get(user_id):
    """try to return user_id corresponding User object.
    This method is used by load_user callback function
    """
    if not user_id:
        return None
    try:
        with open(PROFILE_FILE) as f:
            user_profiles = json.load(f)
            for user_name, profile in user_profiles.iteritems():
                if profile[1] == user_id:
                    return User(user_name)
    except:
        return None
    return None

User類需要繼承flask-login中的UserMixin類戴陡,用于實現(xiàn)相應(yīng)的用戶會話管理。
這里我們是直接存儲用戶信息到一個json文件"profiles.json"
我們并不直接存儲密碼沟涨,而是存儲加密后的hash值恤批,在這里我們使用了werkzeug.security包中的generate_password_hash函數(shù)來進(jìn)行加密,由于此函數(shù)默認(rèn)使用了sha1算法裹赴,并添加了長度為8的鹽值开皿,所以還是相當(dāng)安全的。一般用途的話也就夠用了篮昧。
驗證password的時候赋荆,我們需要使用werkzeug.security包中的check_password_hash函數(shù)來驗證密碼
get_id是UserMixin類中就有的method,在這我們需要overwrite這個method懊昨。在json文件中沒有對應(yīng)的user id時窄潭,可以使用uuid.uuid4()生成一個用戶唯一id

至此,我們就實現(xiàn)了第二步和第五步酵颁,接下來要看第三步嫉你,如何去維護(hù)一個session
維護(hù)用戶session
先看下代碼,這里把相應(yīng)代碼也放入到app.py當(dāng)中
from forms import LoginForm
from flask_wtf.csrf import CsrfProtect
from model import User
from flask_login import login_user, login_required
from flask_login import LoginManager, current_user
from flask_login import logout_user

app = Flask(name)

app.secret_key = os.urandom(24)

use login manager to manage session

login_manager = LoginManager()
login_manager.session_protection = 'strong'
login_manager.login_view = 'login'
login_manager.init_app(app=app)

這個callback函數(shù)用于reload User object躏惋,根據(jù)session中存儲的user id

@login_manager.user_loader
def load_user(user_id):
return User.get(user_id)

csrf protection

csrf = CsrfProtect()
csrf.init_app(app)

@app.route('/login')
def login():
form = LoginForm()
if form.validate_on_submit():
user_name = request.form.get('username', None)
password = request.form.get('password', None)
remember_me = request.form.get('remember_me', False)
user = User(user_name)
if user.verify_password(password):
login_user(user, remember=remember_me)
return redirect(request.args.get('next') or url_for('main'))
return render_template('login.html', title="Sign In", form=form)

維護(hù)用戶的會話幽污,關(guān)鍵就在這個LoginManager對象。
必須實現(xiàn)這個load_user callback函數(shù)簿姨,用以reload user object
當(dāng)密碼驗證通過后距误,使用login_user()函數(shù)來登錄用戶簸搞,這時用戶在會話中的狀態(tài)就是登錄狀態(tài)了

受保護(hù)網(wǎng)頁
保護(hù)特定網(wǎng)頁,只需要對特定路由加一個裝飾器就可以准潭,如下

app.py

...

@app.route('/')
@app.route('/main')
@login_required
def main():
return render_template(
'main.html', username=current_user.username)

...

current_user保存的就是當(dāng)前用戶的信息趁俊,實質(zhì)上是一個User對象,所以我們直接調(diào)用其屬性, 例如這里我們要給模板傳一個username的參數(shù)刑然,就可以直接用current_user.username
使用@login_required來標(biāo)識改路由需要登錄用戶寺擂,非登錄用戶會被重定向到'/login'路由(這個就是由login_manager.login_view = 'login' 語句來指定的)

用戶登出

app.py

...

@app.route('/logout')
@login_required
def logout():
logout_user()
return redirect(url_for('login'))

...

至此,我們就實現(xiàn)了一個完整的登陸和登出的過程泼掠。
另外我們可能還需要其它輔助的功能怔软,諸如發(fā)送確認(rèn)郵件,密碼重置择镇,權(quán)限分級管理等挡逼,這些功能都可以通過flask及其插件來完成,這個大家可以自己探索下啦沐鼠!

轉(zhuǎn)載:http://www.reibang.com/p/06bd93e21945

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末挚瘟,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子饲梭,更是在濱河造成了極大的恐慌乘盖,老刑警劉巖,帶你破解...
    沈念sama閱讀 218,755評論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件憔涉,死亡現(xiàn)場離奇詭異订框,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)兜叨,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,305評論 3 395
  • 文/潘曉璐 我一進(jìn)店門穿扳,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人国旷,你說我怎么就攤上這事矛物。” “怎么了跪但?”我有些...
    開封第一講書人閱讀 165,138評論 0 355
  • 文/不壞的土叔 我叫張陵履羞,是天一觀的道長。 經(jīng)常有香客問我屡久,道長忆首,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,791評論 1 295
  • 正文 為了忘掉前任被环,我火速辦了婚禮糙及,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘筛欢。我一直安慰自己浸锨,他們只是感情好唇聘,可當(dāng)我...
    茶點故事閱讀 67,794評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著揣钦,像睡著了一般雳灾。 火紅的嫁衣襯著肌膚如雪漠酿。 梳的紋絲不亂的頭發(fā)上冯凹,一...
    開封第一講書人閱讀 51,631評論 1 305
  • 那天,我揣著相機(jī)與錄音炒嘲,去河邊找鬼宇姚。 笑死,一個胖子當(dāng)著我的面吹牛夫凸,可吹牛的內(nèi)容都是我干的浑劳。 我是一名探鬼主播,決...
    沈念sama閱讀 40,362評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼夭拌,長吁一口氣:“原來是場噩夢啊……” “哼魔熏!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起鸽扁,我...
    開封第一講書人閱讀 39,264評論 0 276
  • 序言:老撾萬榮一對情侶失蹤蒜绽,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后桶现,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體躲雅,經(jīng)...
    沈念sama閱讀 45,724評論 1 315
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,900評論 3 336
  • 正文 我和宋清朗相戀三年骡和,在試婚紗的時候發(fā)現(xiàn)自己被綠了相赁。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 40,040評論 1 350
  • 序言:一個原本活蹦亂跳的男人離奇死亡慰于,死狀恐怖钮科,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情婆赠,我是刑警寧澤绵脯,帶...
    沈念sama閱讀 35,742評論 5 346
  • 正文 年R本政府宣布,位于F島的核電站页藻,受9級特大地震影響桨嫁,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜份帐,卻給世界環(huán)境...
    茶點故事閱讀 41,364評論 3 330
  • 文/蒙蒙 一璃吧、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧废境,春花似錦畜挨、人聲如沸筒繁。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,944評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽毡咏。三九已至,卻和暖如春逮刨,著一層夾襖步出監(jiān)牢的瞬間呕缭,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,060評論 1 270
  • 我被黑心中介騙來泰國打工修己, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留恢总,地道東北人。 一個月前我還...
    沈念sama閱讀 48,247評論 3 371
  • 正文 我出身青樓睬愤,卻偏偏與公主長得像片仿,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子尤辱,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,979評論 2 355

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