Python开发的Azure Web App通过Azure AD认证获取当前用户邮箱的问题求助
问题描述
我现在在用Python开发一个Azure Web App,用Azure AD来做站点的身份认证。我想显示当前登录用户的邮箱(之后还打算用这个来做权限分层)。我了解到可以通过委托式Graph API来获取用户详情,于是尝试了下面的代码:
authority = f'https://login.microsoftonline.com/{subid}' scope = ["https://graph.microsoft.com/.default"] app = ConfidentialClientApplication( client_id, authority=authority, client_credential=client_secret ) token_response = app.acquire_token_for_client(scopes=scope) access_token = token_response['access_token'] headers = { 'Authorization': f'Bearer {access_token}' } response = requests.get('https://graph.microsoft.com/v1.0/me', headers=headers) user_data = response.json() print("~~~~~~~~~~~~~~~~~~~~~~") for key, value in user_data.items(): print(f'{key}: {value}') print("~~~~~~~~~~~~~~~~~~~~~~")
但运行后返回了错误:
error: {'code': 'BadRequest', 'message': '/me request is only valid with delegated authentication flow.', 'innerError': {'date': '2023-10-20T13:05:47', 'request-id': '9b6d8130-5bf6-4711-ba90-14ccaef0b127', 'client-request-id': '9b6d8130-5bf6-4711-ba90-14ccaef0b127'}}
我本来以为既然用户已经通过Azure AD登录认证了,用委托式应该能行,是不是我哪里理解错了?求帮忙分析或者给个替代方案,另外我的Graph API权限已经配置好了。
解决方案&问题分析
首先得搞清楚你遇到这个错误的核心原因:你用的acquire_token_for_client是客户端凭证流,这个流是让应用以自身的身份去请求Graph API,完全没有当前登录用户的上下文,所以调用/me端点肯定会报错——/me必须绑定到一个具体的用户,只有用委托式认证流(也就是基于当前登录用户的身份去获取令牌)才能正常使用。
你之所以有疑惑,可能是混淆了应用身份和用户身份:虽然用户已经登录了你的Web App,但你代码里拿的是应用自己的令牌,和这个登录用户没关系,自然没法获取到这个用户的信息。
接下来给你几个可行的解决方向:
1. 直接从Web App的认证上下文获取用户邮箱(无需调用Graph API)
其实如果你的Azure Web App已经配置了Azure AD认证,用户登录后,Web App会自动获取到用户的基本信息,包括邮箱,完全不需要调用Graph API!比如在常见的Python Web框架里:
- 如果用Flask,可以从请求的
X-MS-CLIENT-PRINCIPAL-NAMEheader里拿到用户的邮箱(或者用户主体名):from flask import request user_email = request.headers.get('X-MS-CLIENT-PRINCIPAL-NAME') - 如果用Django,也可以从认证后的用户对象里获取相关信息,或者从请求的headers中提取。
这个方法最简便,不需要额外调用Graph API,适合只需要邮箱这类基础信息的场景。
2. 用委托式流调用Graph API获取用户详情(适合需要更多用户信息的场景)
如果确实需要调用Graph API获取更多用户数据,那就要用当前登录用户的访问令牌,而不是应用的令牌:
- 第一步,确保你的Azure AD应用注册已经添加了委托类型的Graph API权限(比如
User.Read),并且已经授予了管理员同意(如果是租户级应用的话)。 - 第二步,在Azure Web App的认证设置里,开启颁发访问令牌(Access Token),这样用户登录后,Web App可以拿到用户的访问令牌用来调用Graph API。
- 第三步,在代码中从当前请求的上下文里获取用户的访问令牌,然后调用Graph API:
import requests from flask import request # 从请求的Authorization header中提取用户的访问令牌 auth_header = request.headers.get('Authorization') if auth_header and auth_header.startswith('Bearer '): user_access_token = auth_header.split(' ')[1] # 调用Graph API的/me端点,指定只获取需要的字段 headers = {'Authorization': f'Bearer {user_access_token}'} response = requests.get('https://graph.microsoft.com/v1.0/me?$select=mail,userPrincipalName', headers=headers) if response.status_code == 200: user_data = response.json() # 优先取mail字段,没有的话取userPrincipalName user_email = user_data.get('mail') or user_data.get('userPrincipalName') print(user_email) - 另外,如果你想更规范地处理令牌的刷新,可以用MSAL的
acquire_token_silent方法,基于用户的登录会话来获取委托令牌:from msal import ConfidentialClientApplication # 初始化MSAL应用 app = ConfidentialClientApplication( client_id, authority=f'https://login.microsoftonline.com/{tenant_id}', client_credential=client_secret ) # 从当前登录用户的会话中获取用户主体名 user_principal_name = request.headers.get('X-MS-CLIENT-PRINCIPAL-NAME') accounts = app.get_accounts(username=user_principal_name) if accounts: # 尝试静默获取令牌(自动处理刷新) result = app.acquire_token_silent(scopes=["User.Read"], account=accounts[0]) if not result: # 静默失败的话触发交互式登录(Web App中一般不需要,因为用户已登录) result = app.acquire_token_interactive(scopes=["User.Read"]) if "access_token" in result: headers = {'Authorization': f'Bearer {result["access_token"]}'} response = requests.get('https://graph.microsoft.com/v1.0/me', headers=headers) user_data = response.json()
总结一下,核心就是要区分应用身份和用户身份,你之前的代码用的是应用身份,而要获取当前登录用户的信息,必须用用户身份对应的委托式令牌,或者直接从Web App的认证上下文里提取基础信息,后者更简单。
备注:内容来源于stack exchange,提问作者Metel Stairs




