flask_login

苏俊友
2023-12-01

1: flask_login安装:

  • pip install flask_login
  • 官方地址: https://flask-login.readthedocs.io/en/latest/#installation

2: 配置flask_login

from flask import Flask
from flask_admin import Admin
from flask_login import LoginManager

app = Flask(__name__)
admin = Admin(app)
# 1:实例化login_manager
login_manager = LoginManager()
# 2:设置密钥
app.config['SECRET_KEY'] = '234567890'
# 3:关联app
login_manager.init_app(app)

app.run()

3: 如何工作:

  • 你需要提供一个用户的回调函数。这个回调函数被用来从session中获取UserID,根据这个UserID获取User对象。

    @login_manager.user_loader
    def load_user(user_id):
        return User.get(user_id)
    
  • 注意: 如果这个USERID不存在, 你需要返回一个None, 不要抛出异常。

4: 用户类

  • 用户类中你可以实现下面的方法和属性。
  • 1: is_authenticated属性:
    • 授权用户
  • 2: is_active:
    • 被激活的账户: 除了身份验证成功,还必须是没有被暂停的账户,也不是程序黑名单中的账户。
  • 3: is_annoymous:
    • 匿名用户
  • 4: get_id:
    • 返回用户的唯一标识。
  • 5: UserMixin类都存在了这些属性和方法,我们可以继承来实现。

5: 登录案例:

  • 身份验证通过之后,我们可以通过login_user功能登录。
  • 注意, 必须验证重定向的安全性,否则容容易收到重定向攻击。
@app.route('/login', methods=['GET', 'POST'])
def login():
    # 获取表单
    form = LoginForm()
    # 如果校验成功
    if form.validate_on_submit():
        # 进行登录
        login_user(user)
		# 刷新
        flask.flash('Logged in successfully.')
		# 获取下一个链接
        next = flask.request.args.get('next')
        # 判断链接安全性
        if not is_safe_url(next):
            return flask.abort(400)
		# 重定向到下个页面。
        return flask.redirect(next or flask.url_for('index'))
    return flask.render_template('login.html', form=form)
  • 模板中也可以使用该用户:

    {% if current_user.is_authenticated %}
      Hi {{ current_user.name }}!
    {% endif %}
    
  • 用户需要登录的视图,采用装饰器登录:

    @app.route("/settings")
    @login_required
    def settings():
        pass
    
  • 用户注销时:

    @app.route("/logout")
    @login_required
    def logout():
        logout_user()
        return redirect(somewhere)
    

6: 未登录访问:

  • 1: 当用户没有登录的时候访问视图,Flask_login会发送一个消息并将他们重定向到视图。

    login_manager.login_view = "users.login"
    
  • 2: 我们也可以自定义消息内容:

    login_manager.login_message = u"Bonvolu ensaluti por uzi tiun paĝon."
    
  • 3: 自定义消息的级别:

    login_manager.login_message_category = "info"
    
  • 4: 如果想要自定义:

    @login_manager.unauthorized_handler
    def unauthorized():
        # do stuff
        return a_response
    

7: 请求加载器自定义登录:

  • 有时您希望在不使用 cookie 的情况下登录用户,例如使用标头值或作为查询参数传递的 api 键。在这些情况下,您应该使用request_loader回调。此回调的行为应与您的user_loader回调相同,只是它接受 Flask 请求而不是 user_id。
@login_manager.request_loader
def load_user_from_request(request):

    # 获取请求的键 
    api_key = request.args.get('api_key')
    # 如果有获取该用户,存在该用户则返回。
    if api_key:
        user = User.query.filter_by(api_key=api_key).first()
        if user:
            return user

    # 获取认证密钥
    api_key = request.headers.get('Authorization')
    if api_key:
        api_key = api_key.replace('Basic ', '', 1)
        try:
            # 解码
            api_key = base64.b64decode(api_key)
        except TypeError:
            pass
       	# 根据密钥获取用户
        user = User.query.filter_by(api_key=api_key).first()
        if user:
            return user

    return None

8: 匿名用户:

  • 默认情况下,当用户未实际登录时,current_user设置为一个AnonymousUserMixin对象。

  • 如果您对匿名用户有自定义要求(例如,他们需要有一个权限字段),您可以提供一个可调用的(类或工厂函数)来创建匿名用户。

    login_manager.anonymous_user = MyAnonymousUser
    

9: 记住用户:

  • login_user(remember = True)

10: FLASK_Login原理解析:

  • 首次登录的时候,如果认证成功,首先会将用户id, Sessionid等信息存储到Session中。并将用户对象存入request.context中。

  • login_user源码解析:

    def login_user(user, remember=False, force=False, fresh=True):
        
        if not force and not user.is_active:
            return False
    	# 从user对象中获取用户的ID
        user_id = getattr(user, current_app.login_manager.id_attribute)()
        # 将用户信息写入session中
        session['user_id'] = user_id
        session['_fresh'] = fresh
        session['_id'] = current_app.login_manager._session_identifier_generator()
        if remember:
            session['remember'] = 'set'
    	# 将user对象存储进当前的request context
        _request_ctx_stack.top.user = user
        # 通过send来发射此signal,当注册监听此signal的回调函数收到此signal之后就会执行函数。
        user_logged_in.send(current_app._get_current_object(), user=_get_user())
        return True
    
  • 非第一次登录: 首先携带session信息来请求,先判断是否在session中能够拿到用户ID, 如果能拿到,根据ID看看是否能查询到用户,如果能则授权成功,返回处理后的response对象。

    • 1: 从session中获取用户的ID
    • 2: 当用户的请求访问的是受登录保护的路由时,就要通过用户ID重新load user,如果load user失败则进入鉴权失败处理流程,如果成功,则允许正常处理请求。
  • 视图登录保护:

    @app.route('/')
    @app.route('/main')
    @login_required
    def main():
        return render_template(
            'main.html', username=current_user.username)
    
  • 分析@login_required:

    # flask_login/utils.py
    def login_required(func):
        @wraps(func)
        def decorated_view(*args, **kwargs):
            # 如果request method为例外method,即在EXEMPT_METHODS中的method,可以不必鉴权
            if request.method in EXEMPT_METHODS:
                return func(*args, **kwargs)
    
            # 如果_login_disabled为True则不必鉴权
            elif 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
    
    • 默认情况下只有OPTIONS method在EXEMPT_METHODS set中,而GET、PUT、POST等常见的methods都需要鉴权。

    • _login_disabled默认为False

    • 正常鉴权的关键在于current_user.is_authenticated是否为True,为True则正常处理请求,为False则进入unauthorized处理流程。那么这个current_user到底怎么就能鉴权了?它是怎么来的呢?来看下定义:

      current_user = LocalProxy(lambda: _get_user())
      

      原来current_user是一个LocalProxy对象,其代理的对象需要通过_get_user()来获取,简单来说_get_user()会返回两种用户,一种是正常的用户对象(鉴权成功),一种是anonymous用户对象(鉴权失败)。而正常的用户对象其is_authenticated属性总是为True,相对的anonymous用户对象的is_authenticated属性总是为False。

  • 分析: _get_user():

    # flask_login/utils.py
    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)
    
    • 我们调用_get_user函数时就会直接从request context中获取user对象return getattr(_request_ctx_stack.top, 'user', None)但如果是非首次登陆,当前request context中并没有保存user对象,就需要调用current_app.login_manager._load_user()来去load user对象。
  • 分析: _load_user()

    # flask_login/login_manager.py
    def _load_user(self):
        	
            user_accessed.send(current_app._get_current_object())
            config = current_app.config
            if config.get('SESSION_PROTECTION', self.session_protection):
                deleted = self._session_protection()
                if deleted:
                    return self.reload_user()
    
            is_missing_user_id = 'user_id' not in session
            if is_missing_user_id:
                cookie_name = config.get('REMEMBER_COOKIE_NAME', COOKIE_NAME)
                header_name = config.get('AUTH_HEADER_NAME', AUTH_HEADER_NAME)
                has_cookie = (cookie_name in request.cookies and
                              session.get('remember') != 'clear')
                if has_cookie:
                    return self._load_from_cookie(request.cookies[cookie_name])
                elif self.request_callback:
                    return self._load_from_request(request)
                elif header_name in request.headers:
                    return self._load_from_header(request.headers[header_name])
    
            return self.reload_user()
    
    • _load_user大体的过程是首先检查SESSION_PROTECTION设置,如果SESSION_PROTECTION 为strong或者basic类型,那么就会执行_session_protection()动作,否则不执行此操作。
    • _session_protection在session_id不一致的时候(比如IP变化会导致session id的变化)才真正有用,这时,如果为basic类型或者session permanent为True时,只标注session为非新鲜的(not fresh);而如果为strong,则会删除session中的用户信息,并重新load user,即调用reload_user
    • session permanent为True时,用户退出浏览器不会删除session,其会保留permanent_session_lifetime s(默认是31天),但是当其为False且SESSION_PROTECTION 设为strong时,用户的session就会被删除。
    • 接下来的代码是说当session中没有用户信息时(这里通过是否能获取到user_id来判断),如果有则直接reload_user,如果没有,则有三种方式来load user,一种是通过remember cookie,一种通过request,一种是通过request header,依次尝试。
    • remember cookie是指,当用户勾选’remember me’复选框时,Flask-Login会将用户信息放入到指定的cookie当中,同样也是加密的。这就是为什么当session中没有携带用户信息时,我们可以通过remember cookie来获取用户的信息。
  • 分析: reload_user():

    # flask_login/login_manager.py
    def reload_user(self, user=None):
            ctx = _request_ctx_stack.top
    
            if user is None:
                user_id = session.get('user_id')
                if user_id is None:
                    # 当无法获取到有效的用户id时,就认为是anonymous user
                    ctx.user = self.anonymous_user()
                else:
                    # user callback就是我们通过@login_manager.user_loader装饰的函数,用于获取user object
                    if self.user_callback is None:
                        raise Exception(
                            "No user_loader has been installed for this "
                            "LoginManager. Add one with the "
                            "'LoginManager.user_loader' decorator.")
                    user = self.user_callback(user_id)
                    if user is None:
                        ctx.user = self.anonymous_user()
                    else:
                        ctx.user = user
            else:
                ctx.user = user
    
    • 首先获取user_id,如果获取不到有效的id,就将user设为anonymous_user。
    • 获取到id后,再通过@login_manager.user_loader装饰的函数获取到user对象,如果没有获取到有效的user对象,就认为是anonymous user。
    • 最后将user保存于request context中(无论是正常的用户还是anonymous用户)。
 类似资料:

相关阅读

相关文章

相关问答