在了解使用Flask來實現用戶認證之前,我們首先要明白用戶認證的原理。假設現在我們自己去實現用戶認證,需要做哪些事情呢?
- 首先,登錄。用戶能夠輸入用戶名和密碼進行登錄,所以需要網頁和表單,實現用戶輸入和提交的過程。
- 接著,校驗登錄是否成功。用戶提交了用戶名和密碼,后臺需要比對用戶名密碼是否正確,而要想比對,首先系統中就要有存儲用戶名密碼的地方,大多數后臺系統會通過數據庫來存儲,也可以存儲到文件當中。存儲用戶名密碼需要加密存儲尤其是密碼,如果只是簡單的用明文存儲,很容易被“有心人”盜取,從而造成用戶信息泄露
- 登錄之后,我們需要維持用戶登錄狀態,以便用戶在訪問特定網頁的時候來判斷用戶是否已經登錄,以及是否有權限訪問改網頁。這需要維護一個會話來保存用戶的登錄狀態和用戶信息。
- 從第三步我們也可以看出,如果我們的網頁需要權限保護,那么當請求到來的時候,我們首先要檢查用戶的信息,比如是否已經登錄,是否有權限等,如果檢查通過,那么在response的時候就會將相應網頁回復給請求的用戶,但是如果檢查不通過,那么就需要返回錯誤信息。
- 用戶登出
flask通常是使用Flask-Login模塊來實現上述流程控制。下面介紹使用Flask-Login登錄注銷,以及幫助大家解答一些可能比較常見的問題。
代碼實現
首先,先概述下例子,有三個url,分別是:
/auth/login???? 用于登錄
/auth/logout????用于注銷
/test???????????用于測試,需要登錄才能訪問
安裝必要的庫
pip install Flask==0.10.1
pip install Flask-Login==0.3.2
pip install Flask-WTF==0.12
pip install WTForms==2.1
編寫web框架。在開始登錄之前,我們先把整個 web 的框架搭建出來,也就是,我們要能夠先在不登錄的情況下訪問到上面提到的三個url,我就直接放在一個叫做 app.py 的文件中。
#!/usr/bin/env python
# encoding: utf-8
from flask import Flask, Blueprintapp = Flask(__name__)# url redirect
auth = Blueprint('auth', __name__)@auth.route('/login', methods=['GET', 'POST'])
def login():return "login page"@auth.route('/logout', methods=['GET', 'POST'])
def logout():return "logout page"????# test method
@app.route('/test')
def test():return "yes , you are allowed"app.register_blueprint(auth, url_prefix='/auth')
app.run(debug=True)
現在,我們可以嘗試一下運行一下這個框架,使用 python app.py?運行即可,然后打開瀏覽器,分別訪問一下,看一下是否都正常
http://localhost:5000/test
http://localhost:5000/auth/login
http://localhost:5000/auth/logout
設置登錄才能查看。現在框架已經設置完畢,我們可以將 test 和 auth/logout 這兩個 page 設置成登錄之后才能查看。因為這個功能已經和 login 有關系了,所以這時我們就需要使用到 Flask-Login了。代碼如下
#!/usr/bin/env python
# encoding: utf-8
from flask import Flask, Blueprint
from flask.ext.login import LoginManager, login_requiredapp = Flask(__name__)#################### 以下這段是新增加的 ####################
app.secret_key = 's3cr3t'
login_manager = LoginManager()# 設置不同的安全等級防止用戶會話遭篡改,屬性可以設為None、basic或strong
# 設為 strong 時,Flask-Login 會記錄客戶端 IP 地址和瀏覽器的用戶代理信息,如果發現異動就登出用戶
login_manager.session_protection = 'strong' # 如果未登錄,返回的頁面
login_manager.login_view = 'auth.login'
login_manager.init_app(app)# Flask-Login 要求程序實現一個回調函數,使用指定的標識符加載用戶。加載用戶的回調函數接收以 Unicode 字符串形式表示的用戶標識符。如果能找到用戶,這個函數必須返回用戶對象;否則應該返回 None,這里因為設置框架所以就默認返回 None。
@login_manager.user_loader
def load_user(user_id):return None
#################### 以上這段是新增加的 #################### auth = Blueprint('auth', __name__)@auth.route('/login', methods=['GET', 'POST'])
def login():return "login page"# 通過Flask-Login提供的login_required裝飾器來增加路由保護,如果未認證用戶訪問這個路由,Flask-Login會將這個請求發往登錄頁面
@auth.route('/logout', methods=['GET', 'POST'])
@login_required
def logout():return "logout page"# test method
@app.route('/test')
@login_required
def test():return "yes , you are allowed"app.register_blueprint(auth, url_prefix='/auth')
app.run(debug=True)
其實我們就增加了兩項代碼,一項是初始化 LoginManager 的, 另外一項就是給 test 和 auth.logout 添加了 login_required 的裝飾器,表示要登錄了才能訪問。注意 login_required 必須放在 auth.route 后面
#################### 部分源碼 ####################
@app.route('/test', methods=['GET', 'POST'])
@csrf.exempt
@login_required
def test():pass
# test= app.route('/test', methods=['GET', 'POST'])(test)
# test= login_required(test)# login_required 源碼
def login_required(func):@wraps(func)def decorated_view(*args, **kwargs):if current_app.login_manager._login_disabled:return func(*args, **kwargs)elif not current_user.is_authenticated:return current_app.login_manager.unauthorized()return func(*args, **kwargs)return decorated_view# app.route 實際最后執行代碼
def app.route() if view_func is not None:old_func = self.view_functions.get(endpoint)if old_func is not None and old_func != view_func:raise AssertionError('View function mapping is overwriting an existing endpoint function: %s' % endpoint)self.view_functions[endpoint] = view_func#################### 分析 ####################
# 原因,正常情況下裝飾器需要將函數地址傳入并返回一個新的函數地址,但是 app.route 創建了一個新的結構并將傳入的函數地址直接保存到結構中,導致其他的裝飾器對這個函數地址修改影響不到 app.route 創建的結構,而在路由分發的時候,直接調用的是結構中保存的地址,所以其他裝飾器不起作用,所以必須將裝飾器放在 app.route 下面#################### 簡化代碼 ####################
def a():return 1def b():return 2c = a
a = b
print(c())
用戶授權。到此,我們發現?test 是不能訪問的,會被重定向到 login 的那個 page。看一下現在的代碼, login_required 有了, 那么就差login了,接下來寫login,看Flask-Login的文檔發現一個叫做login_user的函數,看看它的原型:
flask.ext.login.login_user(user, remember=False, force=False, fresh=True)
這里需要一個user的對象,所以先創建一個Model,其實這個Model還是有一點講究的,最好是繼承自Flask-Login的UserMixin,然后需要實現幾個方法,Model 為:
# user models
class User(UserMixin):def is_authenticated(self):return Truedef is_actice(self):return Truedef is_anonymous(self):return Falsedef get_id(self):return "1"
這里給所有的函數都返回了默認值,默認對應的情況是這個用戶已經登錄,并且是有效的。
然后在 login 的 view 里面 login_user, logout的view里面logout_user,這樣整個登錄過程就連接起來了,最后的代碼是這樣的:
#!/usr/bin/env python
# encoding: utf-8from flask import Flask,Blueprint
from flask.ext.login import LoginManager,login_required,login_user,logout_user,UserMixinapp = Flask(__name__)# user models
class User(UserMixin):def is_authenticated(self):return Truedef is_actice(self):return Truedef is_anonymous(self):return Falsedef get_id(self):return "1"# flask-login
app.secret_key = 's3cr3t'
login_manager = LoginManager()
login_manager.session_protection = 'strong'
login_manager.login_view = 'auth.login'
login_manager.init_app(app)@login_manager.user_loader
def load_user(user_id):user = User()return userauth = Blueprint('auth', __name__)@auth.route('/login', methods=['GET', 'POST'])
def login():user = User()login_user(user)return "login page"@auth.route('/logout', methods=['GET', 'POST'])
@login_required
def logout():logout_user()return "logout page"@app.route('/test')
@login_required
def test():return "yes , you are allowed"app.register_blueprint(auth, url_prefix='/auth')
app.run(debug=True)
總結
到此,這就是一個比較精簡的Flask-Login 教程了,通過這個框架大家可以自行擴展達到更豐富的功能,諸如發送確認郵件,密碼重置,權限分級管理等,這些功能都可以通過flask及其插件來完成,這個大家可以自己探索下。
問題
1、未登錄訪問鑒權頁面如何處理
如果未登錄訪問了一個做了 login_required 限制的 view,那么 flask-login 會默認 flash 一條消息,并且將重定向到 login view, 如果你沒有指定 login view, 那么 flask-login 將會拋出一個401錯誤。指定 login view 只需要直接設置login_manager即可:
login_manager.login_view = "auth.login"
2、自定義flash消息
login_manager.login_message = u"請登錄!" # 自定義 flash 的消息
login_manager.login_message_category = "info" # flash 消息的級別,一般設置成 info 或者 error
?
3、自定義未登錄處理函數
如果你不想使用默認的規則,那么你也可以自定義未登錄情況的處理函數,只需要使用 login_manager 的 unauthorized_handler 裝飾器即可。
@login_manager.unauthorized_handler
def unauthorized():# do stuffreturn render_template("some template")
4、匿名用戶是怎么處理的?有哪些屬性?
在 flask-login 中,如果一個匿名用戶訪問站點,那么 current_user 對象會被設置成一個 AnonymousUserMixin 的對象,AnonymousUserMixin 對象有以下方法和屬性:
- is_active?and is_authenticated are?False
- is_anonymous?is?True
- get_id()?returns?None
5、自定義匿名用戶Model:
如果你有需求自定義匿名用戶的 Model,那么你可以通過設置 login_manager 的 anonymous_user 屬性來實現,而賦值的對象只需是可調用對象(class 和 function都行)即可。
login_manager.anonymous_user = MyAnonymousUser
6、Flask-Login如何加載用戶的:
當一個請求過來的時候,如果 ctx.user 沒有值,那么 flask-login 就會使用 session 中 session['user_id'] 作為參數,調用 login_manager 中使用 user_loader 裝飾器設置的 callback 函數加載用戶,需要注意的是,如果指定的 user_id 無效,不應該拋出異常,而是應該返回 None。
登錄成功后,就可以使用 current_use r對象了,current_user 保存的就是當前用戶的信息,實質上是一個 User 對象,所以我們直接調用其屬性, 例如這里我們要給模板傳一個 username 的參數,就可以直接用 current_user.username
@login_manager.user_loader
def load_user(user_id):return User.get(user_id)
session['user_id'] 其實是在調用 login_in 函數之后自動設置的。
7、Flask-Login設置session過期時間:
在 Flask-Login 中,如果你不特殊處理的話,session 是在你關閉瀏覽器之后就失效的。也就是說每次重新打開頁面都是需要重新登錄的。如果你需要自己控制 session 的過期時間的話:
- 首先需要設置 login_manager 的 session類型為永久的,
- 然后再設置 session 的過期時間
#################### 配置文件 ####################
class Config:...PERMANENT_SESSION_LIFETIME = datetime.timedelta(minutes=5)#################### 登錄 ####################
def login():login_user(user)session.permanent = True # 設置session永久有效 注意這個要設置在request里邊 即請求內部
同時,還需要注意的是 cookie 的默認有效期其實是 一年 的,所以,我們最好也設置一下:
login_manager.remember_cookie_duration=timedelta(days=1)
8、如何在同域名下的多個系統共享登錄狀態
這個需求可能在公司里面會比較常見,也就是說我們一個公司域名下面會有好多個子系統,但是這些子系統都是不同部門開發的,那么,我們如何在這不同系統間共享登錄狀態?也就是說,只要在某一個系統登錄了,在使用其他系統的時候也共享著登錄的狀態,不需要再次登錄,除非登錄失效。
Server-side Sessions with Redis
這個說明嘗試,也差不多是類似的解決方法。
9、使用Flask自帶的函數加密存儲密碼
# models.pyfrom 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 = usernameself.id = self.get_id()@propertydef password(self):raise AttributeError('password is not a readable attribute')@password.setterdef 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 Falsereturn 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 inthe 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 Noneexcept ValueError:return Nonereturn Nonedef get_id(self):"""get user id from profile file, if not exist, it willgenerate 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:passexcept ValueError:passreturn unicode(uuid.uuid4())@staticmethoddef 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 Nonetry: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 Nonereturn Non
-
User類需要繼承flask-login中的UserMixin類,用于實現相應的用戶會話管理。
-
這里我們是直接存儲用戶信息到一個json文件"profiles.json"
-
我們并不直接存儲密碼,而是存儲加密后的hash值,在這里我們使用了werkzeug.security包中的generate_password_hash函數來進行加密,由于此函數默認使用了sha1算法,并添加了長度為8的鹽值,所以還是相當安全的。一般用途的話也就夠用了。
-
驗證password的時候,我們需要使用werkzeug.security包中的check_password_hash函數來驗證密碼
-
get_id是UserMixin類中就有的method,在這我們需要overwrite這個method。在json文件中沒有對應的user id時,可以使用uuid.uuid4()生成一個用戶唯一id
?