学习python flask框架 如何使用Blueprints(2)

什么是Flask 蓝图 (Blueprints)

Blueprint 是 Flask 提供的一种方式,用来在一个应用中组织视图函数和其他相关代码模块。使得应用更具可维护性和可扩展性。

每个蓝图都可以有自己的路由,视图函数和静态资源等,可以将不同的资源进行分组。

Flask蓝图框架的结构

my_flask_app/
│
├── app/
│   ├── __init__.py
│   ├── routes/  # 将不同功能模块的路由分开管理。
│   │   ├── __init__.py
│   │   ├── auth # 主模块的路由。
│   │   │   ├──__init__.py
│   │   │   └── routes.py
│   │   ├── blog # 主模块的路由。
│   │   │   ├──__init__.py
│   │   │   └── routes.py
│   │   ├── main # 主模块的路由。
│   │   │   ├──__init__.py
│   │   │   └── main.py
│   │   ├── user # 主模块的路由。
│   │   │   ├──__init__.py
│   │   │   ├──user.py
│   │   │   └──login.py
│   ├── models/ # 管理数据模型,通常与数据库操作相关。
│   │   ├── __init__.py
│   │   └── User.py # 用户模型。
│   ├── templates/ # 存放 HTML 模板文件。
│   │   ├── layout.html #
│   │   └── home.html
│   └── static/ # 存放静态文件,如 CSS 和 JavaScript。
│       ├── css/
│       └── js/
│
├── config.py
├── requirements.txt
├── migrations/ # 数据库迁移文件,通常与 SQLAlchemy 相关。
│   └── ...
└── run.py

可以看到routes路径下有很多模块,例如,authblogmainuser,这几个模块就是蓝图,auth(权限模块),blog(博客模块),main(测试模块),user(用户模块)。

创建蓝图

创建蓝图我们需要涉及到一下两个步骤:

  1. 定义蓝图:需要在独立的模块中定义蓝图(也就是项目结构中的auth,blog,main,user文件中定义)
  2. 注册蓝图:在主应用中注册蓝图(根目录下的__init__.py文件)

定义蓝图

bp_user = Blueprint('user', __name__, url_prefix='/user')

以上代码就是定义蓝图,作用说明如下:

参数名作用说明
'user'蓝图的名称(唯一标识),一般用模块名
__name__当前模块的 Python 名,用于定位静态文件和模板
url_prefix为所有注册在该蓝图下的路由加上统一前缀(如 /user/login

一些常用的蓝图参数

Blueprint(
    name,                   # 蓝图名(必须)
    import_name,            # 模块名(必须,通常为 __name__)
    static_folder=None,     # 指定该蓝图的静态文件目录
    static_url_path=None,   # 该蓝图静态文件的 URL 路径(如 '/user/static')
    template_folder=None,   # 指定该蓝图的模板文件目录(如 'templates/user')
    url_prefix=None,        # URL 前缀(如 '/user')
    subdomain=None          # 子域名路由支持(如 'www.ez.com')
)

bp_user = Blueprint('user', __name__, static_folder='static', template_folder='templates',
                    url_prefix='/user', subdomain='www.ez.com')

注册蓝图

# 应用工厂 (Application Factory) 用于创建和配置 Flask 应用实例。这种方法允许你创建多个应用实例,或者在不同配置下初始化应用。
import logging
import os
import os.path
from logging.handlers import RotatingFileHandler

from flask import Flask, render_template
from flask.logging import default_handler

from flask_sqlalchemy import SQLAlchemy

db = SQLAlchemy()


def create_app(config_name, config_sql):
    app = Flask(__name__)
    app.config.from_object(config_name)
    app.config.from_object(config_sql)

    # 初始化数据库
    db.init_app(app)

    # 为什么 import 要放在 create_app() 函数体内?
    # 为了确保在导入蓝图时 app 已经准备好,从而避免循环引用和上下文错误
    from .routes.user.user import bp_user  
    from .routes.auth.routes import bp_auth  
    from .routes.blog.routes import bp_blog  
    from .routes.user.login import bp_login  
    # 注册蓝图
    app.register_blueprint(bp_main)
    app.register_blueprint(bp_user)
    app.register_blueprint(bp_auth, url_prefix='/auth', static_folder='static', template_folder='templates')
    app.register_blueprint(bp_blog, url_prefix='/blog', static_folder='static', template_folder='templates')
    app.register_blueprint(bp_login, url_prefix='/customer', static_folder='static', template_folder='templates')

    # 全局404页面
    @app.errorhandler(404)
    def page_not_found(error):
        return render_template('404.html', error=error), 404

    return app


以上代码可以看到蓝图的注册部分,显示带入定义的蓝图,然后使用 app.register_blueprint(x)注册蓝图,或者在注册蓝图是添加常用的参数,url_prefix,static_folder,template_folder,如果定义的时候没有配置这些参数,在注册时也可以配置,效果是一样的。

使用蓝图

import time
from sqlalchemy import text
from flask import Flask, jsonify, Blueprint, render_template, request
from sqlalchemy import or_

from app import db
from app.models import User

app = Flask(__name__)

bp_user = Blueprint('user', __name__, url_prefix='/user')


@bp_user.route('/add_user', methods=['POST'])
def add_user():
    user_name = request.form['user_name']
    user_email = request.form['email']

    new_user = User(user_name=user_name, email=user_email)
    db.session.add(new_user)
    db.session.commit()
    result = {
        'code': 200,
        'msg': 'success',
        'data': {'id': new_user.id, 'user_name': new_user.user_name, 'email': new_user.user_name}
    }
    app.logger.info('result successfully.', result)
    return jsonify(result)

小测试

错误示范

如果没有添加路由前缀时会返回我们设置的全局404页面

正确示范

如何在蓝图中使用钩子

在蓝图中使用钩子,离不开以下函数

全局钩子

before_app_request

  • 触发时机:在请求处理之前调用(当前端发送请求后先进入使用before_app_request的函数)

  • 作用范围:应用级别

  • 典型用途

    • 用户身份验证、权限校验

    • 设置上下文变量(比如 g.user

    • 请求日志记录(请求方法、路径、IP 等)

    • 拦截非法请求(提前 return 响应)

after_app_request

  • 触发时机:视图函数返回 Response 对象之后,响应发送到前端之前

  • 作用范围:应用级别

  • 典型用途

    • 修改响应头(如添加 CORS)

    • 统一封装响应格式(如将返回包裹在 {code, msg, data} 中)

    • 写入访问日志(响应码、耗时等)

    • 清理资源、记录性能

局部钩子

before_request

  • 触发时机:在请求处理之前调用(当前端发送一个user蓝图模块的请求后先进入使用before_request的函数)

  • 作用范围:当前蓝图级别(如 /user/info

  • 典型用途:和全局钩子一摸一样,只是作用域某个蓝图中用来拦截一些非法请求,比如做一个crm系统,就需要用到这样的局部钩子

after_request

  • 触发时机:视图函数返回响应后执行

  • 作用范围:当前蓝图级别(如 /user/info

  • 典型用途:和全局钩子一样的用途

注意事项

TypeError: The view function did not return a valid response. The return type must be a string, dict, tuple, Response instance, or WSGI callable, but it was a dict returned from after_request, which is not allowed.

如果遇到以上错误就表明你在使用@bp.after_app_request时返回的不是一个response方法体,或者你返回的时Python的数据结构,比如(dict)字典就会出现以上的错误信息

after_app_reques和after_reques必须返回Response或兼容的对象。

代码展示

我写了一个伪登录系统来验证模拟钩子的作用,分别有三个接口,登录接口,注册接口,登出接口,在/user/login.py文件下编写,然后我们在/auth/routes.py文件下编写钩子的逻辑,主要就是对token进行验证,严重通过的用户才能访问到数据,验证失败的用户会提示权限不足或者token过期等字样,登录和注册接口不做授权验证直接放行,以下是代码逻辑

依赖安装

 # jwt依赖
 pip install PyJWT
 # bcrypt依赖
 pip install bcrypt
 # pymysql+flask-sqlalchemy+Flask依赖
 pip install flask flask-sqlalchemy pymysql  

routes代码

import json
from concurrent.futures import ThreadPoolExecutor
from functools import wraps

import jwt
import redis
from flask import Blueprint, render_template, jsonify, request, g

bp_auth = Blueprint('auth', __name__)

# jwt密钥配置
SECRET_KEY = '30b5f9f434a9b69a43c5281e80b7d698cfb4c6bc0acdc96b7aa0c7ac8cbafa8d'

# 不需要验证token的路径和方法
EXPIRE_PATHS = [('/customer/login', 'POST'), ('/customer/register', 'POST')]
# redis配置
rds = redis.Redis(host='localhost', port=6379, db=0)

"""验证token逻辑"""


def jwt_required(f):
    # 保留原函数的元信息
    @wraps(f)
    def decorated(*args, **kwargs):
        path = request.path
        method = request.method
        # 登录和注册接口不验证token直接放行
        if (path, method) in EXPIRE_PATHS:
            return
        token = request.headers.get("Authorization")
        if not token:
            return jsonify({'code': 401, 'msg': 'lack token '}), 401
        try:
            token_str = token.split("Bearer ")[1]
            # 如果不存在说明已经退出了
            token_rds = rds.get(f"token:{token_str}")
            if token_rds is None:
                return jsonify({'code': 401, 'msg': 'Token logged out'}), 401

            payload = jwt.decode(token_str, SECRET_KEY, algorithms=['HS256'])
            g.user = payload['username']
        except jwt.ExpiredSignatureError:
            return jsonify({'code': 401, 'msg': 'token is expired'}), 401
        except jwt.InvalidTokenError:
            return jsonify({'code': 401, 'msg': 'token is invalid'}), 401
        return f(*args, **kwargs)

    return decorated


@bp_auth.before_app_request
@jwt_required
def before_request():
    # 通过@jwt_required绑定jwt_required函数
    # 执行在每个请求之前的操作
    print("Before request")


@bp_auth.after_app_request
def after_required(response):
    # 在这里可以对响应数据进行处理
    print("After request")
    if response is None:
        res = render_template('404.html')
        return res
    else:
        if response.content_type == 'application/json':
            try:
                origin_data = json.loads(response.get_data(as_text=True))
            except Exception:
                origin_data = response.get_data(as_text=True)
            # 返回新的 Response
            return jsonify(origin_data)
    # 非 JSON 响应直接原样返回
    return response

login代码

import datetime
import time

import bcrypt
import jwt
import redis
from flask import Blueprint, jsonify, request, render_template

from app import db
from app.models import User

bp_login = Blueprint('login', __name__)

SECRET_KEY = '30b5f9f434a9b69a43c5281e80b7d698cfb4c6bc0acdc96b7aa0c7ac8cbafa8d'

rds = redis.Redis(host='localhost', port=6379, db=0)


@bp_login.route('/login', methods=['POST'])
def login():
    user_name = request.form.get('username')
    if user_name is None or user_name == '':
        return jsonify({'code': 400, 'msg': 'The username cannot be empty'}), 400
    password = request.form.get('password')
    if password is None or password == '':
        return jsonify({'code': 400, 'msg': 'The password cannot be empty'}), 400
    user = User.query.filter_by(user_name=user_name).first()
    print("encryption before password:", user.password)
    # 校验密码
    if not user or not verify_password(password, user.password):
        return jsonify({"code": 401, "msg": "Incorrect username or password"}), 401

    payload = {
        'username': user_name,
        'exp': datetime.datetime.utcnow() + datetime.timedelta(minutes=5)  # 过期时间
    }
    # 生成token
    token = jwt.encode(payload, SECRET_KEY, algorithm='HS256')
    # 存储token到redis 用于退出和校验token
    rds.setex(f"token:{token}", 300, "true")
    return jsonify({
        'code': 200,
        'msg': 'Login Successful',
        'token': token
    })


@bp_login.route('/out_login', methods=['POST'])
def out_login():
    token = request.headers.get('Authorization')
    if not token:
        return jsonify({'code': 401, 'msg': 'Token not found'}), 401
    try:
        token = token.split("Bearer ")[1]
        payload = jwt.decode(token, SECRET_KEY, algorithms=['HS256'])
        # 过期时间
        exp_timestamp = payload['exp']
        # 当前时间
        now = int(time.time())
        # 剩余时间
        ttl = exp_timestamp - now
        if ttl > 0:
            rds.delete(f"token:{token}")
            return jsonify({'code': 200, 'msg': 'Logout successful'}), 200
        else:
            return jsonify({'code': 401, 'msg': 'Token has expired'}), 401
    except jwt.ExpiredSignatureError:
        return jsonify({'code': 401, 'msg': 'Token has expired'}), 401


@bp_login.route('/register', methods=['POST'])
def register():
    user_name = request.form.get('username')
    if user_name is None:
        return jsonify({'code': 400, 'msg': 'username cannot be empty'}), 400
    email = request.form.get('email')
    if email is None:
        return jsonify({'code': 400, 'msg': 'email cannot be empty'}), 400
    password = request.form.get('password')
    if password is None:
        return jsonify({'code': 400, 'msg': 'password cannot be empty'}), 400

    verify_password = request.form.get('verify_password')
    if verify_password is None or verify_password == '':
        return jsonify({'code': 400, 'msg': 'confirm password cannot be empty'}), 400
    if verify_password != password:
        return jsonify({'code': 400, 'msg': 'password inconsistency, please re-enter'}), 400
    print("password before encryption:", password)
    encryption = hash_password(password)
    print("password after encryption:", encryption)
    user_new = User(user_name=user_name, password=encryption, email=email)
    db.session.add(user_new)
    db.session.commit()
    return jsonify({'code': 200, 'msg': 'register successful'}), 200


@bp_login.errorhandler(404)
def page_not_found(e):
    return render_template('404.html'), 404


"""密码加密"""


def hash_password(plain_password: str) -> str:
    salt = bcrypt.gensalt()  # 自动生成盐
    return bcrypt.hashpw(plain_password.encode('utf-8'), salt).decode('utf-8')


"""密码验证"""


def verify_password(plain_password: str, hashed_password: str) -> bool:
    flag = bcrypt.checkpw(plain_password.encode('utf-8'), hashed_password.encode('utf-8'))
    print("verify password ", flag)
    return flag

 小测试

注册用户

用户登录

退出登录

结语

以上就是所有本次学习的内容了,如有疑惑欢迎评论和私信,存在错误的讲解也希望可以给出提示,仅用于学习和技术分享不可以用来线上发布!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

亿滋

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值