This article is a translation of the fifth part of the new edition of Miguel Greenberg’s textbook, the issue of which the author plans to complete in May 2018. The previous translation has long since lost its relevance.
This is the fifth edition of the Flask Mega-Tutorial series, in which I will tell you how to create a user login subsystem.
For reference, below is a list of articles in this series.
Note 1: If you are looking for old versions of this course, this is here .
Note 2: If suddenly you would like to speak in support of my (Miguel) work on this blog, or simply do not have the patience to wait for a week of the article, I (Miguel Greenberg) offer a full version of this guide a packed e-book or video. For more information, visit learn.miguelgrinberg.com .
In Chapter 3, you learned how to create a user login form, and in Chapter 4, you learned how to work with a database. In this chapter, you will learn how to combine topics from these two chapters to create a simple user login system.
GitHub links for this chapter: Browse , Zip , Diff .
In chapter 4, the user-defined model was assigned the password_hash field, which is not yet used. The purpose of this field is to save the user password hash that will be used to verify the password entered by the user during the registration process. Password hashing is a complex topic that should be left to security experts, but there are several easy-to-use libraries that implement all of this logic so that it can be called up from an application.
One of the packages that implement password hashing is Werkzeug , which you may have seen in the pip output when installing Flask. Since this is a dependency, Werkzeug is already installed in your virtual environment. The following Python shell session demonstrates how to hash a password:
>>> from werkzeug.security import generate_password_hash >>> hash = generate_password_hash('foobar') >>> hash 'pbkdf2:sha256:50000$vT9fkZM8$04dfa35c6476acf7e788a1b5b3c35e217c78dc04539d295f011f01f18cd2175f' In this example, the foobar password is converted to a long coded string through a series of cryptographic operations that do not have any inverse operation, which means that the person receiving the hashed password cannot use it to obtain the original password. As an additional measure, if you have the same password several times, you will get different results, so it is not possible to determine whether two users have the same password by viewing their hashes.
The verification process is performed with the second function from Werkzeug as follows:
>>> from werkzeug.security import check_password_hash >>> check_password_hash(hash, 'foobar') True >>> check_password_hash(hash, 'barfoo') False The verification function accepts the hash code of the password that was previously generated and the password entered by the user during login. The function returns the value True if the password provided by the user matches the hash, otherwise False .
All password hashing logic can be implemented as two new methods in the user model:
app/models.py : Password Hashing and Verification from werkzeug.security import generate_password_hash, check_password_hash # ... class User(db.Model): # ... def set_password(self, password): self.password_hash = generate_password_hash(password) def check_password(self, password): return check_password_hash(self.password_hash, password) Using these two methods, a user object can now perform a secure password check without having to store the original passwords. Here is an example of using these new methods:
>>> u = User(username='susan', email='susan@example.com') >>> u.set_password('mypassword') >>> u.check_password('anotherpassword') False >>> u.check_password('mypassword') True In this chapter, I will introduce you to the very popular Flask extension called Flask-Login. This extension controls the user's login status, so that, for example, users can log in to the application, and then go to different pages while the application “remembers” that the user is logged in. It also provides “remember me” functionality that allows users to stay logged in even after the browser window is closed. To be ready for this chapter, you can start by installing Flask-Login in your virtual environment:
(venv) $ pip install flask-login As with other extensions, Flask-Login must be created and initialized immediately after the application instance in app / init .py. So this extension is initialized:
app/__init__.py : Flask-Login initialization # ... from flask_login import LoginManager app = Flask(__name__) # ... login = LoginManager(app) # ... The Flask-Login extension works with the user model of the application and expects that it will implement certain properties and methods. This approach is good because as long as these necessary elements are added to the model, Flask-Login has no other requirements, so, for example, it can work with custom models based on any database system.
Below are the four required items:
is_authenticated : a property that is True if the user has valid credentials or False otherwise.is_active : property that returns True if the user account is active or False otherwise.is_anonymous : a property that returns False for ordinary users, and True if the user is anonymous.get_id() : method that returns the unique identifier of the user as a string (unicode if Python 2 is used).I can easily implement all four, but since the implementations are fairly generic, Flask-Login provides the mixin class UserMixin , which includes common implementations that are suitable for most classes of custom models. Here is how the mixin class is added to the model:
app/models.py : Flask-Login user mixin class # ... from flask_login import UserMixin class User(UserMixin, db.Model): # ... Flask-Login keeps track of a registered user by storing his unique identifier in a Flask user session assigned to each user who connects to the application. Each time a logged in user goes to a new page, Flask-Login retrieves the user ID from the session and then loads that user into memory.
Since Flask-Login does not know anything about databases, it needs the help of the application when loading the user. For this reason, the extension expects the application to configure a user loader function that can be called to load a user with an identifier. This feature can be added in the app / models.py module :
app/models.py : Flask-Login user loader function from app import login # ... @login.user_loader def load_user(id): return User.query.get(int(id)) A custom bootloader is registered in Flask-Login using the @login.user_loader decorator. The identifier that Flask-Login passes to the function as an argument will be a string, so for databases using numeric identifiers, you need to convert the string to an integer, as you can see above int(id) .
Let's move on to the login function, which, as you remember, implemented a fake login, which only gave the flash() message. Now that the application has access to the user database and knows how to create and verify password hashes, this browsing function can be completed ( \microblog\app\routes.py ).
app/routes.py : Login view function logic # ... from flask_login import current_user, login_user from app.models import User # ... @app.route('/login', methods=['GET', 'POST']) def login(): if current_user.is_authenticated: return redirect(url_for('index')) form = LoginForm() if form.validate_on_submit(): user = User.query.filter_by(username=form.username.data).first() if user is None or not user.check_password(form.password.data): flash('Invalid username or password') return redirect(url_for('login')) login_user(user, remember=form.remember_me.data) return redirect(url_for('index')) return render_template('login.html', title='Sign In', form=form) The top two lines in the login() function deal with a strange situation. Imagine that you have a user who is logged in and navigates to the URL of your application. Clearly, this is a mistake, so I do not want to allow this. The current_user variable comes from Flask-Login and can be used at any time to get a user object. The value of this variable can be a user object from the database (which Flask-Login reads via the callback of the user loader presented above), or a special anonymous user object if the user has not logged in yet. Remember those properties that Flask requires in a user object? One of them was is_authenticated , which is very useful to check if the user is registered or not. When the user is already logged in, I simply redirect him to the index page.
Instead of calling flash (), which I used earlier, I can now log in to the user's system for real. The first step is to load the user from the database. The username came with a submit form, so I can query the database to find the user.
To do this, I use the filter_by() method of the SQLAlchemy query object. The result of filter_by() is a query that includes only objects that have a matching username. Since I know that there will be only one or zero result, I will complete the query by calling first() , which will return the user object if it exists, or None if it is not. In Chapter 4, you saw that when you call the all() method in a query, the query is executed, and you get a list of all the results that match that query. The first() method is another used way to execute a query when you need only one result.
If I received a match for the username that was provided, I can verify that the password, which also came with the form, is valid. This is done by calling the check_password() method defined above. This will lead to the hash password stored by the user, and determine whether the password entered in the form matches the hash or not. So now I have two possible error conditions: the username may be invalid, or the password may be incorrect for the user. In any of these cases, I scroll through the message and redirect back to the login prompt so that the user can try again.
If the username and password are correct, I call the login_user() function, which comes from Flask-Login. This function will register the user at the time of login, so this means that on any future pages to which the user goes, the current_user variable will be set for this user.
To complete the login process, I simply redirect the newly registered user to the index page.
Obviously, you will need to offer users the ability to exit the application. This can be done using the logout_user() Flask-Login function. Here’s what the exit function looks like:
app/routes.py : Logout view function # ... from flask_login import logout_user # ... @app.route('/logout') def logout(): logout_user() return redirect(url_for('index')) I can make the “ login ” link in the navigation bar automatically switch to the “ logout ” link after the user has logged in. This can be done using a conditional expression in the base.html template:
app/templates/base.html : Conditional login and logout links <div> Microblog: <a href="{{ url_for('index') }}">Home</a> {% if current_user.is_anonymous %} <a href="{{ url_for('login') }}">Login</a> {% else %} <a href="{{ url_for('logout') }}">Logout</a> {% endif %} </div> The is_anonymous property is one of the attributes that Flask-Login adds to user objects through the UserMixin class. The expression current_user.is_anonymous will return True only if the user does not log in.
Flask-Login provides a very useful feature that forces users to register before they can view certain pages of the application. If a user who has not logged in tries to view a secure page, Flask-Login automatically redirects the user to the login form and only after completing the login process, redirects to the page that the user wanted to view.
In order for this function to be implemented, Flask-Login needs to know what a viewing function is that processes logins. This can be added to app / init .py:
# ... login = LoginManager(app) login.login_view = 'login' The “login” value above is the name of the function (or end point) for logging on to the system. In other words, the name you will use in the url_for() call to get the URL.
The Flask-Login method protects the browsing function from anonymous users using a decorator called @login_required . When you add this decorator to the view function under the @app.route decorators from Flask, the function becomes secure and does not allow access to users who are not authenticated. Here's how the decorator can be applied to the application's index view function:
app/routes.py : @login_required decorator from flask_login import login_required @app.route('/') @app.route('/index') @login_required def index(): # ... It remains to implement redirection from a successful login to the page to which the user wanted to access. When a non-logged-in user accesses the viewing function protected by the @login_required decoder, the decorator is about to redirect to the login page, but this redirection will include additional information so that the application can then return to the first page. If the user goes, for example, to / index , the @login_required handler intercepts the request and redirects to /login , but it adds the query string argument to this URL, making the full URL / login? Next = / index . next query string argument is set to the original URL, so the application can use this to redirect after logging in.
Here is a snippet of code that shows how to read and process the next query string argument:
app/routes.py : Redirect to "next" (next) page from flask import request from werkzeug.urls import url_parse @app.route('/login', methods=['GET', 'POST']) def login(): # ... if form.validate_on_submit(): user = User.query.filter_by(username=form.username.data).first() if user is None or not user.check_password(form.password.data): flash('Invalid username or password') return redirect(url_for('login')) login_user(user, remember=form.remember_me.data) next_page = request.args.get('next') if not next_page or url_parse(next_page).netloc != '': next_page = url_for('index') return redirect(next_page) # ... Immediately after the user has logged in, by calling the login_user() function from Flask-Login , you will get the value of the next argument in the query string. Flask contains a request variable containing all the information that the client sent with the request. In particular, the attribute request.args provides the contents of the query string in a friendly dictionary format. In fact, there are three possible cases that must be considered in order to determine where to redirect after a successful login:
next argument, which is set to a relative path (or, in other words, a URL without a part of the domain), then the user is redirected to that URL.next argument, which is set to a full URL that includes the domain name, the user is redirected to the index page.The first and second cases do not require explanation. The third case is to make the application more secure. An attacker can insert a URL to a malicious site in the next argument, so the application redirects only the URL, which ensures that the redirection remains on the same site as the application. To determine if the URL is relative or absolute, I analyze it with the Werkzeug url_parse() function, and then check whether the netloc component is netloc or not.
Do you remember that in Chapter 2 I created a fake user to help me develop the application home page before the user subsystem was created? Well, now the application has real users, so now I can remove the fake ones and start working with real ones. Instead of fake ones, you can use Flask-Login-s current_user in the template:
app/templates/index.html : Passing the current user to the template {% extends "base.html" %} {% block content %} <h1>Hi, {{ current_user.username }}!</h1> {% for post in posts %} <div><p>{{ post.author.username }} says: <b>{{ post.body }}</b></p></div> {% endfor %} {% endblock %} And I can remove the user argument in the view function ( microblog \ app \ routes.py ):
app / routes.py: Do not pass user to template anymore
@app.route('/') @app.route('/index') def index(): # ... return render_template("index.html", title='Home Page', posts=posts) It seems the right time has come to test the input and output operability. Since user registration is still missing, the only way to add a user to the database is through the Python shell, so run the flask shell and enter the following commands to register the user:
>>> u = User(username='susan', email='susan@example.com') >>> u.set_password('cat') >>> db.session.add(u) >>> db.session.commit() If you start the application and try to access http:// localhost:5000/ or http://localhost:5000/index , you will be immediately redirected to the login page. And after completing the login procedure, using the credentials of the user that you added to your database, you will be returned to the original page, in which you will see a personalized greeting.
The last part of the functionality I’m going to build in this chapter is the registration form so that users can register via the web form. Let's start by creating a web form class in app / forms.py :
from flask_wtf import FlaskForm from wtforms import StringField, PasswordField, BooleanField, SubmitField from wtforms.validators import ValidationError, DataRequired, Email, EqualTo from app.models import User # ... class RegistrationForm(FlaskForm): username = StringField('Username', validators=[DataRequired()]) email = StringField('Email', validators=[DataRequired(), Email()]) password = PasswordField('Password', validators=[DataRequired()]) password2 = PasswordField( 'Repeat Password', validators=[DataRequired(), EqualTo('password')]) submit = SubmitField('Register') def validate_username(self, username): user = User.query.filter_by(username=username.data).first() if user is not None: raise ValidationError('Please use a different username.') def validate_email(self, email): user = User.query.filter_by(email=email.data).first() if user is not None: raise ValidationError('Please use a different email address.') In this new form, there are several interesting things related to verification. First, for the email field, I added a second validator after DataRequired, called Email . This is another validator (in the original “stock validator”, i.e. it is more correct to translate it as built-in, standard) that comes with WTForms, which guarantees that what the user enters in this field corresponds to the structure of the email address.
Since this is a registration form, it is usually customary to ask the user to enter the password twice to reduce the risk of typographical errors. For this reason, I have password and password2 . In the second password field, another standard EqualTo validator is used , which verifies that its value is identical to the value for the first password field.
I also added two methods to this class: validate_username() and validate_email() . When you add any methods that match the validate_<_> pattern, WTForms accepts them as custom validators and calls them in addition to the standard validators. In this case, I want to make sure that the username and e-mail address entered by the user are not yet in the database, so these two methods query the database, expecting that there will be no results. In case the result exists, a validation error is triggered by a call to ValidationError . , , , .
-, HTML-, app/templates/register.html . , :
{% extends "base.html" %} {% block content %} <h1>Register</h1> <form action="" method="post"> {{ form.hidden_tag() }} <p> {{ form.username.label }}<br> {{ form.username(size=32) }}<br> {% for error in form.username.errors %} <span style="color: red;">[{{ error }}]</span> {% endfor %} </p> <p> {{ form.email.label }}<br> {{ form.email(size=64) }}<br> {% for error in form.email.errors %} <span style="color: red;">[{{ error }}]</span> {% endfor %} </p> <p> {{ form.password.label }}<br> {{ form.password(size=32) }}<br> {% for error in form.password.errors %} <span style="color: red;">[{{ error }}]</span> {% endfor %} </p> <p> {{ form.password2.label }}<br> {{ form.password2(size=32) }}<br> {% for error in form.password2.errors %} <span style="color: red;">[{{ error }}]</span> {% endfor %} </p> <p>{{ form.submit() }}</p> </form> {% endblock %} , , :
<p>New User? <a href="{{ url_for('register') }}">Click to Register!</a></p> , , , app/routes.py :
from app import db from app.forms import RegistrationForm # ... @app.route('/register', methods=['GET', 'POST']) def register(): if current_user.is_authenticated: return redirect(url_for('index')) form = RegistrationForm() if form.validate_on_submit(): user = User(username=form.username.data, email=form.email.data) user.set_password(form.password.data) db.session.add(user) db.session.commit() flash('Congratulations, you are now a registered user!') return redirect(url_for('login')) return render_template('register.html', title='Register', form=form) , . , . , if validate_on_submit() , , , , .

Source: https://habr.com/ru/post/346346/
All Articles