Principle of user authentication

Before we learn how to implement user authentication using Flask, we need to understand how user authentication works. Suppose we want to implement user authentication ourselves now, what do we need to do?

  1. First, the user needs to be able to enter a user name and password, so you need web pages and forms for user input and submission.
  2. After the user submits the user name and password, we need to check whether the user name and password are correct. In order to check, we must first have a place to store the user name and password in the system. Most background systems will store the user name and password through the database, but in fact, we can also simply store it in the file. (For simplicity, this article stores user information in a JSON file.)
  3. After login, we need to maintain the user login state, so that the user can determine whether the user is logged in and has the permission to access the changed web page when accessing a specific page. This requires maintaining a session to hold the user’s login status and user information.
  4. From the third step we can see, if our web page need permissions to protect, so when the request comes, we’ll first need to check the user’s information, such as whether to have logged in, whether have permissions, if check through, so at the time of the response will be a corresponding web page response to the request of the user, but if you don’t through the inspection, Then you need to return an error message.
  5. In the second step, we know to store the user name and password, but if you simply store the user name and password in plain text, it is easy to be stolen by “intentional people”, resulting in user information leakage, so we should actually encrypt the user information, especially the password before storing it.
  6. The user to log out

The login process is implemented through Flask and the corresponding plug-ins

The following describes how to implement the entire login process using the Flask framework and the corresponding plug-ins:

  • flask-wtf
  • wtf
  • werkzeug
  • flask_login

Flask-wtf and WTF are used for form functionality

Flask-wtf does some encapsulation for WTF, but there are some things that need to be used directly with WTF, such as StringField, etc. Flask-wtf and WTF are mainly used to establish the correspondence between elements in HTML and classes in Python, and control elements in HTML by manipulating corresponding classes and objects in Python code. We need to use flask-wtf and WTF in Python code to define the front page form (essentially a form class) and pass the corresponding form object as an argument to render_template. The Jinja template engine then renders the corresponding template as HTML text and returns it to the user as an HTTP response.

Example code for defining a form class:

# forms.py from flask_wtf import FlaskForm from wtforms import StringField, BooleanField, PasswordField from WtForms. Validators import DataRequired # Define forms that need to inherit from FlaskForm Class LoginForm(FlaskForm): When the field is initialized, The first parameter is to set the label property username = StringField('User Name', validators=[DataRequired()]) Password = PasswordField('Password', validators=[DataRequired()]) remember_me = BooleanField('remember me', default=False)Copy the code

In WTF, each field represents an HTML element. For example, StringField represents the <input type=”text”> element. Of course, WTF fields also define some special functions, such as validators. Refer to the WTF tutorial for details. The corresponding HTML template might look like login.html:

{% extends "layout.html" %} <html> <head> <title>Login Page</title> </head> <body> <form action="{{ url_for("login") }}"  method="POST"> <p> User Name:<br> <input type="text" name="username" /><br> </p> <p> Password:</br> <input type="password" name="password" /><br> </p> <p> <input type="checkbox" name="remember_me"/>Remember Me </p> {{ form.csrf_token }} </form> </body> </html>Copy the code

Here {{form.csrf_token}} can also be replaced with {{form.hidden_tag()}}

The Jinja template engine converts objects and attributes to the corresponding HTML tag, the corresponding template, as shown in login.html:

<! -- The template syntax should follow Jinja syntax --> <! -- extend from base layout --> {% extends "base.html" %} {% block content %} <h1>Sign In</h1> <form action="{{ url_for("login") }}" method="post" name="login"> {{ form.csrf_token }} <p> {{ form.username.label }}<br> {{ form.username(size=80) }}<br> </p> <p> {{ form.password.label }}<br> <! We can pass the attributes of the input tag, </p> <p>{{form.remember_me}} Remember Me</p> <p><input type="submit"  value="Sign In"></p> </form> {% endblock %}Copy the code

Now we need to define the corresponding route in the View and present the corresponding login interface to the user. For simplicity, place the view’s associated route definition in the main program

# app.py
@app.route('/login')
def login():
    form = LoginForm()
    return render_template('login.html', title="Sign In", form=form)
Copy the code

For simplicity, when a user requests a ‘/login’ route, the login. HTML page is returned directly. Note that the HTML page is converted from the corresponding template by the Jinja template engine. At this point, if we integrate the above code into flask, we should see the login screen, so how do we store it after the user submits it? Here we temporarily do not use the database such a complex tool storage, first simply save as a file. Now let’s see how to store it.

Encryption and storage

We can start by defining a User class that handles user-specific operations, including storage and validation.

# models.py

from werkzeug.security import generate_password_hash
from werkzeug.security import check_password_hash
from flask_login import UserMixin
import json
import uuid

# define profile.json constant, the file is used to
# save user name and password_hash
PROFILE_FILE = "profiles.json"

class User(UserMixin):
    def __init__(self, username):
        self.username = username
        self.id = self.get_id()

    @property
    def password(self):
        raise AttributeError('password is not a readable attribute')

    @password.setter
    def password(self, password):
        """save user name, id and password hash to json file"""
        self.password_hash = generate_password_hash(password)
        with open(PROFILE_FILE, 'w+') as f:
            try:
                profiles = json.load(f)
            except ValueError:
                profiles = {}
            profiles[self.username] = [self.password_hash,
                                       self.id]
            f.write(json.dumps(profiles))

    def verify_password(self, password):
        password_hash = self.get_password_hash()
        if password_hash is None:
            return False
        return check_password_hash(self.password_hash, password)

    def get_password_hash(self):
        """try to get password hash from file.

        :return password_hash: if the there is corresponding user in
                the file, return password hash.
                None: if there is no corresponding user, return None.
        """
        try:
            with open(PROFILE_FILE) as f:
                user_profiles = json.load(f)
                user_info = user_profiles.get(self.username, None)
                if user_info is not None:
                    return user_info[0]
        except IOError:
            return None
        except ValueError:
            return None
        return None

    def get_id(self):
        """get user id from profile file, if not exist, it will
        generate a uuid for the user.
        """
        if self.username is not None:
            try:
                with open(PROFILE_FILE) as f:
                    user_profiles = json.load(f)
                    if self.username in user_profiles:
                        return user_profiles[self.username][1]
            except IOError:
                pass
            except ValueError:
                pass
        return unicode(uuid.uuid4())

    @staticmethod
    def get(user_id):
        """try to return user_id corresponding User object.
        This method is used by load_user callback function
        """
        if not user_id:
            return None
        try:
            with open(PROFILE_FILE) as f:
                user_profiles = json.load(f)
                for user_name, profile in user_profiles.iteritems():
                    if profile[1] == user_id:
                        return User(user_name)
        except:
            return None
        return None
Copy the code
  • The User class needs to inherit the UserMixin class from flask-Login for User session management.
  • Here we are directly storing user information in a JSON file called “profiles.json”
  • Instead of storing the password directly, we store the encrypted hash value. In this case, we use the generate_password_hash function from the WerkZeug. Security package. So it’s pretty safe. It’s good enough for general purposes.
  • To verify the password, use the check_password_hash function in the Werkzeug. Security package to verify the password
  • Get_id is a method that exists in the UserMixin class, and here we need to overwrite the method. If there is no corresponding user ID in the JSON file, you can use uuid.uuid4() to generate a unique USER ID

So now that we’ve done steps 2 and 5, let’s look at step 3, how do WE maintain a session

Maintaining User Sessions

Let’s take a look at the code and put it in app.py

from forms import LoginForm from flask_wtf.csrf import CsrfProtect from model import User from flask_login import login_user, login_required from flask_login import LoginManager, current_user from flask_login import logout_user app = Flask(__name__) app.secret_key = os.urandom(24) # use login manager to manage session login_manager = LoginManager() login_manager.session_protection = 'strong' Login_manager. login_view = 'login' login_manager.init_app(app=app) # User_loader def load_user(user_id) def load_user(user_id); return User.get(user_id) # csrf protection csrf = CsrfProtect() csrf.init_app(app) @app.route('/login') def login(): form = LoginForm() if form.validate_on_submit(): user_name = request.form.get('username', None) password = request.form.get('password', None) remember_me = request.form.get('remember_me', False) user = User(user_name) if user.verify_password(password): login_user(user, remember=remember_me) return redirect(request.args.get('next') or url_for('main')) return render_template('login.html', title="Sign In", form=form)Copy the code
  • The LoginManager object is the key to maintaining the user’s session.
  • The load_user callback function must be implemented to reload the user object
  • After the password is verified, the login_user() function is used to login to the user, and the user’s status in the session is the login state

Protected Web page

To protect a particular web page, simply add a decorator to a particular route, as follows

# app.py

# ...
@app.route('/')
@app.route('/main')
@login_required
def main():
    return render_template(
        'main.html', username=current_user.username)
# ...
Copy the code
  • Current_user holds information about the current User, which is essentially a User object, so we call its properties directly. For example, if we want to pass a username parameter to the template, we can use current_user.username directly
  • Use @login_required to identify the log-in user required to change the route. Non-log-in users are redirected to the ‘/login’ route (as specified by the login_manager.login_view = ‘login’ statement).

The user to log out

# app.py

# ...
@app.route('/logout')
@login_required
def logout():
    logout_user()
    return redirect(url_for('login'))
# ...
Copy the code

At this point, we have achieved a complete login and logout process.

What’s more, you’ll need a few extra things to do, such as sending confirmation emails, password resetting, and hierarchical management, all of which are done by flask and its add-on.