基于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 流程实现步骤
- 验证SPA传入的令牌:通过Guard认证器校验令牌的签名、受众、过期时间等有效性。
- 获取Graph API的委托令牌:使用已验证的用户令牌,向Azure AD发起
urn:ietf:params:oauth:grant-type:jwt-bearer类型的请求,获取针对Graph API的访问令牌。 - 调用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
相关产品推荐
相关产品推荐

