Try   HackMD

Flask實作_ext_11_Flask-Login_登入狀態管理

tags: 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幫我們記錄了四種用戶狀態:

  • is_authenticated
    • 登入成功時return True(這時候才能過的了login_required)
  • is_active
    • 帳號啟用並且登入成功的時候return True
  • is_anonymous
    • 匿名用戶return True(登入用戶會return False)
  • get_id()
    • 取得當前用戶id

觀察一下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_idreturn self.id,這個self.id就是Modelid了,這樣就不難明白為什麼要在使用者Model中繼承該類了。

目前為止非常抽像,在調整實作專案的時候會比較清楚,先有個概念即可。

設置二:call back function: user_loader

第二個前置設置為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()

相關的說明皆書寫於註解,基本上範例很簡單,大概作業如下:

  1. 設置一:UserMixin
  2. 設置二:call back function: user_loader
  3. 使用者(user)登入驗證帳號密碼,實務上我們會從資料庫中取回該帳號使用者並驗證密碼是否正確
  4. user作為login_user的參數綁定

後續就可以直接利用current_user隨處取得目前的使用者相關資訊(依個人實作User類設置而定)、登入狀態以及利用裝飾器@login_request驗證登入狀況(內含session與Flask的threadrequest 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

不少人對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_anonymousTrue,如下:。

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

login_user是主要實作過程中會記錄下我們帳號的一個function,在login驗證完成之後,我們會讓實作的User物件當參數丟給login,常用兩個參數是userremember,用途各自如下:

  1. user: 實作User類別
  2. 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,所以會取得userget_id,這個userget_id就是設置一中的get_id,這樣就取得user_idsession了,如下程式碼:

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

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()跟上面我們所追蹤的結果是一致的。

其它

property

這部份在下有簡單說明的文章供查閱

自己寫一個login檢核

我們預計讓這個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'],這樣子就可以做一個簡單的登入檢核了