flask
flask_ext
python
Login
在開發ASP.NET
的時候,或多或少會有使用者狀態記錄的需求,這時候也許就會利用SESSION
來記錄登入狀態以及權限,如果你使用的是MASTER-DETAIL
的架構的話,你也許會在MASTER
框上寫一個判斷式,在每一次的PAGE_LOAD
的時候就讀一次權限,不考慮權限的情況下,最少也會考慮人員是否為登入狀態,是的話才給看某個頁面。
而flask-login
就是幫Flask開發人員做這件事的一個擴展應用,官方文件開頭說的很清楚了,flask-login
只做登入/登出的狀態記錄而以,搭配裝飾器讓我們快速的稽核使用者登入某頁面的時候狀態是否為已登入,若否,那就導向login頁面要求使用者登入。
pip install flask-login
要記得,flask-login
會使用到session
,因此務必設置參數SECRET_KEY
,如下:
app.config['SECRET_KEY'] = 'Your Key'
from flask_login import LoginManager
# 實作
login_manager = LoginManager()
login_manager.init_app(app)
# 看__init__的部份,在實作的時候將app當參數應該是可行的,如下說明
login_manager = LoginManager(app)
flask_login.LoginManager
的__init__
中,還是有app
可以接參數,判斷app
是否為None
,若是則執行self.init_app
class LoginManager(object):
def __init__(self, app=None, add_context_processor=True):
# ...中略
if app is not None:
self.init_app(app, add_context_processor)
UserMixin
幫我們記錄了四種用戶狀態:
觀察一下UserMixin
內有什麼乾坤,如下:
class UserMixin(object):
@property
def is_active(self):
return True
@property
def is_authenticated(self):
return True
@property
def is_anonymous(self):
return False
def get_id(self):
try:
return text_type(self.id)
except AttributeError:
raise NotImplementedError('No `id` attribute - override `get_id`')
實務上我們會在建置使用者的Model
時繼承UserMixin
, 其中get_id
是return self.id
,這個self.id
就是Model
的id
了,這樣就不難明白為什麼要在使用者Model
中繼承該類了。
目前為止非常抽像,在調整實作專案的時候會比較清楚,先有個概念即可。
第二個前置設置為call back function,設置該function的主要用意是讓flask-login
隨時想到就call你,而且還一定call的到,如下:
@login_manager.user_loader
def load_user(userid):
return User.get(userid)
當然上面那句是玩笑話,可以看下面的延伸閱讀,在下有簡單說明。
範例?是的,這樣子就可以開始使用flask-login
了,不開玩笑,開始吧。
範例來自官方git,看完範例再看下面的了解程式碼。
from flask import Flask, url_for, request, redirect
from flask_login import LoginManager, UserMixin, login_user, current_user, login_required, logout_user
app = Flask(__name__)
# 會使用到session,故為必設。
app.secret_key = 'Your Key'
login_manager = LoginManager(app)
# login\_manager.init\_app(app)也可以
# 假裝是我們的使用者
users = {'foo@bar.tld': {'password': 'secret'}}
class User(UserMixin):
"""
設置一: 只是假裝一下,所以單純的繼承一下而以 如果我們希望可以做更多判斷,
如is_administrator也可以從這邊來加入
"""
pass
@login_manager.user_loader
def user_loader(email):
"""
設置二: 透過這邊的設置讓flask_login可以隨時取到目前的使用者id
:param email:官網此例將email當id使用,賦值給予user.id
"""
if email not in users:
return
user = User()
user.id = email
return user
@app.route('/login', methods=['GET', 'POST'])
def login():
"""
官網git很給力的寫了一個login的頁面,在GET的時候回傳渲染
"""
if request.method == 'GET':
return '''
<form action='login' method='POST'>
<input type='text' name='email' id='email' placeholder='email'/>
<input type='password' name='password' id='password' placeholder='password'/>
<input type='submit' name='submit'/>
</form>
'''
email = request.form['email']
if request.form['password'] == users[email]['password']:
# 實作User類別
user = User()
# 設置id就是email
user.id = email
# 這邊,透過login_user來記錄user_id,如下了解程式碼的login_user說明。
login_user(user)
# 登入成功,轉址
return redirect(url_for('protected'))
return 'Bad login'
@app.route('/protected')
@login_required
def protected():
"""
在login_user(user)之後,我們就可以透過current_user.id來取得用戶的相關資訊了
"""
# current_user確實的取得了登錄狀態
if current_user.is_active:
return 'Logged in as: ' + current_user.id + 'Login is_active:True'
@app.route('/logout')
def logout():
"""
logout\_user會將所有的相關session資訊給pop掉
"""
logout_user()
return 'Logged out'
if __name__ == '__main__':
app.debug = True
app.run()
相關的說明皆書寫於註解,基本上範例很簡單,大概作業如下:
user
)登入驗證帳號密碼,實務上我們會從資料庫中取回該帳號使用者並驗證密碼是否正確user
作為login_user
的參數綁定
後續就可以直接利用current_user
隨處取得目前的使用者相關資訊(依個人實作User類設置而定)、登入狀態以及利用裝飾器@login_request
驗證登入狀況(內含session
與Flask的thread
、request context
相關觀念)。
試著在未登入的情況下先直接連接@login_required
的網頁
http://127.0.0.1/protected
,正確的話會取得錯誤資訊。
接著利用http://127.0.0.1/login
正常的登入,輸入上面設置的帳號密碼,再重新連接http://127.0.0.1/protected
可以看的到我們利用current_user.id
確實取得使用者帳號資訊
flask-login
確實在實作網站上幫了我們不少忙,current_user
並非只能應用於後端,前端jinja2
也可以直接使用該物件,只要利用{{ current_user}}
就可以在前端直接取得使用者訊息,唯一要注意的是current_user
是一個線程物件,因此在寫入資料庫的時候習慣上會跟Ginberg(Flask Web開發作者)一樣使用current_ser._get_current_object().xx
。(這個觀念也是git上請教Ginberg學來的)
不少人對user_loader
感到好奇,我們透過原始碼了解一下:
def user_loader(self, callback):
self.user_callback = callback
return callback
實作的程式碼如下:
@login_manager.user_loader
def load_user(userid):
return User.get(userid)
透過裝飾器,我們將load_user
丟到callback
,並且賦值給user_callback
。
接著我們開始追蹤到底是誰用了user_callback
,看到reload_user
,如下:
def reload_user(self, user=None):
ctx = _request_ctx_stack.top
if user is None:
# 當user是None,就透過session來取得user_id
user_id = session.get('user_id')
# 如果user_id又是None
if user_id is None:
# 那就當你是匿名登入
ctx.user = self.anonymous_user()
else:
# 如果你沒有設置load_user給它call back,它就氣噗噗告訴你沒有實作
if self.user_callback is None:
raise Exception(
"No user_loader has been installed for this "
"LoginManager. Refer to"
"https://flask-login.readthedocs.io/"
"en/latest/#how-it-works for more info.")
# 這邊,user透過cuser_callback取得user
user = self.user_callback(user_id)
# 如果user還是None,那就給它匿名屬性,不然ctx.user就賦值user
if user is None:
ctx.user = self.anonymous_user()
else:
ctx.user = user
else:
# 給這需求上下文一個屬性user,賦值user
ctx.user = user
第2行:ctx = _request_ctx_stack = LocalStack()
這是Flask請求上下文的佇列,在此實作中取最上的指令,簡單的範例如下:
>>> from werkzeug import LocalStack
>>> ls=LocalStack()
>>> ls.push(100)
[100]
>>> ls.top
100
>>> ls.push(40)
[100, 40]
>>> ls.top
40
第10行:我們可以觀察一下初始化LoginManager
,在初始化的時候對這個參數是預設為匿名function
def __init__(self, app=None, add_context_processor=True):
#: A class or factory function that produces an anonymous user, which
#: is used when no one is logged in.
self.anonymous_user = AnonymousUserMixin
而這個AnonymousUserMixin
不同於Usermixin
,你看,它的is_anonymous
是True
,如下:。
class AnonymousUserMixin(object):
'''
This is the default object for representing an anonymous user.
'''
@property
def is_authenticated(self):
return False
@property
def is_active(self):
return False
@property
def is_anonymous(self):
return True
def get_id(self):
return
第20行:user = self.user_callback(user_id)
,而user_callback=callback
,而callback就call了你設置的return User.get(userid)
,一切就明白了。
最後,這個reload_user
就四處給人call,這部份可以從login_manager.py
來看,從這邊可以看的到,除了透過session
來記錄之外,最重要的是還搭配了request
的上下文(thread
)。
我們如果沒有需要很複雜的狀態管理,其實簡單自己寫一個function也可以達到相同的目地。
login_user
是主要實作過程中會記錄下我們帳號的一個function,在login
驗證完成之後,我們會讓實作的User
物件當參數丟給login
,常用兩個參數是user
與remember
,用途各自如下:
user
: 實作User
類別remember
: 記住我,實務上會在登錄的Form上有一個Bool欄記錄True/False在經過login_user
之後,後面的應用就都可以利用current_user
來取得用戶資訊,原始碼如下:
def login_user(user, remember=False, duration=None, force=False, fresh=True):
if not force and not user.is_active:
return False
user_id = getattr(user, current_app.login_manager.id_attribute)()
session['user_id'] = user_id
session['_fresh'] = fresh
session['_id'] = current_app.login_manager._session_identifier_generator()
if remember:
session['remember'] = 'set'
if duration is not None:
try:
# equal to timedelta.total_seconds() but works with Python 2.6
session['remember_seconds'] = (duration.microseconds +
(duration.seconds +
duration.days * 24 * 3600) *
10**6) / 10.0**6
except AttributeError:
raise Exception('duration must be a datetime.timedelta, '
'instead got: {0}'.format(duration))
_request_ctx_stack.top.user = user
user_logged_in.send(current_app._get_current_object(), user=_get_user())
return True
第5行:那個getattr(user, current\\\_app.login\\\_manager.id_attribute)
,id_attribute
是何方神聖,是ID_ATTRIBUTE
,而ID_ATTRIBUTE
是是get_id
,所以會取得user
的get_id
,這個user
的get_id
就是設置一中的get_id
,這樣就取得user_id
給session
了,如下程式碼:
def get_id(self):
try:
return text_type(self.id)
except AttributeError:
raise NotImplementedError('No `id` attribute - override `get_id`')
第6行:利用session['user_id']
來記錄登錄的使用者id
第10行:記住我的功能,透過參數remember
可以方便的實現這功能
第23行:需求上下文中的佇列最上面的那一個的user
就賦值這個user
給它,user_loader
也是一樣的作法,如下:
def reload_user(self, user=None):
ctx = _request_ctx_stack.top
#...下略...#
current_user
如何可以讓我們在login_user(user)
之後就可以以此物件取得目前用戶資訊,如下:
current_user = LocalProxy(lambda: _get_user())
def _get_user():
if has_request_context() and not hasattr(_request_ctx_stack.top, 'user'):
current_app.login_manager._load_user()
return getattr(_request_ctx_stack.top, 'user', None)
第2行:條件成立的話就執行current_app.login_manager._load_user()
,如下:
def _load_user(self):
'''Loads user from session or remember_me cookie as applicable'''
#...中略...#
return self.reload_user()
而_load_user
又回傳self.reload_user()
跟上面我們所追蹤的結果是一致的。
這部份在下有簡單說明的文章供查閱
我們預計讓這個function以裝飾器來執行,只要session['userid']
沒有東西,我們就將來源導向login
,如下簡單範例:
from functools import wraps
from flask import session, request, redirect, url_for
def login_required(func):
@wraps(func)
def decorated_function(*args, **kwargs):
if session.get('userid') is None:
return redirect(url_for('login', next=request.url))
return func(*args, **kwargs)
return decorated_function
第8行:利用next帶著request.url在login之後可以繼續回來源網頁去,只是這部份就有隱約在了,如果允許的話,還是需要特別的處理過。
後續記得在login的時候賦值給session['userid'],這樣子就可以做一個簡單的登入檢核了