什么是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路径下有很多模块,例如,auth,blog,main,user,这几个模块就是蓝图,auth(权限模块),blog(博客模块),main(测试模块),user(用户模块)。
创建蓝图
创建蓝图我们需要涉及到一下两个步骤:
- 定义蓝图:需要在独立的模块中定义蓝图(也就是项目结构中的auth,blog,main,user文件中定义)
- 注册蓝图:在主应用中注册蓝图(根目录下的__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
小测试
注册用户
用户登录
退出登录
结语
以上就是所有本次学习的内容了,如有疑惑欢迎评论和私信,存在错误的讲解也希望可以给出提示,仅用于学习和技术分享不可以用来线上发布!