Flask 搭建 Restful 风格项目

参考文章: 使用 Flask + Flask RESTful 快速搭建 API 服务

参考了上述的文章使用 Flask 搭建了一个 Restful 风格的项目,对上述文章中自己一些不太理解的地方进行了一些扩充(扩充部分使用了前后双横线进行了分割 ,搭建过程中使用了 cursor,部分解释来自 cursor 中的AI 对话,AI 生成解释部分文中已标注),可供参考。

项目初始化

  • 使用 Flask + Flask RESTful 搭建 API 应用并使用 Blueprint(蓝图) 管理 API;
  • 使用 Flask-SQLAlchemy 扩展实现 ORM 操作 MySQL 数据库;
  • 基于 JWT 验证实现注册、登录以及登出接口;
  • 实现一个最基本的列表获取接口;
  • 解决跨域问题

设置vscode不显示 pycache 文件夹(按个人需求设置)

环境创建

注意: 以下所有操作与命令都在 Macos 环境下进行,Windows 或 Linux 可能有些许不同

使用 Anaconda 环境,可以用清华源下载

ini 复制代码
conda create -n py312 python=3.12.11

或者使用虚拟环境

bash 复制代码
python -m venv .venv  # 项目目录中会多一个 .venv 的虚拟环境目录
. .venv/bin/activate  # 激活虚拟环境

依赖安装

复制代码
pip install Flask flask-restful python-dotenv

hello-world 接口实现

python 复制代码
# app/__init__.py
from flask import Flask
from flask_restful import Resource, Api
app = Flask(__name__)
# app = Flask('wzf')
api = Api(app)

class Hello(Resource):
    def get(self):
        return {'message': 'test'}

api.add_resource(Hello, '/hello')

使用 Flask() 初始化一个 Flask 应用实例赋值给 app,传入的 __name__ 则是模块名 "app"。我们这里也可以传入一个固定字符串,但是一般情况下不会这么使用。

再使用 Api(app) 初始化一个 flask_restful 实例赋值给 api。 接下来我们定义了 Hello 这个类,它继承于 Resource 类。这个类中定义一个名为 get 的函数,它返回一个固定的 JSON 为{'message': 'test'}

最后我们使用 api.add_resource(HelloWorld, '/hello') 去注册接口,并指定了访问路由,当访问的接口路径为 "/hello" 且请求方式为 GET 时,就会调用该类中定义好的 get() 函数处理。在以 Resource 类为基类的派生类中,就是我们定义不同 HTTP 请求方式的地方,所以在这个类中,你还可以定义 postputdelete 等函数。



Flask(__name__)Flask('wzf')对比 区别

python 复制代码
# Flask(__name__)
app = Flask(__name__)
print(app.name)  # 输出: "app" (如果文件是 app/__init__.py)

# Flask('wzf')
app = Flask('wzf')
print(app.name)  # 输出: "wzf"

# Flask(__name__)
app = Flask(__name__)
# 日志显示: app.views, app.models 等

# Flask('wzf')  
app = Flask('wzf')
# 日志显示: wzf.views, wzf.models 等
参数 模板文件夹 静态文件夹 说明
__name__ 当前模块目录/templates 当前模块目录/static 相对模块位置
'wzf' 当前工作目录/templates 当前工作目录/static 相对工作目录

推荐使用 Flask(__name__) 的原因:

  1. 模块化:应用名称自动反映模块结构
  2. 路径正确:模板和静态文件路径基于模块位置
  3. 可移植性:不依赖当前工作目录
  4. 标准做法:Flask 官方推荐的方式 何时使用固定字符串:
  • 特殊需求:需要自定义应用名称
  • 多应用场景:区分不同的 Flask 应用实例
  • 测试环境:需要特定的应用标识

关于 __name__

在 python 中,__name__的值取决于当前模块是如何被执行的

  1. 当模块被直接执行时,__name__ = "__main__"
  2. 当模块被导入时, __name__ = "模块名"

可以运行以下示例尝试

python 复制代码
# demo_name.py
print(f"当前模块的 __name__ 值是: '{__name__}'")

def some_function():
    print(f"在函数内部,__name__ 仍然是: '{__name__}'")

if __name__ == '__main__':
    print("这个模块被直接执行了!")
    some_function()
else:
    print("这个模块被导入了!")
    print(f"导入时的模块名是: {__name__}") 
python 复制代码
# import_demo.py
#!/usr/bin/env python3
# -*- coding: utf-8 -*-

print("开始导入 demo_name 模块...")
import demo_name
print("导入完成!") 

__name__ 的值取决于:

执行方式 __name__ 的值 说明
python file.py "__main__" 直接执行
import file "file" 被导入
from file import func "file" 被导入


运行

python 复制代码
# run.py
from app import app
if __name__ == '__main__':
app.run(host='0.0.0.0', port=10630, debug=True)

host='0.0.0.0'

  • 作用:指定服务器监听的网络接口
  • 0.0.0.0:监听所有网络接口(包括外部访问)
  • 127.0.0.1:只监听本地回环接口(仅本机访问)
  • 实际效果:允许从其他设备访问你的应用

port=10630

  • 作用:指定服务器监听的端口号
  • 默认值:通常是 5000
  • 自定义端口:10630
  • 访问地址:http://localhost:10630http://你的IP:10630

debug=True

  • 作用:启用调试模式
  • 功能:
    • 代码修改后自动重启服务器
    • 显示详细的错误信息
    • 提供交互式调试器
  • 注意:生产环境应该设置为 False

启动方式1

直接运行

shell 复制代码
python run.py

启动方式2

在项目目录下新建 .env文件,设置一些环境变量,这些环境变量的命名方式都是 Flask 规定的,这样指定环境变量的好处就是我们可以通过控制台执行 flask run 命令来启动服务。 需要注意的是,如果你通过 flask run 命令来启动服务,那么 Flask 的配置会默认以环境变量为准,并且会忽略 run.py 中的配置项。

ini 复制代码
# 当前环境
FLASK_ENV=development
# 是否开启调试模式
FLASK_DEBUG=True
# 项目入口文件
FLASK_APP=run.py
# 运行地址
FLASK_RUN_HOST=0.0.0.0
# 运行端口
FLASK_RUN_PORT=5003

# 应用密钥
SECRET_KEY=your-secret-key-change-in-production
shell 复制代码
flask run
# 或者 python run.py


SECRET_KEY的作用: 以下内容来自询问 ChatGPT 的回答,问题:"python FLASK 应用中的 SECRET_KEY 可以解释一下吗"(2025-06-27)

在Flask 应用中,SECRET_KEY 是一个非常关键的安全配置,它主要用于以下几个方面:

🔐 为什么要设置 SECRET_KEY?

  1. 签名与加密 Session Cookie Flask 默认将会话数据存储在客户端的 Cookie 中,为防止被篡改,Flask 使用该密钥对 Cookie 进行签名和加密。没有设置 SECRET_KEY,会导致 RuntimeError: The session is unavailable because no secret key was set
  2. 防止伪造和篡改 SECRET_KEY 相当于"盐"(salt),用于在生成签名时混淆数据,防止攻击者伪造 Cookie 或篡改会话内容。
  3. 支持安全扩展(如 Flask-WTF、Flash、Flask‑Login) 多个依赖签名机制的扩展都需要 SECRET_KEY 来生成 CSRF 令牌、闪现消息等功能的完整性与安全性 。

🛠 设置方法

  • 开发环境:一般使用 os.urandom(24) 自动生成随机密钥,方便快捷
  • 生产环境 :需要使用一个固定且强随机的密钥,并且保密。推荐做法:
    • 生成方法:
go 复制代码
import secrets
print('安全的 SECRET_KEY:', secrets.token_hex(32))"
  • 设置为环境变量
  • 配置文件或实例里,不硬编码在代码中。

✅ 最佳实践

  • 秘钥长度:至少 24 字节(192 位),或者生成的 48 位十六进制字符串更安全 。
  • 保密性:绝不将 SECRET_KEY 提交到代码仓库,或暴露在公共位置。
  • 稳定性:生产环境中不要每次启动都重新生成,否则用户登录状态会丢失 。
  • 管理方式:使用环境变量或配置文件(如 .env、config.py)来加载密钥 。
  • 定期更换(可选):出于安全考虑,可制定策略定期更换 SECRET_KEY,并做好用户会话过期机制迁移 。

🔁 完整示例

python 复制代码
# 生产环境加载方式
import secrets
from flask import Flask

app = Flask(__name__)
app.config['SECRET_KEY'] = os.environ.get('FLASK_SECRET_KEY', secrets.token_hex(32))

# 使用 session、Flash、CSRF 等功能

你可以按以下步骤操作:

  1. 本地生成强随机密钥,例如:
scss 复制代码
python -c "import secrets; print(secrets.token_hex(24))"
  1. 设置环境变量:
ini 复制代码
export FLASK_SECRET_KEY="生成的长随机字符串"
  1. 在 Flask 中加载: app.config['SECRET_KEY'] = os.environ['FLASK_SECRET_KEY']

🧠 总结

  • SECRET_KEY 是保护 Flask 会话与签名机制的核心密钥。
  • 它使服务端能够验证客户端传来的 Cookie 是否被篡改,防止伪造。
  • 推荐在开发中临时使用随机密钥;但生产环境必须配置固定、复杂且保密的密钥,并用环境变量或配置文件管理。


flask runpython run.py两个启动方式的区别:

Flask CLI启动

  1. Flask CLI 程序启动
  2. 读取环境变量FLASK_APP=run.py
  3. 确定应用入口点

模块导入过程

python 复制代码
# FLASK CLI 内部执行
import run

导入时的执行顺序:

  1. 执行 run.py 的第一行:from app import app
  • 导入 app/__init__.py 模块
  • 执行 app/__init__.py 中的所有代码
  • 创建 Flask 应用实例和 API 实例
  • 注册 Hello 资源到 /hello 路由
  1. 执行 run.py 的第二行:if __name__ == '__main__':
  • 此时 __name__ = "run"(不是 "__main__"
  • 条件为 False,不执行 app.run()

python run.py vs flask run对比

方面 python run.py flask run
执行方式 直接执行脚本 通过 Flask CLI
__name__ "__main__" "run"
服务器启动 app.run(host='0.0.0.0', port=10630, debug=True) app.run(host='0.0.0.0', port=5003, debug=True)
配置来源 代码中硬编码 环境变量 + 默认值

测试

可以通过浏览器或者postman 等工具访问 http://127.0.0.1:5003/hello来测试是否返回了{"message": "test"}JSON 字符串

目录结构优化

对于代码

python 复制代码
# app/__init__.py
from flask import Flask
from flask_restful import Resource, Api
app = Flask(__name__)
# app = Flask('wzf')
api = Api(app)

class Hello(Resource):
    def get(self):
        return {'message': 'test'}

api.add_resource(Hello, '/hello')

可以发现,apiResource等的管理都在一个文件中进行,如果我们需要引入数据库连接等功能或者新增很多其他的业务功能,这种显然是不合理的。因此,按照以下的项目目录结构来组织代码,增强项目的可扩展性

bash 复制代码
/
├── .venv/
├── app/
│   └── api/ # api 接口模块
│       └── __init__.py # 注册以及生成蓝图
│       └── common/ # 公共方法
│       └── models/ # 模型,与数据库相关
│       └── resources/ # 接口
│       └── schema/ # 校验
│   └── __init__.py # 整个应用的初始化
│   └── config.py # 配置项
│   └── manage.py # 数据库迁移工具管理
├── .env # 环境变量
├── run.py # 入口文件

mysql数据库连接

环境准备

python 相关库安装

pip install Flask-SQLAlchemy Flask-Migrate pymysql

MySQL环境安装

我这里使用了 docker 环境来安装,当然,直接在 MySQL 官网下载对应的安装包也可以

  1. 先拉取 MySQL 镜像
bash 复制代码
docker pull --platform linux/arm64 mysql:8.4.5 # --platform linux/arm64 指定 arm 版本安装 x86平台安装时可以删除
  1. 使用 MySQL 镜像新建容器(虚拟机)
shell 复制代码
docker run -p 3306:3306 --name  sample-mysql -e MYSQL_ROOT_PASSWORD=123456 -d mysql:8.4.5

# 把MySQL的存储data文件、存储配置文件挂载出来,所以用以下更全的会相对好一些,这样重启容器也不会造成丢失数据
docker run -p 3306:3306 --name  sample-mysql -e MYSQL_ROOT_PASSWORD=123456 -v /Users/maplewan/docker-data/sample-mysql/log:/var/log/mysql -v /Users/maplewan/docker-data/sample-mysql/data:/var/lib/mysql -v /Users/maplewan/docker-data/sample-mysql/conf:/etc/mysql -d mysql:8.4.5
# 把宿主机目录 /Users/maplewan/docker-data/sample-mysql/conf 全量挂载到了容器的 /etc/mysql,但这个目录里缺少了 MySQL 启动时会自动查找的子目录 /etc/mysql/conf.d(和 /etc/mysql/mysql.conf.d)。因此在执行 includedir 指令时找不到路径,就报错退出
mkdir -p /Users/maplewan/docker-data/sample-mysql/conf/conf.d
mkdir -p /Users/maplewan/docker-data/sample-mysql/conf/mysql.conf.d
  1. 连接测试 可以使用 DBeaver 软件来连接测试

可能会出现Public Key Retrieval is not allowed MySQL的问题

通常出现在使用 MySQL 8+ 数据库时,客户端尝试通过用户名密码连接,但使用了 caching_sha2_password 认证插件,而 JDBC 连接配置中没有允许公钥检索。

可以在 JDBC_URL中添加参数allowPublicKeyRetrieval=true&useSSL=false 如:jdbc:mysql://localhost:3306/your_db?allowPublicKeyRetrieval=true&useSSL=false

使用数据库实现注册功能

1. 更新.env文件相关变量

ini 复制代码
# .env

# 当前环境
FLASK_ENV=development
# 是否开启调试模式
FLASK_DEBUG=True
# 项目入口文件
FLASK_APP=run.py
# 运行地址
FLASK_RUN_HOST=127.0.0.1
# 运行端口
FLASK_RUN_PORT=5003

# 应用密钥
SECRET_KEY=7b4bd29d8001569cd6a72aa335c84419286fcdcaa1d075131ba3adac41cef4cb

# 数据库配置相关
MYSQL_USER_NAME=root
MYSQL_USER_PASSWORD=123456
MYSQL_HOSTNAME=127.0.0.1
MYSQL_PORT=3307
MYSQL_DATABASE_NAME=sample

2. 创建config类管理配置

python 复制代码
# app/cnfig.py

import os

# 环境变量读取本地 .env 文件

# 数据库相关配置
# 用户名
USERNAME = os.getenv('MYSQL_USER_NAME')
# 密码
PASSWORD = os.getenv("MYSQL_USER_PASSWORD")
# 主机
HOSTNAME = os.getenv("MYSQL_HOSTNAME")
# 端口
PORT = os.getenv("MYSQL_PORT")
# 数据库
DATABASE = os.getenv("MYSQL_DATABASE_NAME")

# 数据库连接相关
DIALECT = "mysql"
DRIVER = "pymysql"

class Config(object):
    DEBUG = False
    TESTING = False
    SECRET_KEY = os.getenv("SECRET_KEY")
    SQLALCHEMY_DATABASE_URI = f"{DIALECT}+{DRIVER}://{USERNAME}:{PASSWORD}@{HOSTNAME}:{PORT}/{DATABASE}"
    print(SQLALCHEMY_DATABASE_URI)
    SQLALCHEMY_ECHO = False

class DevelopmentConfig(Config):
    DEBUG = True

class ProductionConfig(Config):
    DEBUG = False
    SQLALCHEMY_DATABASE_URI = ""

class TestingConfig(Config):
    DEBUG = True
    TESTING = True

config = {
    'development': DevelopmentConfig,
    'production': ProductionConfig,
    'testing': TestingConfig,
    'default': DevelopmentConfig
}

3. 初始化数据库连接与数据库迁移工具

python 复制代码
# app/api/models/__init__.py
from flask_sqlalchemy import SQLAlchemy
db = SQLAlchemy()

# app/manage.py 
from flask_migrate import Migrate
migrate = Migrate()

4. 创建数据库模型类

python 复制代码
# app/api/models/user.py

from . import db
from datetime import datetime

class User(db.Model):
    __tablename__ = 'user' # 表名 与 数据库中的表名一一对应
    
    # 主键 id
    id = db.Column(db.Integer(), primary_key=True, nullable=False, autoincrement=True, comment='主键ID')
    # 用户名
    username = db.Column(db.String(40), nullable=False, default='', comment='用户姓名')
    # 密码
    pwd = db.Column(db.String(255), comment='密码')
    # salt
    salt = db.Column(db.String(32), comment='salt')
    # 创建时间
    created_at = db.Column(db.DateTime(), nullable=False, default=datetime.now, comment='创建时间')
    # 更新时间
    updated_at = db.Column(db.DateTime(), nullable=False, default=datetime.now, onupdate=datetime.now, comment='更新时间')

    # 新增用户
    def addUser(self):
        db.session.add(self)
        db.session.commit()

    # 用户信息
    def dict(self):
        return {
            'id': self.id,
            'username': self.username,
            'pwd': self.pwd,
            'salt': self.salt,
            # 'created_at': self.created_at.isoformat() if self.created_at else None,
            # 'updated_at': self.updated_at.isoformat() if self.updated_at else None
            'created_at': format_datetime_to_json(self.created_at),
            'updated_at': format_datetime_to_json(self.updated_at)
        }
    
    # 获取密码和salt
    def getPwd(self):
        return {
            'pwd': self.pwd,
            'salt': self.salt
        }
    
    # 按 username 查询用户
    @classmethod
    def findUserByUsername(cls, username):
        return db.session.execute(db.select(cls).filter_by(username=username)).first()
    
    # 返回所有用户
    @classmethod
    def findAllUser(cls):
        return db.session.query(cls).all()

5. 使用蓝图,用于接口分模块管理

创建一些公共的 utils 和入参校验方法

python 复制代码
# app/api/common/utils.py
# 公共 response 方法
def res(data=None, message='Ok', success=True, code=200):
    return {
        'success': success,
        'message': message,
        'data': data,
    }, code
# 格式化时间,不然时间转 json 格式的时候会报错
def format_datetime_to_json(datetime, format_str='%Y-%m-%d %H:%M:%S'):
	return datetime.strftime(format_str)


# app/api/schema/register_sha.py
def register_args_valid(parser):
    parser.add_argument('username', type=str, location='json')
    parser.add_argument('password', type=str, dest='pwd', location='json')

1. 创建注册服务接口

python 复制代码
# app/api/resources/register.py
import uuid

from flask_restful import Resource, reqparse
from werkzeug.security import generate_password_hash

from ..models.user import User
from ..common.utils import res
from ..schema.register_sha import register_args_valid

class Register(Resource):
    def post(self):
        parser = reqparse.RequestParser()
        # parser.add_argument('username', type=str, location='json')
        # parser.add_argument('password', type=str, dest='pwd', location='json')
        register_args_valid(parser)
        data = parser.parse_args()
        if User.findUserByUsername(data['username']):
            # return {'success': False, 'message': '用户名已存在', 'data': None }, 400
            return res(message='用户名已存在', success=False, code=400)
        else:
            try:
                data['salt'] = uuid.uuid4().hex
                data['pwd'] = generate_password_hash('{}{}'.format(data['salt'], data['pwd']))
                user = User(**data)
                user.addUser()
                # return {'success': True, 'message': '注册成功', 'data': user.dict()}, 201
                return res(data=user.dict(), message='注册成功', success=True, code=201)
            except Exception as e:
                # return {'success': False, 'message': '注册失败,{}'.format(e), 'data': None}, 500
                return res(message='注册失败,{}'.format(e), success=False, code=500)

2. 蓝图初始化与相关Resource添加

在实际开发中,我们会将业务接口拆分模块,比如 /api/xxx,所以现在我们需要创建一个 api 蓝图来统一管理,在 /app/api/__init__.py 文件中写入以下代码:

python 复制代码
# app/api/__init__.py

from flask import Blueprint
from flask_restful import Api
from .resources.register import Register

api_blueprint = Blueprint('api', __name__, url_prefix='/api')
api = Api(api_blueprint)

api.add_resource(Register, '/register')

6. 创建 Flask 对象并初始化相关对象

python 复制代码
# app/__init__.py

# from flask import Flask
# from flask_restful import Resource, Api
# app = Flask(__name__)
# api = Api(app)
# class Hello(Resource):
#     def get(self):
#         return {'message': 'test'}
# api.add_resource(Hello, '/hello')

import os
from flask import Flask
from .config import config
from .api.models import db
from .api import api_blueprint
from .manage import migrate

def create_app(config_name):
    # 初始化Flask项目
    app = Flask(__name__)
    # 加载配置
    app.config.from_object(config[config_name])
    # 初始化数据库
    db.init_app(app)  # init_app 会去读取 app.config 中相关的数据库连接配置,连接数据库
    # 初始化迁移
    migrate.init_app(app, db)
    # 注册蓝图
    app.register_blueprint(api_blueprint)
    return app

# 创建app实例
app = create_app(os.getenv('FLASK_ENV', 'development'))

7. 初始化(创建/更新)数据库表

shell 复制代码
# 第一次初始化时使用(会在项目目录下创建一个 migrations 文件夹
flask db init 

# 后面每次修改数据库字段(修改app/api/models中相关文件时)时使用,只需要写 app/api/models 下的相关的模型类,通过以下的命令可以生成对应的表(关于联表相关还未涉及,后续补充)
flask db migrate -m '相关信息'
flask db upgrade

# flask db migrate命令可能会报错 ERROR [flask_migrate] Error: 'cryptography' package is required for sha256_password or caching_sha2_password auth methods,可以通过命令安装以下库解决
pip install cryptography

8. 测试接口

运行

shell 复制代码
flask run

如果已经运行,flask 在开发环境下有热更新机制,会自动部署,则不需要执行以上代码

可以使用 postman 等相关工具,使用POST方法请求http://127.0.0.1:5003/api/register,记得带上 body参数。如:

json 复制代码
{
    "username": "maplewan",
    "password": "123"
}

实现登录登出功能(接口鉴权相关)

登录相关

1. 安装相关库实现Token的创建与校验

shell 复制代码
pip install Flask-JWT-Extended

2. config.py配置类中新增相关配置

  • JWT_SECRET_KEY = os.getenv("JWT_SECRET_KEY")
    • 作用:设置 JWT(JSON Web Token)签名和解密用的密钥。可参考上文中的SECRET\_KEY的作用
    • 来源:通常从环境变量 .env 文件中读取,避免硬编码在代码里。
  • JWT_ACCESS_TOKEN_EXPIRES = timedelta(hours=1)
    • 作用:设置 access token(访问令牌)的有效期,这里是 1 小时。
    • 含义:用户登录后获得的 access token 只能用 1 小时,过期后需要用 refresh token 换新。
    • 安全性:有效期短可以减少 token 泄露带来的风险。
  • JWT_REFRESH_TOKEN_EXPIRES = timedelta(days=30)
    • 作用:设置 refresh token(刷新令牌)的有效期,这里是 30 天。
    • 含义:refresh token 有效期更长,用户 access token 过期后可以用 refresh token 换取新的 access token,无需重新登录。
    • 安全性:refresh token 也要妥善保管,泄露后风险较大。
  • JWT_BLOCKLIST_TOKEN_CHECKS = ['access']
    • 作用:指定哪些类型的 token 需要检查是否在 blocklist(黑名单)中。
    • 常见用法:比如用户登出、账号被封禁时,可以把某些 token 加入 blocklist,防止被继续使用。
    • ['access']:只检查 access token 是否被拉黑。你也可以写 ['access', 'refresh'],这样 access 和 refresh token 都会被检查。
python 复制代码
# app/config.py

# ...
from datetime import timedelta
# ...
class Config(object):
    # ...
    # JWT 相关配置
	JWT_SECRET_KEY = os.getenv("JWT_SECRET_KEY") # 密钥
	JWT_ACCESS_TOKEN_EXPIRES = timedelta(hours=1) # 1小时
	JWT_REFRESH_TOKEN_EXPIRES = timedelta(days=30) # 30天
	JWT_BLOCKLIST_TOKEN_CHECKS = ['access'] # 检查类型
	# ...

需要注意的是:当你的 access token 是在 14:01获取得到的,那么你的15:01时失效,哪怕你在15:00的时候使用了它。这样的用户交互其实是有问题的。因此我们考虑"每次用token都自动延长有效期",自己实现"滑动过期"机制。

  • 通常做法是:每次用户操作时,检测access token快过期了,就用refresh token自动换一个新的access token。

因此我们在编写登录接口时(login.py),可以将access token的过期时间返回给前端,前端来检测当access token 快过期的时候(比如 < 2min),使用 refresh token 来重新拉取一下 access token

3. .env添加相关变量

ini 复制代码
# .env

# 登录验证相关参数
# JWT密钥
JWT_SECRET_KEY=3281700dd2dc233be7dcf77085e4d42172a1d74910f771576e46439a47281e73

4. 创建 Flask 应用时初始化 JWT 扩展

python 复制代码
# app/__init__.py

# ...
from flask_jwt_extended import JWTManager
# ...
def create_app(config_name):
    # ...
    # 初始化 JWT
    jwt = JWTManager(app)
    return app
# ...

5. 实现 login 相关接口

与实现"注册"功能类似,新建一个 Login 类,并且定义了一个 post 函数表明该接口是 POST 请求。因为登录接口传入的参数和注册接口一致,所以直接引入注册接口的校验函数。解析完参数后,判断该用户是否已经注册,如果没注册则抛出错误,如果注册了则进行密码校验,校验通过了就使用扩展提供的函数新建两个 Token,其中 access_token 是用来鉴权的,有效期 1 小时(在 config.py 中配置的)。

为了避免用户需要频繁的重新登录,再生成一个refresh_token,当access_token 过期后使用 refresh_token 来换取新的 access_token,当然,refresh_token 也有 30 天的有效期。 因此还需要写一个get 函数来实现通过refresh_token获取access_token。加上 @jwt_required 装饰器,当加上该装饰器时,JWT 扩展会为我们自动在调用此接口时做 Token 校验,它默认是只校验 access_token 的,在括号内传入 refresh=True 则表示用有效的 refresh_token 可以通过校验。

python 复制代码
# app/api/resources

from datetime import datetime
import uuid

from flask_restful import Resource, reqparse
from flask_jwt_extended import create_access_token, create_refresh_token, decode_token, get_jwt_identity, jwt_required
from werkzeug.security import check_password_hash

from ..schema.register_sha import register_args_valid
from ..models.user import User
from ..common.utils import res

def generate_tokens(id):
    access_token = create_access_token(identity=id)
    refresh_token = create_refresh_token(identity=id)
    return {
        'access_token': 'Bearer ' + access_token,
        'refresh_token': 'Bearer ' + refresh_token
    }

class Login(Resource):
    def post(self):
        # 解析请求参数
        parser = reqparse.RequestParser()
        # 请求参数校验
        register_args_valid(parser)
        data = parser.parse_args()
        username = data['username']
        user_tuple = User.findUserByUsername(username)
        if user_tuple:
            try:
                (user, ) = user_tuple
                pwd, salt = user.getPwd().get('pwd'), user.getPwd().get('salt')
                valid = check_password_hash(pwd, '{}{}'.format(salt, data['pwd']))
                if valid:
                    # 生成 token
                    tokens_data = generate_tokens(username)
                    decoded_token = decode_token(tokens_data['access_token'].split(' ')[1]) # 解析过期时间返回给前端
                    return res(data={
                        'access_token': tokens_data['access_token'],
                        'refresh_token': tokens_data['refresh_token'],
                        'exp': decoded_token['exp'] * 1000, # 将时间戳转换为毫秒
                    }, message='success', success=True, code=200)
                else:
                    return res(message='密码错误', success=False, code=401)
            except Exception as e:
                return res(data=None, message='登录失败,{}'.format(e), success=False, code=500)
        else:
            return res(message='用户不存在', success=False, code=400)

    @jwt_required(refresh=True)
    def get(self):
        # access_token 过期后,使用 refresh_token 获取新的 access_token
        # 可以先从 refresh_token 中获取用户名,再生成新的 access_token
        current_username = get_jwt_identity()

        # 在生成新的 token
        access_token = create_access_token(identity=current_username)
        return res(data={'access_token': 'Bearer ' + access_token}, message='获取新的 access_token 成功', success=True, code=200)

6. 注册登录接口

python 复制代码
# app/__init__.py

from .resources.login import Login
api.add_resource(Login, '/login')
# api.add_resource(Login, '/login', '/refresh', '/test')  # 可以添加多个路由

7. 测试登录接口

可以使用 postman 等相关工具,请求http://127.0.0.1:5003/api/login

  1. POST 方法 请求body
json 复制代码
{
	"username": "admin",
	"password": "admin"
}
  1. GET 方法 请求header,注意 Authorization的值为 POST 方法得到的 refresh_token
makefile 复制代码
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJmcmVzaCI6ZmFsc2UsImlhdCI6MTc1MTI2ODg2OSwianRpIjoiZTVkZTEyNTItMDEzOC00ZjIwLTkxM2UtNjY3ZDdkNTY5ZTA4IiwidHlwZSI6InJlZnJlc2giLCJzdWIiOiJhZG1pbiIsIm5iZiI6MTc1MTI2ODg2OSwiY3NyZiI6ImZmODJlNmE0LTA3MzItNDkzYS05NjNlLWUwMDY4OTdmY2RhOCIsImV4cCI6MTc1Mzg2MDg2OX0.nfQAl015FxxMJKosTdYQj1y9_P2MJZsjp19kVJbnfpU

登出相关

在用户退出登录后,要销毁 Token。首先我们需要一个表来存放已经销毁的 Token,在 app/api/models 下新建 revoked_token.py 文件:

1. 创建数据库模型类:revoked_token.py

python 复制代码
# app/api/models/revoked_token.py

from . import db

class RevokedToken(db.Model):
    __tablename__ = 'revoked_token'

    id = db.Column(db.Integer(), primary_key=True, nullable=False, autoincrement=True, comment='主键ID')
    jti = db.Column(db.String(120), nullable=False, comment='JWT ID')

    def add(self):
        db.session.add(self)
        db.session.commit()

    # 检查 JWT ID 是否在黑名单中
    @classmethod
    def is_jti_blacklisted(cls, jti):
        return cls.query.filter_by(jti=jti).first() is not None

2. 创建登出服务类:logout.py

创建一个 revoked_token 表,用来存放已经销毁的 Token,并且定义一个查询的方法,用来查询 Token 是否已销毁。 然后在 app/api/resources 下新建 logout.py 写入登出接口逻辑:用户退出登录时,先获取到 Token 中的唯一标识 jti 然后将它加入销毁 Token 的表中。

python 复制代码
# app/api/resources/logout.py

from flask_restful import Resource
from flask_jwt_extended import jwt_required, get_jwt
from ..models.revoked_token import RevokedToken
from ..common.utils import res

class Logout(Resource):
    @jwt_required()
    def post(self):
        jti = get_jwt()['jti']
        try:    
            revoked_token = RevokedToken(jti=jti)
            revoked_token.add()
            return res(data=None, message='退出成功', success=True, code=200)
        except Exception as e:
            return res(data=None, message='服务器繁忙', success=False, code=500)

3. 注册登出接口

python 复制代码
# app/__init__.py

from .resources.logout import Logout
api.add_resource(Logout, '/logout')

4. 注册JWT钩子函数,检查token是否在黑名单中

需要注册一个 JWT 扩展提供的钩子函数,用来校验 Token 是否在销毁列表中。在 app/__init__.py 中添加以下内容,这样当用户在调用需要鉴权的接口时,JWT 扩展会先调用钩子函数校验是否是已经销毁的 Token

python 复制代码
# app/__init__.py

# ...
from flask_jwt_extended import JWTManager
# ...

def create_app(config_name):
    # ...
    # 初始化 JWT
    jwt = JWTManager(app)
    register_JWT_hooks(jwt)
    return app

def register_JWT_hooks(jwt):
    # 注册JWT钩子函数,用于检查token是否在黑名单中
    @jwt.token_in_blocklist_loader
    def check_if_token_in_blocklist(jwt_header, jwt_payload):
        jti = jwt_payload['jti']
        return RevokedToken.is_jti_blacklisted(jti)

# ...

5. 更新数据库表

新增了一个 revoked_tokenModel,需要更新一下数据库表,一下命令会在数据库中新建一个revoked_token

shell 复制代码
flask db migrate -m "添加 revoked token 表"
flask db upgrade

6. 测试接口

url: http://127.0.0.1:5003/api/logout

method: POST

header: Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJmcmVzaCI6ZmFsc2UsImlhdCI6MTc1MTI4ODUyMiwianRpIjoiMjM0ZTczODUtZDQ5Yy00OTg2LWJlYjYtMDcxMjI0NGJlNTQzIiwidHlwZSI6ImFjY2VzcyIsInN1YiI6ImFkbWluIiwibmJmIjoxNzUxMjg4NTIyLCJjc3JmIjoiMzFmNDZjZWUtZDQ4Ni00ZDNmLWEyYmQtMWI3MTRhMzE4OWM2IiwiZXhwIjoxNzUxMjkyMTIyfQ.De01aQccl_MWK0srrcKKQSNgZ9bBqnopXuCI211a0AE

response:

json 复制代码
{
    "success": true,
    "message": "退出成功",
    "data": null
}

接口调用成功之后如果再次调用,由于access_token已经失效,会报错,如下

json 复制代码
{
    "msg": "Token has been revoked"
}

关于为什么登出一定要维护一张 revoked_token 表?可参考以下:



问题:logout 必须维护一张 revoked_token 的表吗 jwt 没有方法直接是 token 失效吗

以下为 gpt 总结,仅供参考

JWT(无论是 Flask-JWT-Extended 还是其他实现)本身没有办法让某个 token 立即失效 ,所以如果你想实现"登出即失效",必须维护一张 revoked_token(黑名单)表或集合

详细解释

  1. JWT 的无状态特性
  • JWT 生成后,服务端不会保存任何 token 状态。
  • 只要 token 没过期,且签名正确,任何人拿到都能用。
  • 这意味着服务端无法主动让某个 token 失效,除非你有"黑名单"机制。
  1. 为什么需要 revoked_token 表?
  • 用户登出、账号被封、手动踢下线等场景,都需要让某个 token 立即失效。
  • 这时只能把 token 的唯一标识(如 jti)存到数据库或缓存(如 Redis)里,每次校验 token 时查一下是否在黑名单。
  1. Flask-JWT-Extended 的推荐做法
  • 你可以用 set、数据库表、Redis 等存储被拉黑的 token 的 jti。
  • 每次请求时,@jwt.token_in_blocklist_loader 回调会自动检查。

示例:

python 复制代码
BLACKLIST = set()

@jwt.token_in_blocklist_loader
def check_if_token_revoked(jwt_header, jwt_payload):
    jti = jwt_payload['jti']
    return jti in BLACKLIST

@app.route('/logout', methods=['POST'])
@jwt_required()
def logout():
    jti = get_jwt()['jti']
    BLACKLIST.add(jti)
    return jsonify(msg="Token revoked")
  1. JWT 没有"直接失效"方法的原因
  • JWT 的设计就是无状态的,服务端不存 token。
  • 只要 token 没过期,理论上永远有效。
  • 只有通过"黑名单"机制,才能让某个 token 立即失效。
  1. 最佳实践建议
  • 生产环境推荐用 Redis 存 revoked token,性能高、可过期。
  • 只存还没过期的 token 的 jti,定期清理。
  • 如果只用内存 set,重启服务后黑名单会丢失,不适合生产。

总结

  • JWT 没有内置"让 token 立即失效"的方法,只能靠黑名单机制。
  • 登出、封号等场景,必须维护 revoked_token 表/集合
  • 这是 JWT 的通用设计,不是 Flask-JWT-Extended 的限制。


查找所有用户

python 复制代码
# app/api/resources/user.py

from flask_restful import Resource
from flask_jwt_extended import jwt_required, get_jwt_identity
from ..models.user import User
from ..common.utils import res

class UserService(Resource):
    @jwt_required()
    def get(self):
        userList = User.findAllUser()
        result = [user.dict() for user in userList]
        return res(data=result, message='success', success=True, code=200)
python 复制代码
# app/__init__.py
from .resources.user import UserService
api.add_resource(UserService, '/user')

测试接口http://127.0.0.1:5003/api/user

method: POST

header: Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJmcmVzaCI6ZmFsc2UsImlhdCI6MTc1MTI4ODUyMiwianRpIjoiMjM0ZTczODUtZDQ5Yy00OTg2LWJlYjYtMDcxMjI0NGJlNTQzIiwidHlwZSI6ImFjY2VzcyIsInN1YiI6ImFkbWluIiwibmJmIjoxNzUxMjg4NTIyLCJjc3JmIjoiMzFmNDZjZWUtZDQ4Ni00ZDNmLWEyYmQtMWI3MTRhMzE4OWM2IiwiZXhwIjoxNzUxMjkyMTIyfQ.De01aQccl_MWK0srrcKKQSNgZ9bBqnopXuCI211a0AE,注意这里需要是login获取得到的 access_token 的值

解决跨域问题

1. python 相关库安装

shell 复制代码
pip install Flask-Cors

2. 在创建 Flask 应用时初始化

python 复制代码
# app/__init__.py

# ...
from flask_cors import CORS

def create_app(config_name):
	#...    
    # 解决跨域
    CORS(app)
    # ...

3. 为什么要解决跨域问题?

问题:请帮我详细解释一下为什么要 CORS(app)

以下为 gpt 总结,仅供参考

为什么要在 Flask 项目中使用 CORS(app)

什么是 CORS?

CORS(Cross-Origin Resource Sharing,跨域资源共享)是一种浏览器的安全机制,用于允许或限制不同源(协议+域名+端口)之间的资源请求。

  • 同源策略:浏览器默认只允许同源(协议、域名、端口都相同)的请求访问资源。
  • 跨域请求:当前端页面和后端API不在同一个域(或端口)时,浏览器会拦截请求,除非后端明确允许。

为什么要用 CORS(app)

1. 前后端分离项目必需
  • 现代开发中,前端(如 React/Vue/Angular)和后端(如 Flask)通常分开部署,端口或域名不同。
  • 例如:前端 http://localhost:3000,后端 http://localhost:5000,这就是跨域。
2. 浏览器安全限制
  • 浏览器会自动拦截跨域请求,前端会报错:
    Access to XMLHttpRequest at 'http://localhost:5000/api' from origin 'http://localhost:3000' has been blocked by CORS policy
3. 后端需要明确声明允许跨域
  • 只有后端响应头里有 Access-Control-Allow-Origin,浏览器才允许前端访问数据。
  • Flask 默认不会加这个头,所以需要用 CORS(app)

CORS(app) 的作用

  • CORS(app) 是 Flask-CORS 插件的用法。
  • 它会自动为所有响应加上 Access-Control-Allow-Origin: *(或你指定的域名)。
  • 这样前端就能正常访问后端接口,不会被浏览器拦截。

示例:

python 复制代码
from flask import Flask
from flask_cors import CORS

app = Flask(__name__)
CORS(app)  # 允许所有域名跨域访问

你也可以只允许特定域名:

python 复制代码
CORS(app, origins=["http://localhost:3000"])

常见场景

  • 本地开发:前端和后端端口不同,必须加 CORS。
  • 生产环境:前端和后端分开部署,域名不同,也要加 CORS。
  • 第三方API:如果你要开放API给别人用,也要加 CORS。

五、安全建议

  • 开发环境 可以用 CORS(app) 允许所有域名。
  • 生产环境建议只允许可信域名,防止被恶意网站调用。

六、总结

  • CORS 是浏览器的安全机制,防止未授权的跨域请求。
  • Flask 默认不支持跨域,必须用 Flask-CORS 插件。
  • CORS(app) 让你的 API 能被前端安全、正常地访问。

总结

以上是 使用 python Flask 搭建具有登录注册功能的Restful API 基本后端代码模板。

代码地址:https://github.com/MapleWan/flask_restful_template

最终目录结构

shell 复制代码
.
├── app
│   ├── __init__.py
│   ├── api
│   │   ├── __init__.py
│   │   ├── common
│   │   │   └── utils.py
│   │   ├── models
│   │   │   ├── __init__.py
│   │   │   ├── revoked_token.py
│   │   │   └── user.py
│   │   ├── resources
│   │   │   ├── login.py
│   │   │   ├── logout.py
│   │   │   ├── register.py
│   │   │   └── user.py
│   │   └── schema
│   │       └── register_sha.py
│   ├── config.py
│   └── manage.py
├── requirements.txt
└── run.py

其实从以上目录结构可以看出,其实modelsresources文件夹内还可以进一步按照模块来组织等等,更多最佳实践有待进一步探索

相关推荐
WanderInk32 分钟前
在递归中为什么用 `int[]` 而不是 `int`?——揭秘 Java 参数传递的秘密
java·后端·算法
why技术36 分钟前
哎,我糊涂啊!这个需求居然没想到用时间轮来解决。
java·后端·面试
寻月隐君1 小时前
Rust 核心概念解析:引用、借用与内部可变性
后端·rust·github
万粉变现经纪人1 小时前
如何解决pip安装报错ModuleNotFoundError: No module named ‘django’问题
后端·python·pycharm·django·numpy·pandas·pip
ai小鬼头1 小时前
创业心态崩了?熊哥教你用缺德哲学活得更爽
前端·后端·算法
夕颜1112 小时前
关于 Python 的踩坑记录
后端
LaoZhangAI2 小时前
2025年虚拟信用卡订阅ChatGPT Plus完整教程(含WildCard停运后最新方案)
前端·后端
愿你天黑有灯下雨有伞2 小时前
企业级异常处理方案:Spring Boot自定义异常全局拦截实战
java·spring boot·后端
蓝倾4 小时前
淘宝获取商品分类接口操作指南
前端·后端·fastapi
小希爸爸4 小时前
curl 网络测试常用方法
后端