# Flask實作_ext_11_Flask-Login_登入狀態管理 ###### tags: `flask` `flask_ext` `python` `Login` :::danger 官方文件: * [Flask-Login](https://flask-login.readthedocs.io/en/latest/) * [Flask-Login_github](https://github.com/maxcountryman/flask-login) * [Flask-Login_簡中翻譯](http://www.pythondoc.com/flask-login/) ::: ## 說明 在開發`ASP.NET`的時候,或多或少會有使用者狀態記錄的需求,這時候也許就會利用`SESSION`來記錄登入狀態以及權限,如果你使用的是`MASTER-DETAIL`的架構的話,你也許會在`MASTER`框上寫一個判斷式,在每一次的`PAGE_LOAD`的時候就讀一次權限,不考慮權限的情況下,最少也會考慮人員是否為登入狀態,是的話才給看某個頁面。 而`flask-login`就是幫Flask開發人員做這件事的一個擴展應用,官方文件開頭說的很清楚了,`flask-login`只做登入/登出的狀態記錄而以,搭配裝飾器讓我們快速的稽核使用者登入某頁面的時候狀態是否為已登入,若否,那就導向login頁面要求使用者登入。 ## 安裝 ```python= pip install flask-login ``` 要記得,`flask-login`會使用到`session`,因此務必設置參數`SECRET_KEY`,如下: ```python= app.config['SECRET_KEY'] = 'Your Key' ``` ## 範例 ### 初始化 ```python= 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` ```python= 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`內有什麼乾坤,如下: ```python= 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: user_loader 第二個前置設置為call back function,設置該function的主要用意是讓`flask-login`隨時想到就call你,而且還一定call的到,如下: ```python= @login_manager.user_loader def load_user(userid): return User.get(userid) ``` 當然上面那句是玩笑話,可以看下面的<a href="#note_1">延伸閱讀</a>,在下有簡單說明。 ### 實作 範例?是的,這樣子就可以開始使用`flask-login`了,不開玩笑,開始吧。 範例來自[官方git](https://github.com/maxcountryman/flask-login),看完範例再看下面的了解程式碼。 ```python= 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`的參數綁定 * 請見<a href="#note_2">延伸閱讀</a>說明 後續就可以直接利用`current_user`隨處取得目前的使用者相關資訊<sub>(依個人實作User類設置而定)</sub>、登入狀態以及利用裝飾器`@login_request`驗證登入狀況<sub>(內含`session`與Flask的`thread`、`request context`相關觀念)</sub>。 試著在未登入的情況下先直接連接`@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<sub>(Flask Web開發作者)</sub>一樣使用`current_ser._get_current_object().xx`。<sub>(這個觀念也是git上請教Ginberg學來的)</sub> ## 延伸閱讀 ### <span id='note_1'>user_loader</span> 不少人對`user_loader`感到好奇,我們透過原始碼了解一下: ```python= def user_loader(self, callback): self.user_callback = callback return callback ``` 實作的程式碼如下: ```python= @login_manager.user_loader def load_user(userid): return User.get(userid) ``` 透過裝飾器,我們將`load_user`丟到`callback`,並且賦值給`user_callback`。 接著我們開始追蹤到底是誰用了`user_callback`,看到`reload_user`,如下: ```python= 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請求上下文的佇列,在此實作中取最上的指令,簡單的範例如下: ```python= >>> from werkzeug import LocalStack >>> ls=LocalStack() >>> ls.push(100) [100] >>> ls.top 100 >>> ls.push(40) [100, 40] >>> ls.top 40 ``` 第10行:我們可以觀察一下初始化`LoginManager`,在初始化的時候對這個參數是預設為匿名function ```python= 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`,如下:。 ```python= 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也可以達到相同的目地。 ### <span id='note_2'>login_user</span> `login_user`是主要實作過程中會記錄下我們帳號的一個function,在`login`驗證完成之後,我們會讓實作的`User`物件當參數丟給`login`,常用兩個參數是`user`與`remember`,用途各自如下: 1. `user`: 實作`User`類別 2. `remember`: 記住我,實務上會在登錄的Form上有一個Bool欄記錄True/False 在經過`login_user`之後,後面的應用就都可以利用`current_user`來取得用戶資訊,原始碼如下: ```python= 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`了,如下程式碼: ```python= 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`也是一樣的作法,如下: ```python= def reload_user(self, user=None): ctx = _request_ctx_stack.top #...下略...# ``` ### current_user `current_user`如何可以讓我們在`login_user(user)`之後就可以以此物件取得目前用戶資訊,如下: ```python= current_user = LocalProxy(lambda: _get_user()) ``` ```python= 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()`,如下: ```python def _load_user(self): '''Loads user from session or remember_me cookie as applicable''' #...中略...# return self.reload_user() ``` 而`_load_user`又回傳`self.reload_user()`跟上面我們所追蹤的結果是一致的。 ## 其它 ### property 這部份在下有簡單說明的[文章供查閱](https://hackmd.io/s/S1ZBOJIVM) ### 自己寫一個login檢核 我們預計讓這個function以裝飾器來執行,只要`session['userid']`沒有東西,我們就將來源導向`login`,如下簡單範例: ```python= 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'],這樣子就可以做一個簡單的登入檢核了