You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

基于Azure保护Symfony API并调用Graph的技术方案咨询

Symfony 4 集成 Azure AD 令牌验证与 On Behalf Of 流程实现

一、推荐使用的 Symfony Bundle

  • knpuniversity/oauth2-client-bundle:基于 league/oauth2-client 封装的 Symfony 专用Bundle,提供了开箱即用的Azure AD Provider,配置简单,能快速实现令牌验证、资源用户信息获取等功能。
  • symfony/oauth2-client-bundle:Symfony官方维护的OAuth2客户端集成Bundle,同样兼容league/oauth2-client的Azure Provider,适合需要更底层定制的场景。

两个Bundle都能避免重复实现令牌解析、签名验证等基础逻辑,直接对接Azure AD的认证体系。

二、Authenticator Guard 的使用建议

在Symfony 4中,应该使用Authenticator Guard来实现API的令牌验证逻辑。它是Symfony 4安全系统的推荐方案,能将Azure令牌验证逻辑封装为独立的认证器,无缝集成到Symfony的防火墙体系中,同时支持无状态API场景(符合SPA与API通信的需求)。

三、On Behalf Of 流程实现步骤

  1. 验证SPA传入的令牌:通过Guard认证器校验令牌的签名、受众、过期时间等有效性。
  2. 获取Graph API的委托令牌:使用已验证的用户令牌,向Azure AD发起urn:ietf:params:oauth:grant-type:jwt-bearer类型的请求,获取针对Graph API的访问令牌。
  3. 调用Graph API:用新获取的令牌请求Graph API接口(如/me)获取用户信息。

四、核心代码示例

1. Azure令牌验证Guard认证器

// src/Security/AzureTokenAuthenticator.php
namespace App\Security;

use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Security\Core\User\UserProviderInterface;
use Symfony\Component\Security\Guard\AbstractGuardAuthenticator;
use League\OAuth2\Client\Provider\Azure;

class AzureTokenAuthenticator extends AbstractGuardAuthenticator
{
    private $azureProvider;

    public function __construct(Azure $azureProvider)
    {
        $this->azureProvider = $azureProvider;
    }

    // 检查请求是否携带Bearer令牌
    public function supports(Request $request)
    {
        return $request->headers->has('Authorization') 
            && str_starts_with($request->headers->get('Authorization'), 'Bearer ');
    }

    // 提取令牌内容
    public function getCredentials(Request $request)
    {
        return trim(str_replace('Bearer ', '', $request->headers->get('Authorization')));
    }

    // 验证令牌并获取用户信息
    public function getUser($credentials, UserProviderInterface $userProvider)
    {
        try {
            $resourceOwner = $this->azureProvider->getResourceOwner($credentials);
            // 可根据Azure用户ID加载本地用户,或返回临时用户对象
            return $userProvider->loadUserByIdentifier($resourceOwner->getId());
        } catch (\Exception $e) {
            throw new AuthenticationException('无效的Azure访问令牌');
        }
    }

    public function checkCredentials($credentials, UserInterface $user)
    {
        // 令牌已在getUser中验证,直接返回true
        return true;
    }

    public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $providerKey)
    {
        // 认证成功,继续处理请求
        return null;
    }

    public function onAuthenticationFailure(Request $request, AuthenticationException $exception)
    {
        return new Response('认证失败:'.$exception->getMessage(), Response::HTTP_UNAUTHORIZED);
    }

    public function start(Request $request, AuthenticationException $authException = null)
    {
        return new Response('需要身份验证', Response::HTTP_UNAUTHORIZED);
    }

    public function supportsRememberMe()
    {
        return false;
    }
}

2. On Behalf Of 令牌获取服务

// src/Service/AzureObosService.php
namespace App\Service;

use League\OAuth2\Client\Provider\Azure;

class AzureObosService
{
    private $azureProvider;
    private $clientId;
    private $clientSecret;
    private $tenantId;

    public function __construct(Azure $azureProvider, string $clientId, string $clientSecret, string $tenantId)
    {
        $this->azureProvider = $azureProvider;
        $this->clientId = $clientId;
        $this->clientSecret = $clientSecret;
        $this->tenantId = $tenantId;
    }

    // 获取Graph API的委托访问令牌
    public function getGraphAccessToken(string $userToken): string
    {
        $params = [
            'grant_type' => 'urn:ietf:params:oauth:grant-type:jwt-bearer',
            'client_id' => $this->clientId,
            'client_secret' => $this->clientSecret,
            'assertion' => $userToken,
            'scope' => 'https://graph.microsoft.com/User.Read',
            'requested_token_use' => 'on_behalf_of',
        ];

        $tokenResponse = $this->azureProvider->getResponse(
            $this->azureProvider->getBaseAccessTokenUrl([]), 
            $params
        );

        return $tokenResponse['access_token'];
    }
}

3. 关键配置示例(services.yaml)

services:
    # Azure AD Provider配置
    League\OAuth2\Client\Provider\Azure:
        arguments:
            $options:
                clientId: '%env(AZURE_CLIENT_ID)%'
                clientSecret: '%env(AZURE_CLIENT_SECRET)%'
                tenantId: '%env(AZURE_TENANT_ID)%'
                urlAuthorize: 'https://login.microsoftonline.com/%env(AZURE_TENANT_ID)%/oauth2/v2.0/authorize'
                urlAccessToken: 'https://login.microsoftonline.com/%env(AZURE_TENANT_ID)%/oauth2/v2.0/token'
                urlResourceOwnerDetails: 'https://graph.microsoft.com/v1.0/me'

    # Guard认证器注册
    App\Security\AzureTokenAuthenticator:
        arguments:
            $azureProvider: '@League\OAuth2\Client\Provider\Azure'
        tags:
            - { name: security.guard.authenticator }

    # OBO服务注册
    App\Service\AzureObosService:
        arguments:
            $azureProvider: '@League\OAuth2\Client\Provider\Azure'
            $clientId: '%env(AZURE_CLIENT_ID)%'
            $clientSecret: '%env(AZURE_CLIENT_SECRET)%'
            $tenantId: '%env(AZURE_TENANT_ID)%'

4. 安全防火墙配置(security.yaml)

security:
    providers:
        azure_user_provider:
            id: App\Security\AzureUserProvider # 自定义用户提供者,需实现UserProviderInterface

    firewalls:
        api:
            pattern: ^/api
            guard:
                authenticators:
                    - App\Security\AzureTokenAuthenticator
            stateless: true # API无状态,不存储会话

内容的提问来源于stack exchange,提问作者Pierre de LESPINAY

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.19 23:00:53