源码链接: https://github.com/maxcountryman/flask-login/blob/0.1.1/flaskext/login.py
python版本:2.7.12
系统: ubuntu16.04
(1) 从_request_ctx_stack的top中获取user属性
def _get_user():
return getattr(_request_ctx_stack.top, "user", None)
_request_ctx_stack是从flask中导入的,本质是werkzeug.local中LocalProxy
问题: 既然可以获取user, 哪什么时候把user存入LocaalProxy栈中?
(2) 对值(用户名/id等)进行加密
def _cookie_digest(payload, key=None):
if key is None:
key = current_app.config['SECRET_KEY']
payload = payload.encode('utf-8')
mac = hmac.new(key, payload, sha1)
return mac.hexdigest()
需要加密的值(payload)和secret key,采用sha1加密方式
(3) 利用_cookie_digest()函数生成cookie
def encode_cookie(payload):
return '%s|%s' % (payload, _cookie_digest(payload))
将payload和加密payload产生的值组合一起产生一个新值,可以保存在cookie之中.客户端将cookie传回服务端,程序取出payload,采用同样的方式(_cookie_digest()函数)加密,生成一个值,并与传回来的加密值进行校验,判断是不是同一个用户
(4) 解析cookie
def decode_cookie(cookie):
try:
payload, digest = cookie.rsplit(u'|', 1)
digest = digest.encode("ascii")
except ValueError:
return None
if _cookie_digest(payload, key = 'fsdfaiew') == digest:
return payload
else:
return None
问题: 第3个和第4个函数是不是可以自定义?
(5) 提取将要访问url的参数部分(去掉url中协议:ip:port部分和锚点部分)
def make_next_param(login, current):
'''
:param login: 要跳转的url
:param current: 要分解的url
:return '/home/fg;param=123?query=1' 或者current
'''
login_scheme, login_netloc = urlparse(login)[:2]
current_scheme, current_netloc = urlparse(current)[:2]
# 协议和ip(包括port)均相同 login协议\ip均不存在 login协议存在且相同ip不存在
# login协议不存在且ip存在且相同
# login协议要么不存在 要么存在且和current相同
# login ip要么不存在 要么存在且和current相同
if ((not login_scheme or login_scheme=current_scheme) and
(not login_netloc or login_netloc=current_netloc)):
parsed = urlparse(current)
# 组合: 路径+参数+查询
# http://www.baidu.com/home/fg;param=123?query=1
# --> /home/fg;param=123?query=1
return urlunparse(('', '', parsed[2], parsed[3], parsed[4], ''))
return current
用urlparse.urlpasre可以将url拆分,用urlparse.urlunparse组合url
如: https://www.baidu.com/home/fg;params=123?name=chim&id=1#123
拆分成六部分:
1. scheme: https
2. netloc: www.baidu.com ip+port
3. path: /home/fg
4. params: params=123 不知道用途
5. query: name=chim&id=1
6. fragment: name 锚点 不作用于服务器
注意:在python3.5中用from urllib.parse import urlparse, urlunparse
(6) 生成将要访问的url
def login_url(login_view, next_url=None, next_field="next"):
if login_view.startswith(('https://', 'http://')):
base = login_view
else:
base = url_for(login_view)
if next_url is None:
return base
# base为完整的url
parts = list(urlparse(base))
# parts[4]为url查询部分 如: 'name=chim&id=1'
# url_decode是werkzeug.url中函数: 解析参数 -->md = MultiDict([('name', u'chim'),('id', u'1')])
md = url_decode(parts[4])
# 将'next'加入md, next为'path;params?query'
md[next_field] = make_next_param(base, next_url)
parts[4] = url_encode(md, sort=True)
return urlunparse(parts)
提取将要访问url中的(param+query+flagment)组合部分,为它定义一个’next’名称,加入当前访问url的query部分
(7) 生成安全token
def make_secure_token(*args, **options):
key = options.get('key')
if key is None:
key = current_app.config['SECRET_KEY']
# 如果是unicode字符集,采用utf-8编码成字符串
payload = "\0".join((
s.encode("utf-8") if isinstance(s, unicode) else s) for s in args
)
# 生成token
mac = hmac.new(key, payload, sha1)
# 返回unicode字符串40位
return mac.hexdigest().decode('utf-8')
采用sha1加密方式
注意: 此本版定义了此函数, 但并未使用
(8) 访问认证
def _create_identifier():
# '客户端主机ip地址|user-agent头'
# unicode()将字符串解码为unicode字符集
base = unicode("%s|%s" % (request.remote_addr,
request.headers.get("User-Agent")), 'utf-8')
hsh = md5()
hsh.update(base.encode('utf-8'))
# 二进制
return hsh.digest()
将客户端主机ip地址和User-Agent头信息进行组合.用于确认用户再次访问时,访问地址和用户代理是否和前一次访
(9) 参数部分
COOKIE_NAME = 'remember_token'
COOKIE_DURATION = timedelta(days=365)
LOGIN_MESSAGE = u'please log in access this page'
REFRESH_MESSAGE = u'please reauthenticate to this page'
(10) LoginManager类 init()
class LoginManager(object):
def __init__(self):
# 匿名用户
self.anonymous_user = AnonymousUser
# url视图endpoint或者url,若未登录,重定向
self.login_view = None
# 登录消息
self.login_message = LOGIN_MESSAGE
# 重新认证视图
self.refresh_view = None
# 重新认证消息
self.needs_refresh_message = REFRESH_MESSAGE
# session保护等级basic(默认),strong, None
self.session.protection = 'basic'
# token处理函数,可以用token_loader(self)方法进行自定义
self.token_callback = None
# 用户处理函数(如重数据库中获取用户信息)
# 可以用user_loader(self)方法进行自定义
self.user_callback = None
# 用户认证处理函数,可以用unauthorized_handler(self)方法进行自定义
# 与user_loader(self)不同的是user_loader(self)是获取用户信息,
# unauthorized_handler(self)是在获取用户信息之后,对用户身份的标识.
# 如用户存在,标识为已认证用户;用户不存在,标识为匿名用户
self.unauthorized_callback = None
# 与self.user_callback()类似,只是它是对即将访问资源的处理
# 如资源存在,如何处理,资源不存在如何处理
$ 可以用needs_refresh_handler(self)方法进行自定义
self.needs_refresh_callback = None
(11) 自定义获取用户信息方法
def user_loader(self, callback):
self.user_callback = callback
返回获取用户处理的方法,可以当作装饰器使用
# 自定义用户获取方法
@lg.user_loader
def load_user(id):
return User.query.filter_by(id=id).first()
(12) 自定义处理cookie中token方法
def token_loader(self, callback):
self.token_callback = callback
可以作为装饰器使用,目的在于获取cookie之中的用户信息
(13) 自定义用户未认证时的处理方法
def unauthorized_handler(self, callback):
self.unauthorized_callback = callback
可以作为装饰器使用
(14) 自定义访问资源时的处理方法
def needs_refresh_handler(self, callback):
self.needs_refresh_callback = callback
可以作为装饰器使用
(15) 初始化方法
def setup_app(self, app, add_context_processor=True):
app.login_manager = self
app.before_request(self._load_user)
app.after_request(self._update_remember_cookie)
if add_context_processor:
app.context_processor(_user_context_processor)
初始化Flask()对象,可以清晰知道flask-login做了什么.主要包括:
1. 在请求处理之前,对用户进行认证
2. 在请求完成之后,设置cookie信息
3. 将_user_context_processor加入至上下文变量之中,也就是current_user
注意:flask-login后面版本改为了init_app(self)
(16) 认证处理
def unauthorized(self):
# 发送信号
user_unauthorized.send(current_app._get_current_object())
if self.unauthorized_callback:
return self.unauthorized_callback()
# 用户未认证且登录视图不存在时,401错误
if not self.login_view:
abort(401)
if self.login_message:
flash(self.login_message)
# login_view是一个用户未认证要跳转的url(可能不完整), request.url是当前请求的url(完整)
return redirect(login_url(self.login_view, request.url))
主要与状态码401(未登陆)的处理有关
(17) 资源获取处理
def needs_refresh(self):
user_needs_reflesh.send(current_app._get_current_object())
if self.needs_refresh_callback:
return self.needs_refresh_callback()
if not self.refresh_view:
abort(403)
flash(self.needs_refresh_message)
return redirect(login_url(self.refresh_view, request.url))
主要与状态码403(Forbidden)的处理有关
(18) 加载用户
def _load_user(self):
config = current_app.config
# 获取session安全等级
if config.get('SESSION_PROTECTION', self.session_protection):
deleted = self._session_protection()
# deleted为True,重新加载user
if deleted:
self.reload_user()
return
# 若设置了remember cookie, session未设置,将cookie里user的id移至session中
cookie_name = config.get('REMEMBER_COOKIE_NAME', COOKIE_NAME)
if cookie_name:
self._load_from_cookie(request.cookie[cookie_name])
else:
# 重新加载user
self.reload_user()
加载用户的方式有两种:
1. 从session中获取,但session保护等级有限制
2. session加载失败,从cookie中获取
两者最终都会reload_user(self)(在当前app的栈中进行处理)
(19) session保护机制处理
def _session_protection(self):
# 从session中获取用户对象
sess = session._get_current_object()
# 访问认证 '访问地址|user-agent头'
ident = _create_identifier()
# 若'_id'不在sess中, 将其保存在其中,session的基础是访问的remote_addr和代理相同
if '_id' not in sess:
sess['_id'] = ident
# sess中存在'_id',但和当前的'访问地址|user-agent'不同, 获取当前用户
elif ident != sess['_id']:
app = current_app._get_current_object()
mode = app.config.get['SESSION_PROTECTION', self.session_protection]
# 若session保护等级为basic(sess.permanent保存什么?), session继续使用(换了代理和网址也不要紧)
if mode == 'basic' or sess.permanent:
sess['_fresh'] = False
# ? send有什么作用
session_protected.send(app)
return False
# 若session保护等级为strong, 删除当前保存的session,不允许保存不同地址的session,换地址则更新session
elif mode == 'strong':
sess.clear()
sess['remember'] = 'clear'
session_protected.send(app)
return True
return False
返回True或者False,并在session中记录’_id’, ‘remember’, ‘_fresh’的状态
问题: 为什么要用session._get_current_object(),而不是之际存在flask.session中.在其他地方对这三个变量的取值是直接从flask.session中获取的?
(20) 重新加载用户
def reload_user(self):
ctx = _request_ctx_stack.top
user_id = session.get('user_id', None)
# 若session中不存在user_id,设置用户为匿名用户
if user_id is None:
ctx.user = self.anonymous_user
else:
# 加载用户
user = self.user_callback(user_id)
if user is None:
logout_user()
else:
ctx.user = user
重新加载用户, 将user放入上下文之中.如果是这样, ctx对_request_ctx_stack.top引用是地址的引用,ctx是别名
(21)从cookie中加载用户
def _load_from_cookie(self, cookie):
# token_callback是什么?
if self.token_callback:
user = self.token_callback(cookie)
# 用户存在
if user is not None:
session['user_id'] = user.get_id()
session['_fresh'] = False
_request_ctx_stack.top.user = user
# 用户不存在,reload_user
else:
self.reload_user()
else:
# 解析cookie, 返回用户标识
user_id = decode_cookie(cookie)
if user_id is not None:
session['user_id'] = user_id
# '_fresh'含义是什么?
session['_fresh'] = False
self.reload_user()
从cookie中取出用户,用reload_user保存用户至上下文中
(22) 更新remember cookie
def _update_remember_cookie(self, response):
# 在session中取出并删除remember的值
operation = session.pop('remember', 'None')
# remember me 操作(一次性的)
if operation == 'set' and 'user_id' in session:
self._set_cookie(response)
elif operation == 'clear':
self._clear_cookie(response)
return response
(23) remember me 的set cookie操作
def _set_cookie(self, response):
config = current_app.config
cookie_name = config.get('REMEMBER_COOKIE_NAME', COOKIE_NAME)
duration = config.get('REMEMBER_COOKIE_DURATION', COOKIE_DURATION)
# 主机信息
domain = config.get('REMEMBER_COOKIE_DOMAIN', None)
if self.token_callback:
data = current_user.get_auth_token()
else:
data = encode_cookie(str(session['user_id']))
expires = datetime.now() + duration
response.set_cookie(cookie_name, expires=expires, domain=domain)
(24) remember me 的clear cookie操作
def _clear_cookie(self, response):
config = current_app.config
cookie_name = config.get('REMEMBER_COOKIE_NAME', COOKIE_NAME)
domin = config.get('REMEMBER_COOKIE_DOMAIN', None)
response.delete_cookie(cookie_name, domain=domin)
(25) current_user定义
current_user = LocalProxy(lambda: _request_ctx_stack.top.user)
def _user_context_processor():
return dict(current_user=_get_user())
问题: 为什么要用lambda?不用好像也可以
(26) 获取cookie的状态
def login_fresh():
return session.get('_fresh', False)
_fresh–用户新登录,返回True
(27)用户登录
def login_user(user, remember=False, force=False):
if (not force) and (not user.is_active()):
return False
user_id = user.get_id()
session['user_id'] = user_id
session['_fresh'] = True
if remember:
session['remember'] = 'set'
app = current_app._get_current_object()
current_app.login_manager.reload_user()
user_logged_in.send(current_app._get_current_object(), user=_get_user())
return True
问题: force的正确用法是什么?为什么要设置这个值?
(28)用户登出
def logout_user():
if 'user_id' in session:
del session['user_id']
if '_fresh' in session:
del session['_fresh']
cookie_name = current_app.config.get['REMEMBER_COOKIE_NAME', COOKIE_NAME]
# request.cookies为字典
if cookie_name in request.cookies:
# remember的值决定cookie的存在与否
session['remember'] = 'clear'
user = _get_user()
if user and (not user.is_anonymous()):
user_logged_out.send(current_app._get_current_object(), user=user)
current_app.login_manager.reload_user()
return True
(29) 用户认证后设置_fresh值,并发送信号
def comfirm_login():
# 最新登录的用户,_fresh设置为True,用cookie加载,_fresh为False
session['_fresh'] = True
user_login_confirmed.send(current_app._get_current_object())
(30) 登录限制装饰器
def login_required(fn)
@wraps(fn)
def decorated_view(*args, **kwargs):
# is_authenticated()在用户类中定义,默认返回True/False
if not current_user.is_authenticated():
# 重定向值login_view
return current_app.login_manager.unauthorized()
return fn(*args, **kwargs)
return decorated_view
(31)登录限制+登录信息限制
def fresh_login_required(fn):
@wraps(fn)
def decorated_view(*args, **kwargs):
# 用户未验证
if not current_user.is_authenticated():
# 重定向至login_view
return current_app.login_manager.unauthorized()
# 用户验证且获取session中'_fresh'的值, 如果不存在或者不新鲜
elif not login_fresh():
# 更新消息,重定向至login_view
return current_app.login_manager.needs_refresh()
return fn(*args, **kwargs)
return decorated_view
在login_required()基础上,验证_fresh值,只允许最新登录的用户访问
(32)定义用户类
class UserMixin(object):
def is_active(self):
return True
def is_authenticated(self):
return True
def is_anonymous(self):
return False
def get_id(self):
try:
return unicode(self.id)
except AttributeError:
raise NotImplementedError('NO `id` attribute -- override get_id')
flask-login提供给用户的行为方法
(33) 匿名用户类
class AnonymousUser(object):
def is_authenticated(self):
return False
def is_active(self):
return False
def is_anonymous(self):
return True
def get_id(self):
return None
(34) 信号处理
(待)