0.前言:
最近使用flask搭建博客實(shí)現(xiàn)登錄時(shí),碰到登錄用戶無法分配Permissions的問題,整整花了一個(gè)星期的時(shí)間,所以想記錄一下。
1.Signal基本了解:
根據(jù)官方文檔給出的解釋:
What are signals? Signals help you decouple applications by sending notifications when actions occur elsewhere in the core framework or another Flask extensions. In short, signals allow certain senders to notify subscribers that something happened.
Signal實(shí)際上是用于解耦系統(tǒng)中實(shí)現(xiàn)某種業(yè)務(wù)邏輯和行為。所謂的解耦,就是某些行為被觸發(fā)時(shí),自動(dòng)發(fā)送定義好的一種信號(hào),與這個(gè)信號(hào)綁定的一些業(yè)務(wù)邏輯或行為,接收到這個(gè)信號(hào)后,會(huì)自動(dòng)執(zhí)行各自相應(yīng)的業(yè)務(wù)邏輯。信號(hào)和一些業(yè)務(wù)邏輯或行為綁定好了之后,只需要發(fā)送一個(gè)信號(hào)即可,所有與該行為有關(guān)的業(yè)務(wù)邏輯或行為都會(huì)自動(dòng)觸發(fā),從而實(shí)現(xiàn)了解耦。信號(hào)發(fā)送無需了解接收信號(hào)的訂閱者是誰,這就是觀察者模式的一種實(shí)現(xiàn)方式。
2.Signal實(shí)現(xiàn)原理:
from flask.signals import Namespace
from flask import current_app
signals = Namespace()
mysignal = signals.signal('save-models') #signal的創(chuàng)建
def save_models:
...
mysignal.send(current_app._get_current_object_) #信號(hào)發(fā)送
Signal的訂閱者:
一些訂閱了指定signal也就是上文中的mysignal的函數(shù),當(dāng)調(diào)用send()函數(shù)發(fā)送signal時(shí),會(huì)自動(dòng)觸發(fā)這些訂閱者的調(diào)用,以完成某些功能。將一個(gè)普通函數(shù)變?yōu)閟ignal的訂閱函數(shù)非常簡(jiǎn)單,只要加一個(gè)decorator即可,仍以上面signal為例,定義一個(gè)訂閱者方法如下:
@mysignal.connect_via(app)
# mysignal.connect也可以
def on_model_saved():
# do something ...
3.踩坑:
上面代碼看起來是不是很簡(jiǎn)單?接下來就開始講之前碰到的坑:
其實(shí)在Flask-Principal應(yīng)用場(chǎng)景解析中有講過,flask在用戶登錄時(shí)實(shí)現(xiàn)身份改變信號(hào)邏輯中,通過identity_changed和identity_loaded這兩個(gè)不同的signal來處理的,當(dāng)用戶登錄之前,我們都會(huì)通過發(fā)送一個(gè)信號(hào)說明用戶身份已經(jīng)改變,當(dāng)相應(yīng)的邏輯處理接收到信號(hào)之后,能后改變其身份信息以及權(quán)限信息,login_view.py:
from flask import Flask, current_app, request, session
from flask_login import LoginManager, login_user, logout_user, login_required, current_user
from flask_principal import identity_changed
@app.route('/login', methods=['GET', 'POST'])
def login():
form = LoginForm()
if form.validate_on_submit():
user = datastore.find_user(email=form.email.data)
if form.password.data == user.password:
# Flask-Login的login_user方法將登錄用戶信息保存于session中
login_user(user)
# 發(fā)送信號(hào)
identity_changed.send(current_app._get_current_object(),
identity=Identity(user.id))
return redirect(request.args.get('next') or '/')
return render_template('login.html', form=form)
Signal訂閱者on_identity_change:
from flask_principal import identity_loaded, RoleNeed, UserNeed, Permission
from flask_login import current_user
admin_need = RoleNeed('admin')
admin_Permission = Permission(admin_need)
@identity_loaded.connect
def on_identity_change(sender, identity):
identity.user = current_user
if hasattr(current_user, 'username'):
identity.provides.add(UserNeed(current_user.username))
if hasattr(current_user, 'role'):
identity.provides.add(RoleNeed(current_user.role))
if hasattr(current_user, 'admin')
identity.provides.add(admin_need)
運(yùn)行后發(fā)現(xiàn){'identity': <Identity id="admin" auth_type="None" provides=set()>}provides為空
然后在debug跟蹤發(fā)現(xiàn),receivers為空的dict,不是說過只要是有定義的signal發(fā)送信號(hào),再綁定對(duì)應(yīng)的訂閱者就會(huì)自動(dòng)觸發(fā)執(zhí)行該函數(shù)嗎?

接下來,就開始看源代碼,看看執(zhí)行流程:
1.在工廠函數(shù)初始化創(chuàng)建app時(shí),就有init_app函數(shù)進(jìn)行初始化,identity_changed.connect(self._on_identity_changed, app)來進(jìn)行連接:
from flask import Flask
from flask_login import LoginManager
from flask_principal import Principal
login_manager = LoginManager()
login_manager.session_protection = 'basic'
login_manager.login_view = 'xxx.login'
Principals = Principal()
def create_app(config_name):
app = Flask(__name__)
...
login_manager.init_app(app)
Principals.init_app(app)//初始化app
return app
app = create_app()
對(duì)應(yīng)源碼:
class Principal(object):
...
...
def init_app(self, app):
if hasattr(app, 'static_url_path'):
self._static_path = app.static_url_path
else:
self._static_path = app.static_path
app.before_request(self._on_before_request)
identity_changed.connect(self._on_identity_changed, app)
if self.use_sessions:
self.identity_loader(session_identity_loader)
self.identity_saver(session_identity_saver)
2.接著通過_on_identity_changed來進(jìn)行調(diào)用,接著調(diào)用set_identity,源碼:
class Principal(object):
...
...
def _on_identity_changed(self, app, identity):
if self._is_static_route():
return
self.set_identity(identity)
3.接著調(diào)用_set_thread_identity來執(zhí)行identity_loaded.send()發(fā)送identity_loaded信號(hào),源碼:
class Principal(object):
...
...
def set_identity(self, identity):
"""Set the current identity.
:param identity: The identity to set
"""
self._set_thread_identity(identity)
for saver in self.identity_savers:
saver(identity)
def _set_thread_identity(self, identity):
g.identity = identity
identity_loaded.send(current_app._get_current_object(),
identity=identity)
4.接下來,我們看send函數(shù)的源碼:
def send(self, *sender, **kwargs):
"""Emit this signal on behalf of *sender*, passing on \*\*kwargs.
Returns a list of 2-tuples, pairing receivers with their return
value. The ordering of receiver notification is undefined.
:param \*sender: Any object or ``None``. If omitted, synonymous
with ``None``. Only accepts one positional argument.
:param \*\*kwargs: Data to be sent to receivers.
"""
# Using '*sender' rather than 'sender=None' allows 'sender' to be
# used as a keyword argument- i.e. it's an invisible name in the
# function signature.
if len(sender) == 0:
sender = None
elif len(sender) > 1:
raise TypeError('send() accepts only one positional argument, '
'%s given' % len(sender))
else:
sender = sender[0]
if not self.receivers:
return []
else:
return [(receiver, receiver(sender, **kwargs))
就在程序執(zhí)行到這一步中,發(fā)現(xiàn)self.receivers為dict{ },為什么找不到接收信號(hào)的訂閱者函數(shù)?其實(shí)我們回過頭來看signal基本實(shí)現(xiàn)機(jī)制就知道,當(dāng)定義一個(gè)signal之后發(fā)送信號(hào),所以與這個(gè)信號(hào)有關(guān)的業(yè)務(wù)邏輯或者行為都會(huì)自動(dòng)觸發(fā),自動(dòng)觸發(fā)的條件就是與這個(gè)信號(hào)有關(guān),也就是說,我們要用引用到這個(gè)條件才行。所以在login_view.py中,引用訂閱者函數(shù)對(duì)應(yīng)的模塊:
from flask import Flask, current_app, request, session
from flask_login import LoginManager, login_user, logout_user, login_required, current_user
from flask_principal import identity_changed
from .permissions import admin_Permission # 引用Permissions模塊
@app.route('/login', methods=['GET', 'POST'])
def login():
form = LoginForm()
if form.validate_on_submit():
user = datastore.find_user(email=form.email.data)
if form.password.data == user.password:
# Flask-Login的login_user方法將登錄用戶信息保存于session中
login_user(user)
# 發(fā)送信號(hào)
identity_changed.send(current_app._get_current_object(),
identity=Identity(user.id))
return redirect(request.args.get('next') or '/')
return render_template('login.html', form=form)
總結(jié):
1.要熟悉實(shí)現(xiàn)某種功能使用到的某個(gè)模塊的實(shí)現(xiàn)原理和流程
2.善于研究源碼,熟悉flask一些機(jī)制
3.記得利用stack overflow等一些平臺(tái)解決問題。
參考:
Gevin大神的文章:https://blog.igevin.info/posts/flask-signal-get-started/
stackoverflow:https://stackoverflow.com/questions/7050137/flask-principal-tutorial-auth-authr/9781669#9781669
官方文檔:http://flask.pocoo.org/docs/1.0/signals/
blinker文檔:https://pythonhosted.org/blinker/
http://www.bjhee.com/flask-ad2.html