[automation operation and maintenance novice village] Flask-2 certification

Posted by webren on Sun, 13 Feb 2022 16:35:03 +0100

[Abstract]

In the last chapter of the flash topic, we mainly explained the routing, exception handling and interface return of Web applications further. Although the code is more robust, it is still far from being used in the production environment. The most critical step is authentication.

Authentication is a very important link in any interactive scenario.

[certification]

You need to have a concept first, that is, authentication is actually two operations:

  1. identity authentication
  2. Permission control

Generally speaking:

  1. First verify whether the user is legal. In Web applications, 401(Unauthorized) is the embodiment of illegal users
  2. Then judge whether the user has the permission to operate. The prompt of no permission in the Web application is 403 (Forbidden).

[flash application]

identity authentication

AK/SK

The simplest way for identity authentication is to give the caller a fixed access_key and secret_key, also known as AK/SK, is very common when the system is called by a third party.

The code implementation is also very simple, as follows:

@app.route("/index")
def index():
    ak = request.headers.get("access_key", "")
    sk = request.headers.get("secret_key", "")
    if ak != "admin" or sk != "admin_secret":
         return "Authentication failed", 401
    # Specific business logic
    pass

In the above code, it is assumed that the caller puts AK/SK in the requested Headers, and our back-end application only allows admin to call. If AK/SK does not match, authentication failure will be returned. If successful, specific business logic can be executed.

I believe some friends should already have an idea, that is to write the authentication logic code in the routing function. Isn't it that every routing function has to write duplicate authentication code? If you don't have this question, you need to reflect. You can take a look at the previous chapters.

It is obviously unreasonable to write duplicate authentication logic for each routing function, so how to optimize it?

Some friends may think that it is not enough to abstract the authentication logic into a separate function and call it every time, as follows:

from flask import Flask

def permission():
    ak = request.headers.get("access_key", "")
    sk = request.headers.get("secret_key", "")
    if ak == "admin" and sk == "admin_secret":
         return True
    return False

@app.route("/index")
def index():
    if not permission():
          return "Authentication failed", 401
    # Specific business logic
    pass

Although the above code looks much simpler on the surface, it still does not change the fact that the authentication logic is coupled with the business logic.

Here you can change your thinking. If a routing function represents a business logic, if you need to authenticate before executing the business logic, is that equivalent to authenticating before calling the routing function?

In this way, the essence of the problem becomes to do a series of operations before calling a function. If it is legal, call the function. If it is not legal, do not call it. It sounds like it's all about the function of the decorator (if you don't know about the decorator, it's strongly recommended to read it first [automation operation and maintenance part] - Python decorator ). The code is modified as follows:

from functools import wraps
from flask import Flask, request

app = Flask(__name__)

def permission(func):
    @wraps(func)
    def inner():
        ak = request.headers.get("access_key", "")
    		sk = request.headers.get("secret_key", "")
    		if ak == "admin" and sk == "admin_secret":
            return "Authentication failed", 401
        return func()
    return inner
  
@app.route("/index")
@permission
def index():
    # Specific business logic
    pass
register

Now the fixed AK/SK can be verified. The next step is to consider whether users can achieve self-service access to AK/SK through registration. In fact, it is equivalent to the function of registration. Below, the function of registration is realized through user name and password.

However, at present, the database has not been introduced into the back-end application, so the user information can be recorded through JSON file for the time being. When the user calls the registration interface, the username/password passed by the user can be recorded into the JSON file. The next time, the user can judge whether the user is legal by retrieving the file. The code is as follows:

import os
import json
from functools import wraps
from hashlib import md5
from flask import Flask, request

app = Flask(__name__)
ACCOUNTS_FILE = os.path.join(os.path.dirname(os.path.abspath(__file__)), "accounts.json")


def permission(func):
    @wraps(func)
    def inner():
        username = request.form.get("username")
        password = request.form.get("password")
        with open("accounts.json", "r+") as f:
            accounts = json.load(f)
        usernames = [account["username"] for account in accounts]
        if username not in usernames:  # Judge whether the user already exists
            return {"data": None, "status_code": "NotFound", "message": "username is not exists"}
        for account in accounts:
            if account["username"] == username:
                if md5(password.encode()).hexdigest() != account["password"]:  # Judge whether the user name and password are consistent
                    return {"data": None, "status_code": "Unauthorized", "message": "password is not correct"}
                return func()
    return inner


@app.route("/register", methods=["POST"])
def register():
    """Registered user information"""
    username = request.form.get("username")
    password = request.form.get("password")
    if not username or not password:  # Judge the parameters entered by the user
        return {"data": None, "status_code": "InvalidParams", "message": "must have username and password"}
    if not os.path.exists(ACCOUNTS_FILE):  # Determine whether the specified file exists
        return {"data": None, "status_code": "NotFound", "message": "not found accounts file"}
    with open("accounts.json", "r+") as f:
        accounts = json.load(f)
    for account in accounts:
        if account["username"] == username:  # Judge whether the user already exists
            return {"data": None, "status_code": "Duplicated", "message": "username is already exists"}
    accounts.append({"username": username, "password": md5(password.encode()).hexdigest()})
    with open("accounts.json", "w") as f:
        json.dump(accounts, f)
    return {"data": username, "status_code": "OK", "message": "register username successfully"}


@app.route("/index")
@permission
def index():
    # Specific business logic
    return "success"


if __name__ == '__main__':
    app.run()

The above code specifies a file of accounts to store user information json, you need to initialize its contents first. If you don't initialize the file, you need to start json An exception will be thrown when loading (), indicating that the file content is not a legal json. The initialization is as follows:

// accounts.json file
[]

First through OS path. abspath(__file__) Get the absolute path where the current startup file is located, and then use OS path. dirname(os.path.abspath(__file__)) Get the directory of the absolute path, and finally through OS path. Join() connects the directory to account JSON file names are combined to get the absolute path of the file.

Here, our user information is stored in an array. The approximate model is as follows:

[
  {"username": "", "password": ""},
  {"username": "", "password": ""}
]

At first glance, the registration function is relatively simple, but in practice, there are still many exceptions to be judged. A variety of predictable exceptions are handled in advance in the routing function of the registered user, and error information is returned. In addition, in the stage of saving the password, special processing is done. In principle, even the application party has no right to know the user's real password, so it is necessary to hash the password when saving the user information, as follows:

accounts.append({"username": username, "password": md5(password.encode()).hexdigest()})

The hash of the password is also used for comparison during verification, as follows:

if md5(password.encode()).hexdigest() != account["password"]

Finally, the interface is called through postman as follows:

Sign in

Now users can register and save their user name and password in the back-end application, so that they can pass the authentication every time they carry the user name and password information in the request.

However, if the user can log in, the user can log in only once. Within the effective time of login, the user can make normal request access, and there is no need to pass the user name and password every time.

Therefore, the code needs to be modified as follows:

Keep the registration logic unchanged.

2. Add global constant LOGIN_TIMEOUT, set a fixed login validity period.

3. Add a global variable SESSION_IDS is used to record the information of logged in users and their login time.

4. Add a login routing function to verify whether the user has been registered. If the registered user has the correct user name and password, the login is successful. Record the login information of the user and return the generated session_id.

5. Modify the decorator function to obtain the session in the request header_ ID field to judge whether the user has logged in and is within the validity period. If the validity period is exceeded, the login information of the user will be deleted from session_ Removed from IDS. The login timestamp is updated every time a request is initiated and the authentication is passed to extend the login validity time

The code is as follows:

import os
import time
import json
from hashlib import md5
from functools import wraps
from flask import Flask, request

app = Flask(__name__)

ACCOUNTS_FILE = os.path.join(os.path.dirname(os.path.abspath(__file__)), "accounts.json")

SESSION_IDS = {}

LOGIN_TIMEOUT = 60 * 60 * 24

def permission(func):
    @wraps(func)
    def inner():
        session_id = request.headers.get("session_id", "")
        global SESSION_IDS
        if session_id not in SESSION_IDS:  # Is there session confidence
            return {"data": None, "status_code": "FORBIDDEN", "message": "username not login"}
        if SESSION_IDS[session_id]["timestamp"] - time.time() > LOGIN_TIMEOUT:  # Is the session still valid
            SESSION_IDS.pop(session_id)  # If it fails, remove the session information
            return {"data": None, "status_code": "FORBIDDEN", "message": "username login timeout"}
        SESSION_IDS[session_id] = time.time()  # Update session time
        return func()
    return inner


@app.route("/register", methods=["POST"])
def register():
    """Registered user information"""
    username = request.form.get("username")
    password = request.form.get("password")
    if not username or not password:  # Judge the parameters entered by the user
        return {"data": None, "status_code": "InvalidParams", "message": "must have username and password"}
    if not os.path.exists(ACCOUNTS_FILE):  # Determine whether the specified file exists
        return {"data": None, "status_code": "NotFound", "message": "not found accounts file"}
    with open("accounts.json", "r+") as f:
        accounts = json.load(f)
    for account in accounts:
        if account["username"] == username:  # Determine whether the user exists
            return {"data": None, "status_code": "Duplicated", "message": "username is already exists"}
    accounts.append({"username": username, "password": md5(password.encode()).hexdigest()})
    with open("accounts.json", "w") as f:
        json.dump(accounts, f)
    return {"data": username, "status_code": "OK", "message": "register username successfully"}


@app.route("/login", methods=["POST"])
def login():
    """User login"""
    username = request.form.get("username")
    password = request.form.get("password")
    if not os.path.exists(ACCOUNTS_FILE):  # Does the user information file exist
        return {"data": None, "status_code": "NotFound", "message": "not found accounts file"}
    with open("accounts.json", "r+") as f:
        accounts = json.load(f)
    usernames = [account["username"] for account in accounts]
    if username not in usernames:  # Is the user registered
        return {"data": None, "status_code": "NotFound", "message": "username is not exists"}
    current_user = None
    for account in accounts:
        if account["username"] == username:
            current_user = account
            if md5(password.encode()).hexdigest() != account["password"]:  # Is the user name and password correct
                return {"data": None, "status_code": "Unauthorized", "message": "password is not correct"}
            session_id = md5((password + str(time.time())).encode()).hexdigest()  # Generate session ID
            global SESSION_IDS
            SESSION_IDS[session_id] = {"user_info": current_user, "timestamp": time.time()}  # Record session information
            return {"data": {"session_id": session_id}, "status_code": "OK", "message": "login successfully"}


@app.route("/cmdb", methods=["POST"])
@permission
def index():
    pass
    return "success"


if __name__ == "__main__":
    app.run(host="127.0.0.1", port=5000, debug=True)

The login request initiated through Postman is as follows:


Complete code
import os
import time
import json
from hashlib import md5
from functools import wraps
from flask import Flask, request

app = Flask(__name__)

ACCOUNTS_FILE = os.path.join(os.path.dirname(os.path.abspath(__file__)), "accounts.json")

SESSION_IDS = {}

LOGIN_TIMEOUT = 60 * 60 * 24

def permission(func):
    @wraps(func)
    def inner():
        session_id = request.headers.get("session_id", "")
        global SESSION_IDS
        if session_id not in SESSION_IDS:
            return {"data": None, "status_code": "FORBIDDEN", "message": "username not login"}
        if SESSION_IDS[session_id]["timestamp"] - time.time() > LOGIN_TIMEOUT:
            SESSION_IDS.pop(session_id)
            return {"data": None, "status_code": "FORBIDDEN", "message": "username login timeout"}
        SESSION_IDS[session_id] = time.time()
        return func()
    return inner


@app.route("/register", methods=["POST"])
def register():
    """ Registered user information """
    username = request.form.get("username")
    password = request.form.get("password")
    if not username or not password:
        return {"data": None, "status_code": "InvalidParams", "message": "must have username and password"}
    if not os.path.exists(ACCOUNTS_FILE):
        return {"data": None, "status_code": "NotFound", "message": "not found accounts file"}
    with open("accounts.json", "r+") as f:
        accounts = json.load(f)
    for account in accounts:
        if account["username"] == username:
            return {"data": None, "status_code": "Duplicated", "message": "username is already exists"}
    accounts.append({"username": username, "password": md5(password.encode()).hexdigest()})
    with open("accounts.json", "w") as f:
        json.dump(accounts, f)
    return {"data": username, "status_code": "OK", "message": "register username successfully"}


@app.route("/login", methods=["POST"])
def login():
    """User login"""
    username = request.form.get("username")
    password = request.form.get("password")
    if not os.path.exists(ACCOUNTS_FILE):
        return {"data": None, "status_code": "NotFound", "message": "not found accounts file"}
    with open("accounts.json", "r+") as f:
        accounts = json.load(f)
    usernames = [account["username"] for account in accounts]
    if username not in usernames:
        return {"data": None, "status_code": "NotFound", "message": "username is not exists"}
    current_user = None
    for account in accounts:
        if account["username"] == username:
            current_user = account
            if md5(password.encode()).hexdigest() != account["password"]:
                return {"data": None, "status_code": "Unauthorized", "message": "password is not correct"}
            session_id = md5((password + str(time.time())).encode()).hexdigest()
            global SESSION_IDS
            SESSION_IDS[session_id] = {"user_info": current_user, "timestamp": time.time()}
            return {"data": {"session_id": session_id}, "status_code": "OK", "message": "login successfully"}


@app.route("/index", methods=["GET"])
@permission
def index():
    pass
    return "success"


if __name__ == "__main__":
    app.run(host="127.0.0.1", port=5000, debug=True)

[summary]

This chapter mainly explains the principle and specific implementation of user identity authentication, and the next chapter will introduce in detail about permission verification.

In fact, there is also a third-party plug-in in flash that can log in, which is called flash login. Interested students can learn about it, but our purpose is to understand the specific logic of identity authentication, not to be a "package man".

In addition, one thing we need to think about carefully is that programming is facing the computer, so we need to be very careful about the rigorous logic and exception handling. Usually, when realizing a function, we don't mean to translate the specific logic of the function into code through positive thinking; Instead, we should fully consider various boundary conditions involved in the process of this function.

Finally, leave a question to think about. If you can think about various boundary conditions carefully, you will find that there is a concurrency problem in the final code:

When many people request to register at the same time, will there be write conflicts in the file? If so, how can the application solve it?

Welcome to add my personal official account [Python to play automation operation and maintenance] to join the reader exchange group to get more dry cargo content.

Topics: Operation & Maintenance Flask