You need to enable JavaScript to run this app.
最新活动
大模型
产品
解决方案
定价
生态与合作
支持与服务
开发者
了解我们

Python开发的Azure Web App通过Azure AD认证获取当前用户邮箱的问题求助

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-NAME header里拿到用户的邮箱(或者用户主体名):
    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

火山引擎 最新活动