FastAPI OAuth2中如何为scopes字典传入标量Scope值?
我来帮你理清这两个核心问题——毕竟我之前在FastAPI对接Azure AD认证时也踩过类似的坑:
一、OAuthFlow的scopes字典该怎么处理你的Azure .default Scope?
首先得明确:OAuthFlow里的scopes字段是Dict[str, str]类型,键是给用户/开发者看的「友好名称」,值是实际的Scope字符串。这是FastAPI用来生成OpenAPI文档时展示可选权限用的,比如在Swagger UI的授权弹窗里,用户能看到这些友好名称,选择对应的权限。
针对你的api://a12b34cd-5e67-89f0-a12b-c3de456f78ab/.default这个Scope,你可以这么配置:
- 如果需要友好名称(方便在文档里说明):
scopes={ "API默认访问权限": "api://a12b34cd-5e67-89f0-a12b-c3de456f78ab/.default" } - 如果不需要额外说明,也可以直接用Scope字符串本身作为键(虽然不太直观,但完全可行):
scopes={ "api://a12b34cd-5e67-89f0-a12b-c3de456f78ab/.default": "默认访问权限" } - 空白项?其实FastAPI不允许键或值为空字符串(会影响OpenAPI文档生成),所以至少要填一个有意义的描述,哪怕是简短的“Default”。
二、SecurityScopes类和OAuthFlow的scopes字典的区别与交互逻辑
这俩其实是分工明确的两个东西:
1. 核心区别
- OAuthFlow的scopes字典:是「静态定义」,用来声明你的API支持哪些Scope,作用是生成OpenAPI文档,告诉客户端“我支持这些权限,你请求令牌时可以指定它们”。它不参与实际的权限校验逻辑,只是文档和客户端引导用的。
- SecurityScopes类:是「动态处理」,用来解析请求中令牌携带的Scope。Azure返回的令牌里,Scope是用空格分隔的字符串(比如如果客户端请求了多个Scope,会是
scope1 scope2),SecurityScopes会自动把这个字符串拆成列表,同时帮你生成WWW-Authenticate响应头里的scope参数,方便客户端知道需要哪些权限。
2. 交互逻辑
举个贴合你场景的例子,你自定义SecurityBase子类时的典型流程:
- 先在OAuthFlow的scopes字典里定义好你支持的Azure Scope(比如你的.default Scope),让Swagger UI等工具能展示这些权限选项。
- 在路由里使用
Security(your_custom_auth, scopes=["xxx/.default"])来指定该路由需要的权限。 - 在自定义认证依赖的
__call__方法中,注入SecurityScopes对象:它会自动收集路由指定的scopes,同时你可以从令牌里解析出实际携带的Scope字符串,用security_scopes.scope_str或者自己拆分列表,然后做权限校验。
比如你的自定义依赖里的核心逻辑:
from fastapi import Security, HTTPException, status from fastapi.security.base import SecurityBase from fastapi.security.oauth2 import SecurityScopes, OAuth2, OAuthFlowAuthorizationCode class AzureOAuth2(SecurityBase): def __init__(self, tenant_id: str, client_id: str): self.scheme_name = "OAuth2AuthorizationCodeBearer" self.model = OAuth2( flows={ "authorizationCode": OAuthFlowAuthorizationCode( authorizationUrl=f"https://login.microsoftonline.com/{tenant_id}/oauth2/v2.0/authorize", tokenUrl=f"https://login.microsoftonline.com/{tenant_id}/oauth2/v2.0/token", scopes={ "API默认访问权限": "api://a12b34cd-5e67-89f0-a12b-c3de456f78ab/.default" } ) } ) async def __call__(self, security_scopes: SecurityScopes): # 1. 从请求头获取Bearer令牌 auth_header = await self._get_auth_header() token = auth_header.split("Bearer ")[1] # 2. 验证Azure令牌(这里可以用pyjwt或者azure-identity的工具) decoded_token = self._validate_azure_token(token) # 3. 解析令牌里的Scope字符串(Azure令牌里的Scope在"scp"字段) token_scopes = decoded_token.get("scp", "").split() # 4. 校验所需Scope是否存在 for required_scope in security_scopes.scopes: if required_scope not in token_scopes: raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail="权限不足", headers={"WWW-Authenticate": f'Bearer scope="{security_scopes.scope_str}"'}, ) return decoded_token
在路由里使用时:
@app.get("/protected-resource") async def get_protected_data( token_data = Security(AzureOAuth2(tenant_id="your-tenant", client_id="your-client"), scopes=["api://a12b34cd-5e67-89f0-a12b-c3de456f78ab/.default"]) ): return {"data": "敏感内容", "user": token_data["name"]}
这里的SecurityScopes会自动把路由指定的scopes转换成空格分隔的字符串(security_scopes.scope_str),用来在401响应里告诉客户端需要哪些权限;同时你可以用security_scopes.scopes获取拆分后的列表,和令牌里的Scope做对比。
最后总结一下
- 把你的Azure .default Scope放到OAuthFlow的scopes字典的值里,键填一个友好名称即可;
- OAuthFlow的scopes是给文档和客户端看的静态声明,SecurityScopes是处理请求令牌的动态工具;
- 自定义认证时,用SecurityScopes来管理路由所需权限,同时解析令牌里的实际Scope做校验。
内容的提问来源于stack exchange,提问作者NeilG
相关产品推荐
相关产品推荐

