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

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.25 00:09:53