OAuth2.1实现用户代签API的Application Password方案及Symfony适配问题
替代Resource Owner Password Grant的适配方案(Symfony + league/oauth-server-bundle)
针对你需要脚本以用户身份调用API、避免暴露账号密码,同时解决Client Credentials Grant不关联用户的问题,提供两种可行方案:
方案一:扩展Client Credentials Grant 关联用户
通过自定义Client实体、用户提供者和认证逻辑,让Client Credentials流返回关联的用户实体,同时保留客户端信息。
1. 自定义OAuth Client实体(关联用户)
// src/Entity/OAuthClient.php namespace App\Entity; use League\OAuth2\Server\Entities\ClientEntityInterface; use Doctrine\ORM\Mapping as ORM; #[ORM\Entity] class OAuthClient implements ClientEntityInterface { #[ORM\Id] #[ORM\GeneratedValue] #[ORM\Column(type: 'integer')] private int $id; #[ORM\Column(type: 'string', unique: true)] private string $identifier; #[ORM\Column(type: 'string')] private string $secret; #[ORM\ManyToOne(targetEntity: User::class)] #[ORM\JoinColumn(nullable: false)] private User $user; // 实现ClientEntityInterface要求的方法 public function getIdentifier(): string { return $this->identifier; } public function getName(): string { return $this->user->getUsername() . '专用客户端'; } public function getRedirectUri(): string { return ''; } // Getter/Setter public function getUser(): User { return $this->user; } public function setUser(User $user): void { $this->user = $user; } public function setIdentifier(string $identifier): void { $this->identifier = $identifier; } public function setSecret(string $secret): void { $this->secret = $secret; } }
2. 自定义用户提供者(从Client获取关联用户)
// src/Security/ClientAssociatedUserProvider.php namespace App\Security; use App\Entity\User; use App\Repository\OAuthClientRepository; use Symfony\Component\Security\Core\Exception\UserNotFoundException; use Symfony\Component\Security\Core\User\UserInterface; use Symfony\Component\Security\Core\User\UserProviderInterface; class ClientAssociatedUserProvider implements UserProviderInterface { public function __construct(private OAuthClientRepository $clientRepository) { } public function loadUserByIdentifier(string $identifier): UserInterface { $client = $this->clientRepository->findOneBy(['identifier' => $identifier]); if (!$client) { throw new UserNotFoundException('客户端不存在'); } return $client->getUser(); } public function refreshUser(UserInterface $user): UserInterface { return $user; } public function supportsClass(string $class): bool { return User::class === $class; } }
3. 配置Security使用自定义用户提供者
# config/packages/security.yaml security: providers: client_associated_user_provider: id: App\Security\ClientAssociatedUserProvider firewalls: api: pattern: ^/api stateless: true oauth2: token_url: /oauth/token user_provider: client_associated_user_provider grant_types: - client_credentials access_control: - { path: ^/api, roles: ROLE_USER }
4. 获取认证后的OAuth客户端
通过事件监听器将客户端实例存入认证Token的属性中:
// src/EventListener/OAuthClientListener.php namespace App\EventListener; use App\Repository\OAuthClientRepository; use Symfony\Component\Security\Http\Event\InteractiveLoginEvent; class OAuthClientListener { public function __construct(private OAuthClientRepository $clientRepository) { } public function onInteractiveLogin(InteractiveLoginEvent $event): void { $request = $event->getRequest(); $clientId = $request->request->get('client_id'); if (!$clientId) { return; } $client = $this->clientRepository->findOneBy(['identifier' => $clientId]); if ($client) { $event->getAuthenticationToken()->setAttribute('oauth_client', $client); } } }
注册监听器:
# config/services.yaml services: App\EventListener\OAuthClientListener: tags: - { name: kernel.event_listener, event: security.interactive_login }
在控制器中获取客户端:
$client = $this->get('security.token_storage')->getToken()->getAttribute('oauth_client');
方案二:使用Personal Access Tokens(PAT)
类似GitHub个人访问令牌,为用户生成专属的长期令牌,无需暴露密码或客户端凭证,更接近谷歌应用密码的使用方式。
1. 自定义AccessToken实体(关联用户)
// src/Entity/AccessToken.php namespace App\Entity; use League\OAuth2\Server\Entities\AccessTokenEntityInterface; use League\OAuth2\Server\Entities\Traits\AccessTokenTrait; use League\OAuth2\Server\Entities\Traits\EntityTrait; use League\OAuth2\Server\Entities\Traits\TokenEntityTrait; use Doctrine\ORM\Mapping as ORM; #[ORM\Entity] class AccessToken implements AccessTokenEntityInterface { use AccessTokenTrait, EntityTrait, TokenEntityTrait; #[ORM\ManyToOne(targetEntity: User::class)] #[ORM\JoinColumn(nullable: false)] private User $user; public function getUser(): User { return $this->user; } public function setUser(User $user): void { $this->user = $user; } }
2. 配置OAuth2 Server使用自定义AccessToken
# config/packages/oauth2_server.yaml oauth2_server: private_key: '%env(OAUTH2_PRIVATE_KEY_PATH)%' public_key: '%env(OAUTH2_PUBLIC_KEY_PATH)%' access_token: class: App\Entity\AccessToken # 其他必要配置(如client、scope仓库)
3. 生成PAT的命令行工具
// src/Command/GeneratePatCommand.php namespace App\Command; use App\Entity\User; use App\Entity\AccessToken; use League\OAuth2\Server\CryptKey; use League\OAuth2\Server\Repositories\ClientRepositoryInterface; use League\OAuth2\Server\Services\AccessTokenServiceInterface; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; use Doctrine\ORM\EntityManagerInterface; class GeneratePatCommand extends Command { protected static $defaultName = 'app:generate-pat'; public function __construct( private EntityManagerInterface $em, private ClientRepositoryInterface $clientRepository, private AccessTokenServiceInterface $accessTokenService, private CryptKey $privateKey ) { parent::__construct(); } protected function configure(): void { $this ->addArgument('user-id', InputArgument::REQUIRED, '用户ID') ->addArgument('client-id', InputArgument::REQUIRED, 'OAuth客户端ID') ->addArgument('expiry', InputArgument::OPTIONAL, '令牌有效期(如+1year)', '+1year'); } protected function execute(InputInterface $input, OutputInterface $output): int { $user = $this->em->getRepository(User::class)->find($input->getArgument('user-id')); if (!$user) { $output->writeln('<error>用户不存在</error>'); return Command::FAILURE; } $client = $this->clientRepository->getClientEntity($input->getArgument('client-id')); if (!$client) { $output->writeln('<error>客户端不存在</error>'); return Command::FAILURE; } $accessToken = new AccessToken(); $accessToken->setUser($user); $accessToken->setClient($client); $accessToken->setExpiryDateTime(new \DateTimeImmutable($input->getArgument('expiry'))); $accessToken->addScope('api:read'); // 添加所需权限范围 $this->em->persist($accessToken); $this->em->flush(); $tokenString = $this->accessTokenService->issueAccessToken($accessToken, $this->privateKey); $output->writeln("生成的PAT:<info>{$tokenString}</info>"); return Command::SUCCESS; } }
4. 脚本调用方式
直接在请求头中携带令牌:
curl -H "Authorization: Bearer {生成的PAT}" https://your-api-domain/api/endpoint
此时认证后的用户为关联的App\Entity\User,可正常使用access_control的角色校验,也可通过$this->getUser()获取用户实例。
内容的提问来源于stack exchange,提问作者anotherdev
相关产品推荐
相关产品推荐

