使用基于角色的访问控制 (RBAC) 和 JWT 验证保护你的 Flask API
本指南将帮助你通过使用 基于角色的访问控制 (RBAC) 和由 Logto 签发的 JSON Web Token (JWT) 来实现授权 (Authorization),从而保护你的 Flask API。
开始之前
你的客户端应用需要从 Logto 获取访问令牌 (Access tokens)。如果你还没有完成客户端集成,请查看我们的 快速开始,适用于 React、Vue、Angular 或其他客户端框架,或者参考我们的 机器对机器指南 以实现服务器到服务器的访问。
本指南聚焦于在你的 Flask 应用中对这些令牌进行服务端验证。

你将学到什么
- JWT 验证:学习如何验证访问令牌 (Access tokens) 并提取认证 (Authentication) 信息
- 中间件实现:创建可复用的中间件以保护 API
- 权限模型:理解并实现不同的授权 (Authorization) 模式:
- 应用级端点的全局 API 资源
- 用于租户特定功能控制的组织权限
- 多租户数据访问的组织级 API 资源
- RBAC 集成:在你的 API 端点中强制执行基于角色的权限 (Permissions) 和权限范围 (Scopes)
前置条件
- 已安装 Python 的最新稳定版本
- 基本了解 Flask 及 Web API 开发
- 已配置 Logto 应用(如有需要请参见 快速开始)
权限 (Permission) 模型概览
在实施保护之前,请选择适合你应用架构的权限 (Permission) 模型。这与 Logto 的三大授权 (Authorization) 场景保持一致:
- 全局 API 资源
- 组织 (Organization)(非 API)权限 (Permissions)
- 组织级 API 资源

- 使用场景: 保护整个应用共享的 API 资源(非组织 (Organization) 专属)
- 令牌类型: 具有全局受众 (Audience) 的访问令牌 (Access token)
- 示例: 公共 API、核心产品服务、管理端点
- 最适合: 所有客户都使用 API 的 SaaS 产品、无租户隔离的微服务
- 了解更多: 保护全局 API 资源

- 使用场景: 控制组织 (Organization) 专属的操作、UI 功能或业务逻辑(非 API)
- 令牌类型: 具有组织 (Organization) 专属受众 (Audience) 的组织令牌 (Organization token)
- 示例: 功能开关、仪表盘权限 (Permissions)、成员邀请控制
- 最适合: 拥有组织 (Organization) 专属功能和工作流的多租户 SaaS
- 了解更多: 保护组织 (Organization)(非 API)权限 (Permissions)

- 使用场景: 保护在特定组织 (Organization) 上下文中可访问的 API 资源
- 令牌类型: 具有 API 资源受众 (Audience) + 组织 (Organization) 上下文的组织令牌 (Organization token)
- 示例: 多租户 API、组织 (Organization) 范围的数据端点、租户专属微服务
- 最适合: API 数据以组织 (Organization) 为范围的多租户 SaaS
- 了解更多: 保护组织级 API 资源
💡 在继续之前选择你的模型 —— 本指南后续内容将以你选择的方式为参考。
快速准备步骤
配置 Logto 资源和权限
- 全局 API 资源
- 组织(非 API)权限
- 组织级 API 资源
- 创建 API 资源:前往 控制台 → API 资源 并注册你的 API(例如,
https://api.yourapp.com
) - 定义权限:添加如
read:products
、write:orders
等权限(Scopes)——参见 定义带权限的 API 资源 - 创建全局角色:前往 控制台 → 角色 并创建包含你的 API 权限的角色——参见 配置全局角色
- 分配角色:将角色分配给需要访问 API 的用户或 M2M 应用程序
- 定义组织权限:在组织模板中创建如
invite:member
、manage:billing
等非 API 组织权限 - 设置组织角色:在组织模板中配置组织专属角色,并为其分配权限
- 分配组织角色:在每个组织上下文中将用户分配到组织角色
- 创建 API 资源:如上注册你的 API 资源,但它将在组织上下文中使用
- 定义权限:添加如
read:data
、write:settings
等限定于组织上下文的权限(Scopes) - 配置组织模板:设置包含你的 API 资源权限的组织角色
- 分配组织角色:将用户或 M2M 应用程序分配到包含 API 权限的组织角色
- 多租户设置:确保你的 API 能处理组织范围的数据和校验
从我们的 基于角色的访问控制 (RBAC) 指南 开始,获取分步设置说明。
更新你的客户端应用
在客户端请求合适的权限(Scopes):
- 用户认证 (Authentication):更新你的应用 → 以请求你的 API 权限和 / 或组织上下文
- 机器对机器:为服务器间访问 配置 M2M 权限(Scopes)→
通常需要在客户端配置中更新以下一项或多项:
- OAuth 流程中的
scope
参数 - 用于 API 资源访问的
resource
参数 - 用于组织上下文的
organization_id
请确保你测试的用户或 M2M 应用已被分配包含所需 API 权限的合适角色或组织角色。
初始化你的 API 项目
要初始化一个新的 Flask 项目,创建一个目录并设置基本结构:
mkdir your-api-name
cd your-api-name
创建一个依赖文件:
Flask
安装依赖:
pip install -r requirements.txt
创建一个基础的 Flask 应用:
from flask import Flask, jsonify
app = Flask(__name__)
@app.route('/')
def hello():
return jsonify({"message": "Hello from Flask"})
if __name__ == '__main__':
app.run(debug=True)
启动开发服务器:
python app.py
更多关于如何设置路由、蓝图以及其他功能的信息,请参考 Flask 官方文档。
初始化常量和工具方法
在你的代码中定义必要的常量和工具函数,用于处理令牌的提取和校验。一个有效的请求必须包含 Authorization
请求头,格式为 Bearer <访问令牌 (Access token)>
。
JWKS_URI = 'https://your-tenant.logto.app/oidc/jwks'
ISSUER = 'https://your-tenant.logto.app/oidc'
class AuthInfo:
def __init__(self, sub: str, client_id: str = None, organization_id: str = None,
scopes: list = None, audience: list = None):
self.sub = sub
self.client_id = client_id
self.organization_id = organization_id
self.scopes = scopes or []
self.audience = audience or []
def to_dict(self):
return {
'sub': self.sub,
'client_id': self.client_id,
'organization_id': self.organization_id,
'scopes': self.scopes,
'audience': self.audience
}
class AuthorizationError(Exception):
def __init__(self, message: str, status: int = 403):
self.message = message
self.status = status
super().__init__(self.message)
def extract_bearer_token_from_headers(headers: dict) -> str:
"""
从 HTTP headers 中提取 bearer 令牌。
注意:FastAPI 和 Django REST Framework 已内置令牌提取功能,
所以此函数主要用于 Flask 及其他框架。
"""
authorization = headers.get('authorization') or headers.get('Authorization')
if not authorization:
raise AuthorizationError('Authorization header 缺失', 401)
if not authorization.startswith('Bearer '):
raise AuthorizationError('Authorization header 必须以 "Bearer " 开头', 401)
return authorization[7:] # 移除 'Bearer ' 前缀
获取你的 Logto 租户信息
你需要以下数值来验证 Logto 签发的令牌:
- JSON Web Key Set (JWKS) URI:Logto 公钥的 URL,用于验证 JWT 签名。
- 发行者 (Issuer):期望的发行者 (Issuer) 值(Logto 的 OIDC URL)。
首先,找到你的 Logto 租户的端点。你可以在多个地方找到它:
- 在 Logto 控制台的 设置 → 域名 下。
- 在你在 Logto 配置的任何应用程序设置中,设置 → 端点与凭证。
从 OpenID Connect 发现端点获取
这些数值可以从 Logto 的 OpenID Connect 发现端点获取:
https://<your-logto-endpoint>/oidc/.well-known/openid-configuration
以下是一个示例响应(为简洁省略了其他字段):
{
"jwks_uri": "https://your-tenant.logto.app/oidc/jwks",
"issuer": "https://your-tenant.logto.app/oidc"
}
在代码中硬编码(不推荐)
由于 Logto 不允许自定义 JWKS URI 或发行者 (Issuer),你可以在代码中硬编码这些数值。但对于生产环境的应用程序,这并不推荐,因为如果将来某些配置发生变化,可能会增加维护成本。
- JWKS URI:
https://<your-logto-endpoint>/oidc/jwks
- 发行者 (Issuer):
https://<your-logto-endpoint>/oidc
校验令牌和权限
在提取令牌并获取 OIDC 配置后,请验证以下内容:
- 签名: JWT 必须有效且由 Logto(通过 JWKS)签名。
- 发行者 (Issuer): 必须与你的 Logto 租户的发行者 (Issuer) 匹配。
- 受众 (Audience): 必须与你在 Logto 中注册的 API 的资源指示器 (resource indicator) 匹配,或在适用时匹配组织 (organization) 上下文。
- 过期时间: 令牌必须未过期。
- 权限 (Scopes): 令牌必须包含你的 API / 操作所需的权限 (scopes)。权限 (scopes) 是
scope
声明中的以空格分隔的字符串。 - 组织 (Organization) 上下文: 如果保护的是组织级 API 资源,请验证
organization_id
声明。
参见 JSON Web Token 以了解更多关于 JWT 结构和声明 (Claims) 的信息。
针对每种权限 (Permission) 模型需要检查什么
不同的权限 (Permission) 模型,其声明 (Claims) 和验证规则也不同:
- 全局 API 资源
- 组织 (非 API) 权限
- 组织级 API 资源
- 受众 (Audience) 声明 (
aud
): API 资源指示器 (resource indicator) - 组织 (Organization) 声明 (
organization_id
): 不存在 - 需要检查的权限 (Scopes) (
scope
): API 资源权限 (permissions)
- 受众 (Audience) 声明 (
aud
):urn:logto:organization:<id>
(组织上下文在aud
声明中) - 组织 (Organization) 声明 (
organization_id
): 不存在 - 需要检查的权限 (Scopes) (
scope
): 组织权限 (permissions)
- 受众 (Audience) 声明 (
aud
): API 资源指示器 (resource indicator) - 组织 (Organization) 声明 (
organization_id
): 组织 ID(必须与请求匹配) - 需要检查的权限 (Scopes) (
scope
): API 资源权限 (permissions)
对于非 API 的组织 (Organization) 权限 (Permissions),组织上下文由 aud
声明表示
(例如,urn:logto:organization:abc123
)。organization_id
声明仅在组织级 API 资源令牌中存在。
对于安全的多租户 API,请始终同时验证权限 (scopes) 和上下文(受众 (audience)、组织 (organization))。
添加校验逻辑
我们使用 PyJWT 来验证 JWT。如果你还没有安装,请先安装:
pip install pyjwt[crypto]
首先,添加这些用于处理 JWT 验证的通用工具:
import jwt
from jwt import PyJWKClient
from typing import Dict, Any
from auth_middleware import AuthInfo, AuthorizationError, JWKS_URI, ISSUER
jwks_client = PyJWKClient(JWKS_URI)
def validate_jwt(token: str) -> Dict[str, Any]:
"""验证 JWT 并返回 payload"""
try:
signing_key = jwks_client.get_signing_key_from_jwt(token)
payload = jwt.decode(
token,
signing_key.key,
algorithms=['RS256'],
issuer=ISSUER,
options={'verify_aud': False} # 我们会手动验证受众 (Audience)
)
verify_payload(payload)
return payload
except jwt.InvalidTokenError as e:
raise AuthorizationError(f'Invalid token: {str(e)}', 401)
except Exception as e:
raise AuthorizationError(f'Token validation failed: {str(e)}', 401)
def create_auth_info(payload: Dict[str, Any]) -> AuthInfo:
"""从 JWT payload 创建 AuthInfo"""
scopes = payload.get('scope', '').split(' ') if payload.get('scope') else []
audience = payload.get('aud', [])
if isinstance(audience, str):
audience = [audience]
return AuthInfo(
sub=payload.get('sub'),
client_id=payload.get('client_id'),
organization_id=payload.get('organization_id'),
scopes=scopes,
audience=audience
)
def verify_payload(payload: Dict[str, Any]) -> None:
"""根据权限 (Permission) 模型验证 payload"""
# 在这里根据权限 (Permission) 模型实现你的验证逻辑
# 具体内容将在下方的权限 (Permission) 模型部分展示
pass
然后,实现中间件以验证访问令牌 (Access token):
from functools import wraps
from flask import request, jsonify, g
from jwt_validator import validate_jwt, create_auth_info
def verify_access_token(f):
@wraps(f)
def decorated_function(*args, **kwargs):
try:
token = extract_bearer_token_from_headers(dict(request.headers))
payload = validate_jwt(token)
# 将认证信息存储在 Flask 的 g 对象中以便通用使用
g.auth = create_auth_info(payload)
return f(*args, **kwargs)
except AuthorizationError as e:
return jsonify({'error': str(e)}), e.status
return decorated_function
根据你的权限 (Permission) 模型,在 jwt_validator.py
中实现相应的验证逻辑:
- 全局 API 资源
- 组织 (Organization)(非 API)权限 (Permissions)
- 组织级 API 资源
def verify_payload(payload: Dict[str, Any]) -> None:
"""验证全局 API 资源的 payload"""
# 检查受众 (Audience) 声明是否匹配你的 API 资源指示器 (Resource indicator)
audiences = payload.get('aud', [])
if isinstance(audiences, str):
audiences = [audiences]
if 'https://your-api-resource-indicator' not in audiences:
raise AuthorizationError('Invalid audience')
# 检查全局 API 资源所需的权限 (Scopes)
required_scopes = ['api:read', 'api:write'] # 替换为你实际需要的权限 (Scopes)
scopes = payload.get('scope', '').split(' ') if payload.get('scope') else []
if not all(scope in scopes for scope in required_scopes):
raise AuthorizationError('Insufficient scope')
def verify_payload(payload: Dict[str, Any]) -> None:
"""验证组织 (Organization) 权限 (Permissions) 的 payload"""
# 检查受众 (Audience) 声明是否符合组织 (Organization) 格式
audiences = payload.get('aud', [])
if isinstance(audiences, str):
audiences = [audiences]
has_org_audience = any(aud.startswith('urn:logto:organization:') for aud in audiences)
if not has_org_audience:
raise AuthorizationError('Invalid audience for organization permissions')
# 检查组织 (Organization) ID 是否与上下文匹配(你可能需要从请求上下文中提取)
expected_org_id = 'your-organization-id' # 从请求上下文中提取
expected_aud = f'urn:logto:organization:{expected_org_id}'
if expected_aud not in audiences:
raise AuthorizationError('Organization ID mismatch')
# 检查所需的组织 (Organization) 权限 (Scopes)
required_scopes = ['invite:users', 'manage:settings'] # 替换为你实际需要的权限 (Scopes)
scopes = payload.get('scope', '').split(' ') if payload.get('scope') else []
if not all(scope in scopes for scope in required_scopes):
raise AuthorizationError('Insufficient organization scope')
def verify_payload(payload: Dict[str, Any]) -> None:
"""验证组织级 API 资源的 payload"""
# 检查受众 (Audience) 声明是否匹配你的 API 资源指示器 (Resource indicator)
audiences = payload.get('aud', [])
if isinstance(audiences, str):
audiences = [audiences]
if 'https://your-api-resource-indicator' not in audiences:
raise AuthorizationError('Invalid audience for organization-level API resources')
# 检查组织 (Organization) ID 是否与上下文匹配(你可能需要从请求上下文中提取)
expected_org_id = 'your-organization-id' # 从请求上下文中提取
org_id = payload.get('organization_id')
if expected_org_id != org_id:
raise AuthorizationError('Organization ID mismatch')
# 检查组织级 API 资源所需的权限 (Scopes)
required_scopes = ['api:read', 'api:write'] # 替换为你实际需要的权限 (Scopes)
scopes = payload.get('scope', '').split(' ') if payload.get('scope') else []
if not all(scope in scopes for scope in required_scopes):
raise AuthorizationError('Insufficient organization-level API scopes')
将中间件应用到你的 API
现在,将中间件应用到你的受保护 API 路由。
from flask import Flask, g, jsonify
from auth_middleware import verify_access_token
app = Flask(__name__)
@app.route('/api/protected', methods=['GET'])
@verify_access_token
def protected_endpoint():
# 从 g.auth 获取认证 (Authentication) 信息
return jsonify({"auth": g.auth.to_dict()})
测试你的受保护 API
获取访问令牌 (Access tokens)
从你的客户端应用程序获取: 如果你已经完成了客户端集成,你的应用可以自动获取令牌。提取访问令牌 (Access token),并在 API 请求中使用它。
使用 curl / Postman 进行测试:
-
用户令牌: 使用你的客户端应用的开发者工具,从 localStorage 或网络面板复制访问令牌 (Access token)
-
机器对机器令牌: 使用客户端凭证流。以下是一个使用 curl 的非规范示例:
curl -X POST https://your-tenant.logto.app/oidc/token \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "grant_type=client_credentials" \
-d "client_id=your-m2m-client-id" \
-d "client_secret=your-m2m-client-secret" \
-d "resource=https://your-api-resource-indicator" \
-d "scope=api:read api:write"你可能需要根据你的 API 资源和权限调整
resource
和scope
参数;如果你的 API 是组织范围的,还可能需要organization_id
参数。
需要查看令牌内容?使用我们的 JWT 解码器 来解码和验证你的 JWT。
测试受保护的端点
有效令牌请求
curl -H "Authorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9..." \
http://localhost:3000/api/protected
预期响应:
{
"auth": {
"sub": "user123",
"clientId": "app456",
"organizationId": "org789",
"scopes": ["api:read", "api:write"],
"audience": ["https://your-api-resource-indicator"]
}
}
缺少令牌
curl http://localhost:3000/api/protected
预期响应 (401):
{
"error": "Authorization header is missing"
}
无效令牌
curl -H "Authorization: Bearer invalid-token" \
http://localhost:3000/api/protected
预期响应 (401):
{
"error": "Invalid token"
}
权限模型相关测试
- 全局 API 资源
- 组织 (非 API) 权限
- 组织级 API 资源
针对受全局权限保护的 API 的测试场景:
- 有效权限 (Scopes): 使用包含所需 API 权限(如
api:read
、api:write
)的令牌进行测试 - 缺少权限 (Scopes): 当令牌缺少所需权限时,预期返回 403 Forbidden
- 受众 (Audience) 错误: 当受众与 API 资源不匹配时,预期返回 403 Forbidden
# 缺少权限的令牌 - 预期 403
curl -H "Authorization: Bearer token-without-required-scopes" \
http://localhost:3000/api/protected
针对组织特定访问控制的测试场景:
- 有效组织令牌 (Organization token): 使用包含正确组织上下文(组织 ID 和权限)的令牌进行测试
- 缺少权限 (Scopes): 当用户没有请求操作的权限时,预期返回 403 Forbidden
- 组织错误: 当受众与组织上下文(
urn:logto:organization:<organization_id>
)不匹配时,预期返回 403 Forbidden
# 错误组织的令牌 - 预期 403
curl -H "Authorization: Bearer token-for-different-organization" \
http://localhost:3000/api/protected
结合 API 资源验证与组织上下文的测试场景:
- 有效组织 + API 权限: 使用同时包含组织上下文和所需 API 权限的令牌进行测试
- 缺少 API 权限: 当组织令牌缺少所需 API 权限时,预期返回 403 Forbidden
- 组织错误: 使用来自不同组织的令牌访问 API 时,预期返回 403 Forbidden
- 受众 (Audience) 错误: 当受众与组织级 API 资源不匹配时,预期返回 403 Forbidden
# 组织令牌缺少 API 权限 - 预期 403
curl -H "Authorization: Bearer organization-token-without-api-scopes" \
http://localhost:3000/api/protected
延伸阅读
RBAC 实践:为你的应用实现安全授权 (Authorization)
构建多租户 SaaS 应用:从设计到实现的完整指南