web-dev-qa-db-fra.com

Comment passer à travers l'URL "suivante" avec Flask et Flask-login?

La documentation pour Flask-login parle de la gestion d'une URL "suivante". L'idée semble être:

  1. L'utilisateur accède à /secret
  2. L'utilisateur est redirigé vers une page de connexion (par exemple, /login)
  3. Après une connexion réussie, l'utilisateur est redirigé vers /secret

Le seul exemple semi-complet utilisant Flask-login que j'ai trouvé est https://Gist.github.com/bkdinoop/6698956 . C'est utile, mais comme il n'inclut pas les fichiers de modèle HTML, je vois si je peux les recréer comme un exercice d'auto-formation.

Voici une version simplifiée du /secret et /login section:

@app.route("/secret")
@fresh_login_required
def secret():
    return render_template("secret.html")

@app.route("/login", methods=["GET", "POST"])
def login():
    <...login-checking code omitted...>
    if user_is_logged_in:
        flash("Logged in!")
        return redirect(request.args.get("next") or url_for("index"))
    else:
        flash("Sorry, but you could not log in.")
        return render_template("login.html")

Et voici login.html:

<form name="loginform" action="{{ url_for('login') }}" method="POST">
Username: <input type="text" name="username" size="30" /><br />
Password: <input type="password" name="password" size="30" /><br />
<input type="submit" value="Login" /><br />

Maintenant, lorsque l'utilisateur visite /secret, il est redirigé vers /login?next=%2Fsecret. Jusqu'à présent, tout va bien - le paramètre "suivant" est dans la chaîne de requête.

Cependant, lorsque l'utilisateur soumet le formulaire de connexion, il est redirigé vers la page d'index, pas vers le /secret URL.

Je suppose que la raison en est que le paramètre "suivant", qui était disponible sur l'URL entrante, n'est pas incorporé dans le formulaire de connexion et n'est donc pas transmis en tant que variable lorsque le formulaire est traité. Mais quelle est la bonne façon de résoudre cela?

Une solution semble fonctionner - changez le <form> tag de

<form name="loginform" action="{{ url_for('login') }}" method="POST">

à:

<form name="loginform" method="POST">

Avec l'attribut "action" supprimé, le navigateur (au moins Firefox 45 sous Windows) utilise automatiquement l'URL actuelle, ce qui lui fait hériter le ?next=%2Fsecret chaîne de requête, qui l'envoie avec succès au gestionnaire de traitement de formulaire.

Mais est-ce qu'omettre l'attribut du formulaire "action" et laisser le navigateur le remplir dans la bonne solution? Fonctionne-t-il sur tous les navigateurs et plates-formes?

Ou Flask ou Flask-login a-t-il l'intention de gérer cela d'une manière différente?

15
David White

Si vous devez spécifier un attribut action différent dans votre formulaire, vous ne pouvez pas utiliser le paramètre suivant fourni par Flask-Login. Je recommanderais de toute façon de mettre le point de terminaison au lieu de l'url dans le paramètre url car il est plus facile à valider. Voici du code de l'application sur laquelle je travaille, peut-être que cela peut vous aider.

Remplacez le gestionnaire non autorisé de Flask-Login pour utiliser le point de terminaison au lieu de l'URL dans le paramètre suivant:

@login_manager.unauthorized_handler
def handle_needs_login():
    flash("You have to be logged in to access this page.")
    return redirect(url_for('account.login', next=request.endpoint))

Utilisation request.endpoint dans vos propres URL également:

{# login form #}
<form action="{{ url_for('account.login', next=request.endpoint) }}" method="post">
...
</form>

Redirigez vers le point de terminaison dans le paramètre suivant s'il existe et est valide, sinon redirigez vers un repli.

def redirect_dest(fallback):
    dest = request.args.get('next')
    try:
        dest_url = url_for(dest)
    except:
        return redirect(fallback)
    return redirect(dest_url)

@app.route("/login", methods=["GET", "POST"])
def login():
    ...
    if user_is_logged_in:
        flash("Logged in!")
        return redirect_dest(fallback=url_for('general.index'))
    else:
        flash("Sorry, but you could not log in.")
        return render_template("login.html")
20
timakro

@timakro fournit une solution soignée. Si vous souhaitez gérer un lien dynamique tel que

index/<utilisateur>

puis en utilisant url_for (request.endpoint, ** request.view_args) à la place car request.endpoint ne contiendra pas les informations dynamiques disponibles:

 @login_manager.unauthorized_handler
 def handle_needs_login():
     flash("You have to be logged in to access this page.")
     #instead of using request.path to prevent Open Redirect Vulnerability 
     next=url_for(request.endpoint,**request.view_args)
     return redirect(url_for('account.login', next=next))

le code suivant est remplacé par:

def redirect_dest(home):
    dest_url = request.args.get('next')
    if not dest_url:
        dest_url = url_for(home)
    return redirect(dest_url)

@app.route("/login", methods=["GET", "POST"])
def login():
    ...
    if user_is_logged_in:
        flash("Logged in!")
        return redirect_dest(home=anyViewFunctionYouWantToSendUser)
    else:
        flash("Sorry, but you could not log in.")
        return render_template("login.html")
2
Kurumi Tokisaki

Juste au cas où quelqu'un essaie de passer par l'URL " suivant " avec Flask-Login mais avec Flask_Restful, la solution de contournement I que j'ai utilisé passe l'argument " next " de la méthode GET à la méthode POST. Avec flask_restful, le L'argument " next_page " est défini sur " Aucun " après avoir cliqué sur le Bouton de connexion dans login.html

login.html

...
<!-- next_page came from "render_template(next_page=request.args.get('next') ...)" in the get() function -->
<!-- And also from render_template('login.html', next_page=next_page) in the post() function -->
<form action="{{ url_for('login', next=next_page) }}" method="POST" >
    <div class="field">
        <div class="control">
            <input class="input is-large" type="email" name="email" placeholder="Your Email" autofocus="">
        </div>
    </div>

    <div class="field">
        <div class="control">
            <input class="input is-large" type="password" name="password" placeholder="Your Password">
        </div>
    </div>
    <div class="field">
        <label class="checkbox">
            <input type="checkbox" name="remember_me">
            Remember me
        </label>
    </div>
    <button class="button is-block is-info is-large is-fullwidth">Login</button>
</form>
...

auth.py

class Login(Resource):

    def get(self):
        if current_user.is_authenticated:
            return redirect(url_for('home'))

        headers = {'Content-Type': 'text/html'}

#1 -->  # Here I pass the "next_page" to login.html
        return make_response(render_template('login.html', next_page=request.args.get('next')), 200, headers)

    def post(self):
#2 -->  # After the user clicks the login button, I retrieve the next_page saved in the GET method
        next_page = request.args.get('next')

        if current_user.is_authenticated:
            return redirect(url_for('home'))

        # Check if account exists in the db
        existing_account = Account.objects(email=request.form.get('email')).first()

        # Only redirects when the URL is relative, which ensures that the redirect 
        # stays within the same site as the application.
        if existing_account:
            if existing_account.check_password(request.form.get('password')):
                login_user(existing_account, remember=request.form.get('remember_me'))

                if not next_page or url_parse(next_page).netloc != '':
                    return redirect(url_for('home'))

#3 -->          # Here I use the retrieved next_page argument
                return redirect(url_for(next_page))

        # Account not recognized
        flash('Please check your login details and try again.')
        headers = {'Content-Type': 'text/html'}

#4 -->  # I also pass the "next_page" here in case the user-provided data is wrong
        return make_response(render_template('login.html', next_page=next_page), 200, headers)
1
Santiago