Flask Login and Principal - current_user is Anonym

2020-06-16 01:47发布

问题:

I'm using Flask Login and Principal for identity and role management. My needs are described straight out of the docs. My code is here:

@identity_loaded.connect_via(app)
def on_identity_loaded(sender, identity):
    # Set the identity user object
    identity.user = current_user

    # Add the UserNeed to the identity
    if hasattr(current_user, 'get_id'):
        print 'current_user ' + str(current_user.get_id())
        identity.provides.add(UserNeed(current_user.get_id))

    # Assuming the User model has a list of roles, update the
    # identity with the roles that the user provides
    if hasattr(current_user, 'roles'):
        if current_user.roles:
            for role in current_user.roles:
                identity.provides.add(RoleNeed(role.name))

In my login code I do this:

identity_changed.send(current_app._get_current_object(),
                                  identity=Identity(user.user_id)

On login, the signal fires as expected. On each subsequent page load, the current_user is anonymous and doesn't have the user id yet all @login_required functions behave as if the user is logged in. Flask login knows that the user is logged in but for some reason the current_user is inconsistent.

Am I missing an essential point of configuration somewhere?

回答1:

I encountered the same problem! The root cause is that both Flask-Login and Flask-Principal are invoked by Flask during the "preprocess" stage of the request in the order that they were registered with your Flask app. If you register Flask-Principal before you register Flask-Login, then @identity_loaded.connect_via(app) will be called before @login_manager.user_loader, and therefore current_user will return the anonymous user.

The Flask-Principal documentation example shows a code excerpt where Flask-Principal is registered before Flask-Login. Tsk tsk! Here's what I ended up doing in my bootstrap:

login_manager = LoginManager()
login_manager.init_app(app)

# ...

principals = Principal(app) # This must be initialized after login_manager.

Then in my users.py view file:

@identity_loaded.connect_via(app)
def on_identity_loaded(sender, identity):
    """ This function is called by Flask-Principal after a user logs in. """

    identity.user = current_user

    if isinstance(current_user, User):
        identity.provides.add(UserNeed(current_user.id))

    for permission in user.permissions:
        # Do permission-y stuff here.

This solved the problem for me.

Edit: I submitted a bug report to the project for the documentation.



回答2:

Thank you for this, here is a related observation in case it is useful. I have been struggling with a similar problem of user roles not persisting in subsequent requests after login. Being a beginner, and playing with different things like flask-user (which I never got working) and settling on a) flask-principal and flask-login and b) flask-navigation instead of flask-nav.

This is so I can 1) easily control which menu items appear based on Principal and 2) avoid having the navigation markup generated outside of a template (as I was always taught to separate logic and presentation and writing a custom menu renderer for flask-nav just to change the surrounding HTML didn't seem right if I want to change the HTML later). I couldn't find a way to iterate through flask-nav objects or add custom properties to nav items whereas in flask-navigation I am creating a custom Item extending flask-navigation's Item to add permissions required.

The challenge I was also trying to solve was having a hierarchy of roles to prevent having to put complex Permission statements in my views (e.g. Admin is also editor, is also user, is also anonymous user etc.) Having googled furiously I couldn't find any concept of hierarchy like that. I also don't particularly want to have multiple roles assigned to a user in my model.

My mistakes were:

  • The above in the load order
  • As I hadn't yet put roles into my models so that there was a many to many relationship between User and Role, in my ignorance I did not realise that Flask-Login needed to load the roles in the @login_manager.user_loader function if the roles wheren't yet in the model. I instead assigned the roles in the login view after login_user(user) before issuing the flask-principal signal.
  • With my different approach the roles got assigned, but forgotten on the next request. This post gave me the clues I was searching for. This is what I ended up doing - all other Principal related code is as in the docs and above.
#CAVEATS - still learning Flask so this may not be the right approach and it is still a W.I.P.
#added a  kind of hierarchy order field to User to drive multiple roles in Permissions
#with this model I only have to assign one role to a user

class Role(db.Model):
    __tablename__ = 'roles'
    id = db.Column(db.Integer(), primary_key=True)
    name = db.Column(db.String(50), unique=True)
    description = db.Column(db.String(200))
    hierarchy_order = db.Column(db.Integer)
    internal = db.Column(db.Boolean) # this is only so I can identify registered users and internal users
    users = db.relationship('User', backref='role',lazy='dynamic')

    def __repr__(self):
        return '<Role: {}>'.format(self.name)

# changed common flask-login example @login_manager.user_loader as follows

@login_manager.user_loader
def load_user(user_id):
    user = User.query.get(int(user_id))
    #work out what roles are below current role
    permissable_roles = Role.query.filter(Role.hierarchy_order<=user.role.hierarchy_order).all()
    user.roles = permissable_roles
    return user

I would dearly love to have this approach as a common convention, but I think I am stuck with having a loop in @login_manager.user_loader which assigns the multiple roles as a hierarchy working down from the assigned role. I hope some of this helps someone struggling with how it all ties together. I still have a lot to learn about where flask stores things and when they are available in different contexts.