将JWT身份验证(Flask-JWT-Extended)集成到Python的Flask Web API中
首先
之前,我在Python中创建的Flask Web API中添加了JWT(JSON Web Token)身份验证。但是,当我将Python版本从3.8升级到3.11时,出现了错误。因此,我尝试切换到Flask-JWT-Extended替代flask-jwt。
将JWT身份验证集成到Python的Flask Web API中。
无法从’collections’导入名为’Mapping’的名字。
我将Python版本从3.8系更新到3.11系时发生了这个错误。
...
from collections import Mapping
ImportError: cannot import name 'Mapping' from 'collections'
...
从Python 3.3开始,collections.Mapping被标记为不推荐使用,并且在Python 3.10中已从collections中移除。另外,由于Flask-JWT长时间未进行维护,它使用了collections.Mapping,这是导致问题的原因。
所以,我匆忙地在Python 3.11版本中寻找了类似的解决方案,并最终选择了Flask-JWT-Extended。
Flask-JWT-Extended 是什么?
Flask-JWT-Extended 用汉语进行简述:
Flask-JWT-Extended不仅为Flask添加了对使用JSON Web Tokens (JWT)进行路由保护的支持,还内置了许多有用的(可选)功能,使得处理JSON Web Tokens更加便捷。
Flask-JWT-Extended 不仅为 Flask 添加了支持以使用 JWT 来保护路由,还添加了许多(以及可选的)功能,以便更轻松地操作 JWT。
环境
-
- ローカル環境
Windows 11 Pro 22H2
Python 3.11.1
PowerShell 7.3.1
Visual Studio Code 1.74.3
Git for Windows 2.39.1.windows.1
MongoDB 6.0.3 / Mongosh 1.6.0
准备用户信息
为了确定用户能否使用Web API的功能,我们在MongoDB中添加了一个名为users的集合,并注册了用户信息,通过用户名称和密码来判断。
> mongosh localhost:27017/admin -u admin -p
> use holoduledb
switched to db holoduledb
> show collections
holodules
> db.createCollection("users");
{ "ok" : 1 }
> db.users.insertOne( {"id":"1", "username":"user01", "password":"dummy", "firstname": "taro", "lastname": "tokyo"} );
{
acknowledged: true,
insertedId: ObjectId("63d1dfafcec12c32af27ec11")
}
> db.users.find();
[
{
_id: ObjectId("63d1dfafcec12c32af27ec11"),
id: '1',
username: 'user01',
password: 'dummy',
firstname: 'taro',
lastname: 'tokyo'
}
]
添加一个用于在 Flask 中使用 JWT 身份验证的包。
请先添加 Flask-JWT-Extended 包。
> poetry add flask-jwt-extended
将JWT认证处理嵌入
我們將JWT身份驗證功能整合到現有的app.py網絡API中。
添加已创建的User类和Flask-JWT包的导入
from flask_jwt_extended import jwt_required, create_access_token, JWTManager, get_jwt_identity
from models.user import User
写出JWT的设置
密钥 JWT_SECRET_KEY 是从配置文件中设置的。
app.config['JWT_SECRET_KEY'] = settings.jwt_secret_key # JWTに署名する際の秘密鍵
app.config['JWT_ALGORITHM'] = 'HS256' # 暗号化署名のアルゴリズム
app.config['JWT_LEEWAY'] = 0 # 有効期限に対する余裕時間
app.config['JWT_EXPIRATION_DELTA'] = timedelta(seconds=300) # トークンの有効期間
app.config['JWT_NOT_BEFORE_DELTA'] = timedelta(seconds=0) # トークンの使用を開始する相対時間
添加JWT验证错误处理程序
@log(logger)
def jwt_unauthorized_loader_handler(reason):
logger.error(f"{reason}")
return make_response(jsonify({'error': 'Unauthorized'}), 401)
将应用程序与 flask_jwt_extended 进行关联
jwt = JWTManager(app)
jwt.unauthorized_loader(jwt_unauthorized_loader_handler)
添加一个函数,通过用户名和密码进行身份验证,并返回访问令牌。
@log(logger)
@app.route('/login', methods=['POST'])
def login():
if not request.is_json:
abort(400)
request_body = request.get_json()
if request_body is None:
abort(400)
whitelist = {'username', 'password'}
if not request_body.keys() <= whitelist:
abort(400)
user = User.from_doc(db.users.find_one({"username": request_body['username']}))
authenticated = True if user is not None and user.password == request_body['password'] else False
auth_user = user if authenticated else None
if auth_user is None:
abort(401)
access_token = create_access_token(identity=auth_user.username)
response_body = {'access_token': access_token}
return make_response(jsonify(response_body), 200)
在需要JWT认证的Web API方法上指定装饰器。
@log(logger)
@app.route('/holodules/<string:date>', methods=['GET'])
@jwt_required()
def holodules(date):
logger.info(f"username: {get_jwt_identity()}")
logger.info(f"date: {date}")
if len(date) != 8:
abort(500)
...
JWT 身份验证处理的操作确认
启动 Web API
> poetry run python app.py
* Serving Flask app 'app'
* Debug mode: off
[INFO ]_log - WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.
* Running on http://127.0.0.1:5000
[INFO ]_log - Press CTRL+C to quit
使用邮递员(Postman),以获取访问令牌(access token)。
请确认能够通过提供用户名和密码来向/login发送请求并获取访问令牌。
Action : POST
URL : http://127.0.0.1:5000/login
Headers : Content-Type: application/json
Body(raw) : {"username": "user01", "password": "password01"}
{
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
}
使用邮差工具调用 Web API 的方法
通过指定获取的访问令牌,调用Web API方法并确保可以获取到响应。
Action : POST
URL : http://127.0.0.1:5000/holodules/20230126
Headers : Content-Type: application/json, Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
[
{
"key": "HL0501_20230126_210000",
"code": "HL0501",
"video_id": "yMNNSGV6eT0",
"datetime": "20230126 210000",
"name": "獅白ぼたん",
"title": "配信予定地【獅白ぼたん/ホロライブ】",
"url": "https://www.youtube.com/watch?v=yMNNSGV6eT0",
"description": "お外中なので帰宅したらあらためてつくる~!✨メンバーシップ参加はこちらから✨https://www.youtube.com/channel/UCUKD-uaobj9jiqB-VXt71mA/join特"
},
{
"key": "HL0004_20230126_190100",
"code": "HL0004",
"video_id": "JKm_iWxBghE",
"datetime": "20230126 190100",
"name": "星街すいせい",
"title": "新髪形お披露目+ちょっと告知!【ホロライブ / 星街すいせい】",
"url": "https://www.youtube.com/watch?v=JKm_iWxBghE",
"description": "?2023/1/25 星街すいせい 2ndアルバム『Specter』発売!?2023/1/28 Hoshimachi Suisei 2nd Solo Live Shout in Crisis 開催!◆"
},
...
]
app.py 中已集成 Flask-JWT-Extended。
import json
from flask import Flask, jsonify, request, abort, make_response, current_app
from flask import jsonify, request, Flask
from flask_jwt_extended import jwt_required, create_access_token, JWTManager, get_jwt_identity
from flask_cors import CORS
from pymongo import MongoClient
from os.path import join, dirname
from urllib.parse import quote_plus
from datetime import timedelta
from models.holodule import Holodule
from models.user import User
from settings import Settings
from logger import log, get_logger
# ロギングの設定
json_path = join(dirname(__file__), "config/logger.json")
log_dir = join(dirname(__file__), "log")
logger = get_logger(log_dir, json_path, False)
# Settings インスタンス
settings = Settings(join(dirname(__file__), '.env'))
# MongoDB 接続情報
mongodb_user = quote_plus(settings.mongodb_user)
mongodb_password = quote_plus(settings.mongodb_password)
mongodb_host = "mongodb://%s/" % (settings.mongodb_host)
# MongoDB 接続認証
client = MongoClient(mongodb_host)
db = client.holoduledb
db.authenticate(name=mongodb_user,password=mongodb_password)
# Flask
app = Flask(__name__)
app.url_map.strict_slashes = False
# CORS
CORS(app)
# JSONのソートを抑止
app.config['JSON_SORT_KEYS'] = False
# Flask JWT
app.config['JWT_SECRET_KEY'] = settings.jwt_secret_key # JWTに署名する際の秘密鍵
app.config['JWT_ALGORITHM'] = 'HS256' # 暗号化署名のアルゴリズム
app.config['JWT_LEEWAY'] = 0 # 有効期限に対する余裕時間
app.config['JWT_EXPIRATION_DELTA'] = timedelta(seconds=300) # トークンの有効期間
app.config['JWT_NOT_BEFORE_DELTA'] = timedelta(seconds=0) # トークンの使用を開始する相対時間
# JWT の認証エラーハンドラ
@log(logger)
def jwt_unauthorized_loader_handler(reason):
logger.error(f"{reason}")
return make_response(jsonify({'error': 'Unauthorized'}), 401)
# JWT
jwt = JWTManager(app)
jwt.unauthorized_loader(jwt_unauthorized_loader_handler)
# レスポンスにCORS許可のヘッダーを付与
@app.after_request
def after_request(response):
# response.headers.add('Access-Control-Allow-Origin', '*')
response.headers.add('Access-Control-Allow-Headers', 'Content-Type,Authorization')
response.headers.add('Access-Control-Allow-Methods', 'GET,PUT,POST,DELETE,OPTIONS')
return response
# ログインしてトークンを返却
@log(logger)
@app.route('/login', methods=['POST'])
def login():
if not request.is_json:
abort(400)
request_body = request.get_json()
if request_body is None:
abort(400)
whitelist = {'username', 'password'}
if not request_body.keys() <= whitelist:
abort(400)
u = request_body['username']
p = request_body['password']
user = User.from_doc(db.users.find_one({"username": request_body['username']}))
authenticated = True if user is not None and user.password == request_body['password'] else False
auth_user = user if authenticated else None
if auth_user is None:
abort(401)
access_token = create_access_token(identity=auth_user.username)
response_body = {'access_token': access_token}
return make_response(jsonify(response_body), 200)
# ホロジュール配信予定を取得
@log(logger)
@app.route('/holodules/<string:date>', methods=['GET'])
@jwt_required()
def holodules(date):
logger.info(f"username: {get_jwt_identity()}")
logger.info(f"date: {date}")
if len(date) != 8:
abort(500)
# MongoDB から年月日を条件にホロジュール配信予定を取得してリストに格納
holodule_list = []
for holodule in db.holodules.find({"datetime": {'$regex':'^'+date}}).sort("datetime", -1):
holodule_list.append(Holodule.from_doc(holodule))
# オブジェクトリストをJSON配列に変換
holodules = []
for holodule in holodule_list:
holodules.append(holodule.to_doc())
# UTF-8コードの application/json として返却
return make_response(jsonify(holodules), 200)
# エラーハンドラ:400
@log(logger)
@app.errorhandler(400)
def bad_request(error):
logger.error(f"{error}")
return make_response(jsonify({'error': 'Bad request'}), 400)
# エラーハンドラ:401
@log(logger)
@app.errorhandler(401)
def Unauthorized(error):
logger.error(f"{error}")
return make_response(jsonify({'error': 'Unauthorized'}), 401)
# エラーハンドラ:404
@log(logger)
@app.errorhandler(404)
def not_found(error):
logger.error(f"{error}")
return make_response(jsonify({'error': 'Not found'}), 404)
# エラーハンドラ:500
@log(logger)
@app.errorhandler(500)
def internal_server_error(error):
logger.error(f"{error}")
return make_response(jsonify({'error': 'Internal Server Error'}), 500)
if __name__ == "__main__":
app.run()
用于日志输出的 logger.py
import json
import datetime
import inspect
from os.path import join
from functools import wraps
from logging import config, getLogger, Filter
class CustomFilter(Filter):
def filter(self, record):
record.real_filename = getattr(record, 'real_filename', record.filename)
record.real_funcName = getattr(record, 'real_funcName', record.funcName)
record.real_lineno = getattr(record, 'real_lineno', record.lineno)
return True
def get_logger(log_dir, json_path, verbose=False):
with open(json_path, "r", encoding="utf-8") as f:
log_config = json.load(f)
# ログファイル名を日付とする
log_path = join(log_dir, f"{datetime.datetime.now().strftime('%Y%m%d')}.log")
log_config["handlers"]["rotateFileHandler"]["filename"] = log_path
# verbose引数が設定されていればレベルをINFOからDEBUGに置換
if verbose:
log_config["root"]["level"] = "DEBUG"
log_config["handlers"]["consoleHandler"]["level"] = "DEBUG"
log_config["handlers"]["rotateFileHandler"]["level"] = "DEBUG"
# ロギングの設定を適用してロガーを取得
config.dictConfig(log_config)
logger = getLogger(__name__)
#logger.addFilter(CustomFilter())
return logger
def log(logger):
def _decorator(func):
# funcのメタデータを引き継ぐ
@wraps(func)
def wrapper(*args, **kwargs):
func_name = func.__name__
extra = {
'real_filename': inspect.getfile(func),
'real_funcName': func_name,
'real_lineno': inspect.currentframe().f_back.f_lineno
}
# funcの開始
logger.info(f'[START] {func_name}', extra=extra)
try:
# funcの実行
return func(*args, **kwargs)
except Exception as err:
# funcのエラーハンドリング
logger.error(err, exc_info=True, extra=extra)
finally:
# funcの終了
logger.info(f'[END] {func_name}', extra=extra)
return wrapper
return _decorator
从 .env 文件中获取设置信息的 settings.py
import os
from dotenv import load_dotenv
class Settings:
def __init__(self, envpath):
# .env ファイルを明示的に指定して環境変数として読み込む
self.__dotenv_path = envpath
load_dotenv(self.__dotenv_path)
# 環境変数から設定値を取得
self.__mongodb_user = os.environ.get("MONGODB_USER")
self.__mongodb_password = os.environ.get("MONGODB_PASSWORD")
self.__mongodb_host = os.environ.get("MONGODB_HOST")
self.__jwt_secret_key = os.environ.get("JWT_SECRET_KEY")
# mongodb の ユーザー
@property
def mongodb_user(self):
return self.__mongodb_user
# mongodb の パスワード
@property
def mongodb_password(self):
return self.__mongodb_password
# mongodb の ホスト:ポート
@property
def mongodb_host(self):
return self.__mongodb_host
# JWT の秘密鍵
@property
def jwt_secret_key(self):
return self.__jwt_secret_key
在 models/user.py 中描写了用户信息的定义。
class User:
def __init__(self, id, username, password, firstname, lastname):
self.__id = id
self.__username = username
self.__password = password
self.__firstname = firstname
self.__lastname = lastname
# id
@property
def id(self):
return self.__id
@id.setter
def id(self, id):
self.__id = id
# username
@property
def username(self):
return self.__username
@username.setter
def username(self, username):
self.__username = username
# password
@property
def password(self):
return self.__password
@password.setter
def password(self, password):
self.__password = password
# firstname
@property
def firstname(self):
return self.__firstname
@firstname.setter
def firstname(self, firstname):
self.__firstname = firstname
# lastname
@property
def lastname(self):
return self.__lastname
@lastname.setter
def lastname(self, lastname):
self.__lastname = lastname
# ドキュメントから変換
@classmethod
def from_doc(cls, doc):
if doc is None:
return None
user = User(doc['id'],
doc['username'],
doc['password'],
doc['firstname'],
doc['lastname'])
return user
# ドキュメントへ変換
def to_doc(self):
doc = { 'id': str(self.id),
'username': str(self.username),
'password' : str(self.password),
'firstname' : str(self.firstname),
'lastname' : str(self.lastname) }
return doc
在 models/holodule.py 文件中记录了与 Holodule 相关的独特定义。
import datetime
class Holodule:
codes = {
"ホロライブ" : "HL0000",
"ときのそら" : "HL0001",
"ロボ子さん" : "HL0002",
"さくらみこ" : "HL0003",
"星街すいせい" : "HL0004",
"AZKi" : "HL0005",
"夜空メル" : "HL0101",
"アキ・ローゼンタール" : "HL0102",
"赤井はあと" : "HL0103",
"白上フブキ" : "HL0104",
"夏色まつり" : "HL0105",
"湊あくあ" : "HL0201",
"紫咲シオン" : "HL0202",
"百鬼あやめ" : "HL0203",
"癒月ちょこ" : "HL0204",
"大空スバル" : "HL0205",
"大神ミオ" : "HL0G02",
"猫又おかゆ" : "HL0G03",
"戌神ころね" : "HL0G04",
"兎田ぺこら" : "HL0301",
"潤羽るしあ" : "HL0302",
"不知火フレア" : "HL0303",
"白銀ノエル" : "HL0304",
"宝鐘マリン" : "HL0305",
"天音かなた" : "HL0401",
"桐生ココ" : "HL0402",
"角巻わため" : "HL0403",
"常闇トワ" : "HL0404",
"姫森ルーナ" : "HL0405",
"獅白ぼたん" : "HL0501",
"雪花ラミィ" : "HL0502",
"尾丸ポルカ" : "HL0503",
"桃鈴ねね" : "HL0504",
"魔乃アロエ" : "HL0505",
"ラプラス" : "HL0601",
"鷹嶺ルイ" : "HL0602",
"博衣こより" : "HL0603",
"沙花叉クロヱ" : "HL0604",
"風真いろは" : "HL0605"
}
def __init__(self, code="", video_id="", datetime=None, name="", title="", url="", description=""):
self.__code = code
self.__video_id = video_id
self.__datetime = datetime
self.__name = name
self.__title = title
self.__url = url
self.__description = description
# キー
@property
def key(self):
_code = self.code;
_code = Holodule.codes[self.name] if self.name in Holodule.codes else ""
_dttm = self.datetime.strftime("%Y%m%d_%H%M%S") if self.datetime is not None else ""
return _code + "_" + _dttm if ( len(_code) > 0 and len(_dttm) > 0 ) else ""
# コード
@property
def code(self):
return self.__code
# video_id
@property
def video_id(self):
return self.__video_id
@video_id.setter
def video_id(self, video_id):
self.__video_id = video_id
# 日時
@property
def datetime(self):
return self.__datetime
@datetime.setter
def datetime(self, datetime):
self.__datetime = datetime
# 名前
@property
def name(self):
return self.__name
@name.setter
def name(self, name):
self.__name = name
# タイトル(Youtubeから取得)
@property
def title(self):
return self.__title
@title.setter
def title(self, title):
self.__title = title
# URL
@property
def url(self):
return self.__url
@url.setter
def url(self, url):
self.__url = url
# 説明(Youtubeから取得)
@property
def description(self):
return self.__description
@description.setter
def description(self, description):
self.__description = description
# ドキュメントから変換
@classmethod
def from_doc(cls, doc):
if doc is None:
return None
holodule = Holodule(doc['code'] if 'code' in doc else Holodule.codes[doc['name']],
doc['video_id'],
datetime.datetime.strptime(doc['datetime'], '%Y%m%d %H%M%S'),
doc['name'],
doc['title'],
doc['url'],
doc['description'])
return holodule
# ドキュメントへ変換
def to_doc(self):
doc = { 'key': str(self.key),
'code': str(self.code),
'video_id': str(self.video_id),
'datetime' : str(self.datetime.strftime("%Y%m%d %H%M%S")),
'name' : str(self.name),
'title' : str(self.title),
'url' : str(self.url),
'description' : str(self.description) }
return doc
最后
在使用 React 进行 Web 应用开发时,由于更新了后端 Python 的版本,导致了这次的错误,因此我觉得这正是一个合适的时机来对整体进行重新审视。
由于更新了收集数据的Python程序版本,它似乎会在一段时间内稳定运行。