在 Symfony 中使用访问令牌认证

访问令牌或 API 令牌通常用于 API 场景中的身份认证。令牌是在认证过程中通过应用或授权服务器取得的字符串。访问令牌用于确认用户身份,并在签发前取得同意。

令牌可以有多种形式,例如不透明字符串、JSON Web Token(JWT),或采用 XML 结构的 SAML2。详细规范见 RFC 6750:OAuth 2.0 Authorization Framework: Bearer Token Usage。

使用访问令牌认证器

本指南假定应用已经配置安全系统,并创建了用户对象。尚未完成这些工作时,先阅读 Symfony 安全指南。

1)配置访问令牌认证器

使用认证器时必须配置 token_handler。处理器接收请求中的令牌,并返回正确的用户标识。为了取得标识,实现可能需要加载并验证令牌,例如检查撤销状态、过期时间、数字签名等。下面分别给出 YAML 和 PHP 两种等价配置,选择适合项目的一种:

# config/packages/security.yaml
security:
    firewalls:
        main:
            access_token:
                token_handler: App\Security\AccessTokenHandler
// config/packages/security.php
namespace Symfony\Component\DependencyInjection\Loader\Configurator;

use App\Security\AccessTokenHandler;

return App::config([
    'security' => [
        'firewalls' => [
            'main' => [
                'access_token' => [
                    'token_handler' => AccessTokenHandler::class,
                ],
            ],
        ],
    ],
]);

处理器必须实现 AccessTokenHandlerInterface:

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

use App\Repository\AccessTokenRepository;
use Symfony\Component\Security\Core\Exception\BadCredentialsException;
use Symfony\Component\Security\Http\AccessToken\AccessTokenHandlerInterface;
use Symfony\Component\Security\Http\Authenticator\Passport\Badge\UserBadge;

class AccessTokenHandler implements AccessTokenHandlerInterface
{
    public function __construct(
        private AccessTokenRepository $repository
    ) {
    }

    public function getUserBadgeFrom(string $accessToken): UserBadge
    {
        // e.g. query the "access token" database to search for this token
        $accessToken = $this->repository->findOneByValue($accessToken);
        if (null === $accessToken || !$accessToken->isValid()) {
            throw new BadCredentialsException('Invalid credentials.');
        }

        // and return a UserBadge object containing the user identifier from the found token
        // (this is the same identifier used in Security configuration; it can be an email,
        // a UUID, a username, a database ID, etc.)
        return new UserBadge($accessToken->getUserId());
    }
}

认证器用返回的用户标识,通过用户提供器加载用户。

令牌有效性检查不能省略。上例通过 isValid() 检查令牌,例如是否过期;这个方法的具体实现由应用提供。对 JWT 等自包含访问令牌,处理器必须校验数字签名并理解所有声明,尤其是 sub、iat、nbf 和 exp。

2)配置令牌提取器(可选)

应用现在已能处理传入的令牌。令牌提取器负责从请求头、请求体等位置提取令牌。

默认从请求头 Authorization 读取 Bearer 方案,例如 Authorization: Bearer the-token-value。Symfony 按 RFC 6750 提供这些提取方式:

header(默认)
通过请求头发送令牌,通常是采用 Bearer 方案的 Authorization 头。
query_string
令牌放在请求查询字符串中,参数通常为 access_token。
request_body
令牌放在 POST 请求体中,字段通常为 access_token。

URI 传递方式存在安全弱点,包含令牌的 URL 或请求体也很可能进入日志。因此,只有无法通过请求头传递令牌时,才应使用 query_string 或 request_body。

也可以创建实现 AccessTokenExtractorInterface 的自定义提取器。下面的原文示例展示改用内置提取器,或指定自定义服务的写法;同一配置只选择其中一种:

# config/packages/security.yaml
security:
    firewalls:
        main:
            access_token:
                token_handler: App\Security\AccessTokenHandler

                # use a different built-in extractor
                token_extractors: request_body

                # or provide the service ID of a custom extractor
                token_extractors: 'App\Security\CustomTokenExtractor'
// config/packages/security.php
namespace Symfony\Component\DependencyInjection\Loader\Configurator;

use App\Security\AccessTokenHandler;
use App\Security\CustomTokenExtractor;

return App::config([
    'security' => [
        'firewalls' => [
            'main' => [
                'access_token' => [
                    'token_handler' => AccessTokenHandler::class,

                    // use a different built-in extractor
                    'token_extractors' => 'request_body',

                    // or provide the service ID of a custom extractor
                    'token_extractors' => CustomTokenExtractor::class,
                ],
            ],
        ],
    ],
]);

可以同时设置多个提取器。顺序很重要:列表中第一个先被调用。

# config/packages/security.yaml
security:
    firewalls:
        main:
            access_token:
                token_handler: App\Security\AccessTokenHandler
                token_extractors:
                    - 'header'
                    - 'App\Security\CustomTokenExtractor'
// config/packages/security.php
namespace Symfony\Component\DependencyInjection\Loader\Configurator;

use App\Security\AccessTokenHandler;
use App\Security\CustomTokenExtractor;

return App::config([
    'security' => [
        'firewalls' => [
            'main' => [
                'access_token' => [
                    'token_handler' => AccessTokenHandler::class,
                    'token_extractors' => [
                        'header',
                        CustomTokenExtractor::class,
                    ],
                ],
            ],
        ],
    ],
]);

3)发送请求

至此,应用可以用 API 令牌认证传入请求。使用默认请求头提取器时,可以发送这样的请求测试功能:

$ curl -H 'Authorization: Bearer an-accepted-token-value' \
    https://localhost:8000/api/some-route

自定义认证成功处理器

默认情况下,请求继续执行,例如运行路由对应的控制器。如果需要自定义成功后的行为,创建实现 AuthenticationSuccessHandlerInterface 的处理器,并把服务 ID 配置到 success_handler:

# config/packages/security.yaml
security:
    firewalls:
        main:
            access_token:
                token_handler: App\Security\AccessTokenHandler
                success_handler: App\Security\Authentication\AuthenticationSuccessHandler
// config/packages/security.php
namespace Symfony\Component\DependencyInjection\Loader\Configurator;

use App\Security\AccessTokenHandler;
use App\Security\Authentication\AuthenticationSuccessHandler;

return App::config([
    'security' => [
    'firewalls' => [
            'main' => [
                'access_token' => [
                    'token_handler' => AccessTokenHandler::class,
                    'success_handler' => AuthenticationSuccessHandler::class,
                ],
            ],
        ],
    ],
]);

如果要自定义默认失败处理,使用 failure_handler,并创建实现 AuthenticationFailureHandlerInterface 的类。

使用 OpenID Connect(OIDC)

OpenID Connect 是 OpenID 技术的第三代,是以 JSON 为数据格式的 RESTful HTTP API。它在 OAuth 2.0 授权框架上增加身份认证层,让应用根据授权服务器完成的认证验证最终用户身份。

1)配置 OidcUserInfoTokenHandler

OidcUserInfoTokenHandler 需要 symfony/http-client 发出 HTTP 请求。尚未安装时运行:

$ composer require symfony/http-client

Symfony 提供通用的 OidcUserInfoTokenHandler,用于调用 OIDC 服务器并取得用户信息:

# config/packages/security.yaml
security:
    firewalls:
        main:
            access_token:
                token_handler:
                    oidc_user_info: https://www.example.com/realms/demo/protocol/openid-connect/userinfo
// config/packages/security.php
namespace Symfony\Component\DependencyInjection\Loader\Configurator;

return App::config([
    'security' => [
        'firewalls' => [
            'main' => [
                'access_token' => [
                    'token_handler' => [
                        'oidc_user_info' => 'https://www.example.com/realms/demo/protocol/openid-connect/userinfo',
                    ],
                ],
            ],
        ],
    ],
]);

启用 OpenID Connect Discovery 时,还需要 symfony/cache,将 OIDC 配置存入缓存。尚未安装时运行:

$ composer require symfony/cache

然后配置 base_uri 和 discovery:

# config/packages/security.yaml
security:
    firewalls:
        main:
            access_token:
                token_handler:
                    oidc_user_info:
                        base_uri: https://www.example.com/realms/demo/
                        discovery:
                            cache:
                                id: cache.app
// config/packages/security.php
namespace Symfony\Component\DependencyInjection\Loader\Configurator;

return App::config([
    'security' => [
    'firewalls' => [
            'main' => [
                'access_token' => [
                    'token_handler' => [
                        'oidc_user_info' => [
                            'base_uri' => 'https://www.example.com/realms/demo/',
                            'discovery' => [
                                'cache' => [
                                    'id' => 'cache.app',
                                ],
                            ],
                        ],
                    ],
                ],
            ],
        ],
    ],
]);

按照 OpenID Connect 规范,默认以 sub 声明作为用户标识。若需采用其他声明,用 claim 指定:

# config/packages/security.yaml
security:
    firewalls:
        main:
            access_token:
                token_handler:
                    oidc_user_info:
                        claim: email
                        base_uri: https://www.example.com/realms/demo/protocol/openid-connect/userinfo
// config/packages/security.php
namespace Symfony\Component\DependencyInjection\Loader\Configurator;

return App::config([
    'security' => [
        'firewalls' => [
            'main' => [
                'access_token' => [
                    'token_handler' => [
                        'oidc_user_info' => [
                            'claim' => 'email',
                            'base_uri' => 'https://www.example.com/realms/demo/protocol/openid-connect/userinfo',
                        ],
                    ],
                ],
            ],
        ],
    ],
]);

oidc_user_info 处理器会自动创建采用指定 base_uri 的 HTTP 客户端。若要使用自己的客户端,用 client 指定服务名:

# config/packages/security.yaml
security:
    firewalls:
        main:
            access_token:
                token_handler:
                    oidc_user_info:
                        client: oidc.client
// config/packages/security.php
namespace Symfony\Component\DependencyInjection\Loader\Configurator;

return App::config([
    'security' => [
        'firewalls' => [
            'main' => [
                'access_token' => [
                    'token_handler' => [
                        'oidc_user_info' => [
                            'client' => 'oidc.client',
                        ],
                    ],
                ],
            ],
        ],
    ],
]);

默认情况下,OidcUserInfoTokenHandler 根据声明创建 OidcUser。若需从声明创建自己的用户对象,必须提供自定义 UserProvider:

// src/Security/Core/User/OidcUserProvider.php
use Symfony\Component\Security\Core\User\AttributesBasedUserProviderInterface;

class OidcUserProvider implements AttributesBasedUserProviderInterface
{
    public function loadUserByIdentifier(string $identifier, array $attributes = []): UserInterface
    {
        // implement your own logic to load and return the user object
    }
}

这里的用户提供器是骨架:还需要补齐项目中的 UserInterface 导入,并实际实现加载和返回用户对象的逻辑。后面的 OidcTokenHandler 示例使用同样的骨架。

2)配置 OidcTokenHandler

OidcTokenHandler 需要 web-token/jwt-library。尚未安装时运行:

$ composer require web-token/jwt-library

生产环境应安装 GMP PHP 扩展。web-token/jwt-library 依赖 brick/math;没有 GMP 或 BCMath 时,它会静默退回纯 PHP 实现,这可能使 JWT 验证慢上几个数量级。

Symfony 提供通用的 OidcTokenHandler,负责解码、验证令牌,并从中取得用户信息。令牌也可以采用 JWE 加密:

# config/packages/security.yaml
security:
    firewalls:
        main:
            access_token:
                token_handler:
                    oidc:
                        # Algorithms used to sign the JWS
                        algorithms: ['ES256', 'RS256']
                        # A JSON-encoded JWK
                        keyset: '{"keys":[{"kty":"...","k":"..."}]}'
                        # Audience (`aud` claim): required for validation purpose
                        audience: 'api-example'
                        # Issuers (`iss` claim): required for validation purpose
                        issuers: ['https://oidc.example.com']
                        encryption:
                            enabled: true # Default to false
                            enforce: false # Default to false, requires an encrypted token when true
                            algorithms: ['ECDH-ES', 'A128GCM']
                            keyset: '{"keys": [...]}' # Encryption private keyset
// config/packages/security.php
namespace Symfony\Component\DependencyInjection\Loader\Configurator;

return App::config([
    'security' => [
        'firewalls' => [
            'main' => [
                'access_token' => [
                    'token_handler' => [
                        'oidc' => [
                            // Algorithms used to sign the JWS
                            'algorithms' => ['ES256', 'RS256'],
                            // A JSON-encoded JWK
                            'keyset' => '{"keys":[{"kty":"...","k":"..."}]}',
                            // Audience (`aud` claim): required for validation purpose
                            'audience' => 'api-example',
                            // Issuers (`iss` claim): required for validation purpose
                            'issuers' => ['https://oidc.example.com'],
                            // Encryption:
                            'encryption' => [
                                'enabled' => true, // Default to false
                                'enforce' => false, // Default to false, requires an encrypted token when true
                                'algorithms' => ['ECDH-ES', 'A128GCM'],
                                'keyset' => '{"keys": [...]}' // Encryption private keyset
                            ],
                        ],
                    ],
                ],
            ],
        ],
    ],
]);

启用 OpenID Connect Discovery 时,OidcTokenHandler 也需要 symfony/cache 缓存 OIDC 配置。尚未安装时运行:

$ composer require symfony/cache

随后可以删除 keyset 配置项,由 OpenID Connect Discovery 导入密钥集,并配置 discovery:

# config/packages/security.yaml
security:
    firewalls:
        main:
            access_token:
                token_handler:
                    oidc:
                        claim: email
                        algorithms: ['ES256', 'RS256']
                        audience: 'api-example'
                        issuers: ['https://oidc.example.com']
                        discovery:
                            base_uri: https://www.example.com/realms/demo/
                            cache:
                                id: cache.app
// config/packages/security.php
namespace Symfony\Component\DependencyInjection\Loader\Configurator;

return App::config([
    'security' => [
        'firewalls' => [
            'main' => [
                'access_token' => [
                    'token_handler' => [
                        'oidc' => [
                            'claim' => 'email',
                            'algorithms' => ['ES256', 'RS256'],
                            'audience' => 'api-example',
                            'issuers' => ['https://oidc.example.com'],
                            'discovery' => [
                                'base_uri' => 'https://www.example.com/realms/demo/',
                                'cache' => [
                                    'id' => 'cache.app',
                                ],
                            ],
                        ],
                    ],
                ],
            ],
        ],
    ],
]);

默认使用 Discovery 时,只接受明确指定用于签名验证的密钥:按 RFC 7517,use 为 sig,或 key_ops 包含 sign 或 verify。若身份提供方返回的密钥没有用途标记,即没有 use 或 key_ops 字段,可以将 enforce_key_usage_verification 设为 false,关闭严格用途筛选:

security:
    firewalls:
        main:
            access_token:
                token_handler:
                    oidc:
                        # ...
                        discovery:
                            base_uri: https://www.example.com/realms/demo/
                            cache:
                                id: cache.app
                            enforce_key_usage_verification: false
return App::config([
    'security' => [
        'firewalls' => [
            'main' => [
                'access_token' => [
                    'token_handler' => [
                        'oidc' => [
                            // ...
                            'discovery' => [
                                'base_uri' => 'https://www.example.com/realms/demo/',
                                'cache' => [
                                    'id' => 'cache.app',
                                ],
                                'enforce_key_usage_verification' => false,
                            ],
                        ],
                    ],
                ],
            ],
        ],
    ],
]);

关闭之后仍会筛选密钥:明确只用于加密的密钥(use: enc,或 key_ops 只有加密操作)仍被排除;没有用途标记的密钥则会被纳入。enforce_key_usage_verification 在 Symfony 8.1 引入。

默认仍按 OpenID Connect 规范使用 sub 作为用户标识。需要其他声明时,在配置中指定:

# config/packages/security.yaml
security:
    firewalls:
        main:
            access_token:
                token_handler:
                    oidc:
                        claim: email
                        algorithms: ['ES256', 'RS256']
                        keyset: '{"keys":[{"kty":"...","k":"..."}]}'
                        audience: 'api-example'
                        issuers: ['https://oidc.example.com']
// config/packages/security.php
namespace Symfony\Component\DependencyInjection\Loader\Configurator;

return App::config([
    'security' => [
        'firewalls' => [
            'main' => [
                'access_token' => [
                    'token_handler' => [
                        'oidc' => [
                            'claim' => 'email',
                            'algorithms' => ['ES256', 'RS256'],
                            'keyset' => '{"keys":[{"kty":"...","k":"..."}]}',
                            'audience' => 'api-example',
                            'issuers' => ['https://oidc.example.com'],
                        ],
                    ],
                ],
            ],
        ],
    ],
]);

默认情况下,OidcTokenHandler 根据声明创建 OidcUser。要创建自己的用户对象,同样需要自定义 UserProvider:

// src/Security/Core/User/OidcUserProvider.php
use Symfony\Component\Security\Core\User\AttributesBasedUserProviderInterface;

class OidcUserProvider implements AttributesBasedUserProviderInterface
{
    public function loadUserByIdentifier(string $identifier, array $attributes = []): UserInterface
    {
        // implement your own logic to load and return the user object
    }
}

配置多个 OIDC Discovery 端点

OidcTokenHandler 支持多个 Discovery 端点,可以验证来自不同身份提供方的令牌:

# config/packages/security.yaml
security:
    firewalls:
        main:
            access_token:
                token_handler:
                    oidc:
                        algorithms: ['ES256', 'RS256']
                        audience: 'api-example'
                        # each "issuer" announced by the discovery documents
                        issuers:
                            - https://idp1.example.com/realms/demo
                            - https://idp2.example.com/realms/demo
                        discovery:
                            base_uri:
                                - https://idp1.example.com/realms/demo/
                                - https://idp2.example.com/realms/demo/
                            cache:
                                id: cache.app
// config/packages/security.php
namespace Symfony\Component\DependencyInjection\Loader\Configurator;

return App::config([
    'security' => [
        'firewalls' => [
            'main' => [
                'access_token' => [
                    'token_handler' => [
                        'oidc' => [
                            'algorithms' => ['ES256', 'RS256'],
                            'audience' => 'api-example',
                            // each "issuer" announced by the discovery documents
                            'issuers' => [
                                'https://idp1.example.com/realms/demo',
                                'https://idp2.example.com/realms/demo',
                            ],
                            'discovery' => [
                                'base_uri' => [
                                    'https://idp1.example.com/realms/demo/',
                                    'https://idp2.example.com/realms/demo/',
                                ],
                                'cache' => [
                                    'id' => 'cache.app',
                                ],
                            ],
                        ],
                    ],
                ],
            ],
        ],
    ],
]);

处理器取得各 Discovery 端点的 JWK 集,并将每组密钥绑定到该 Discovery 文档宣布的签发者。因此,每个令牌只能用自己的 iss 对应签发者的密钥验证。issuers 必须逐字列出所有宣布的签发者,包括末尾斜杠;两个 Discovery 文档不能宣布同一签发者。

在命令行创建 OIDC 令牌

security:oidc:generate-token 帮助生成 JWT,主要用于开发或测试采用 OIDC 认证的应用:

# generate a token using the default configuration
$ php bin/console security:oidc:generate-token john.doe@example.com

# specify the firewall, algorithm, and issuer if multiple are available
$ php bin/console security:oidc:generate-token john.doe@example.com \
    --firewall="api" \
    --algorithm="HS256" \
    --issuer="https://example.com"

用于签名的 JWK 必须设置正确的密钥操作标志。

使用 CAS 2.0

Central Authentication Service(CAS)是面向 Web 的企业多语言单点登录方案和身份提供方,旨在提供完整的认证与授权平台。

配置 Cas2Handler

Symfony 提供通用的 Cas2Handler,用来调用 CAS 服务器。它需要 symfony/http-client 发出 HTTP 请求;尚未安装时运行:

$ composer require symfony/http-client

可以如下配置 CAS 令牌处理器:

# config/packages/security.yaml
security:
    firewalls:
        main:
            access_token:
                token_handler:
                    cas:
                        validation_url: https://www.example.com/cas/validate
// config/packages/security.php
namespace Symfony\Component\DependencyInjection\Loader\Configurator;

return App::config([
    'security' => [
        'firewalls' => [
            'main' => [
                'access_token' => [
                    'token_handler' => [
                        'cas' => [
                            'validation_url' => 'https://www.example.com/cas/validate',
                        ],
                    ],
                ],
            ],
        ],
    ],
]);

处理器自动创建 HTTP 客户端来访问指定的 validation_url。若要用自己的客户端,通过 http_client 指定服务名:

# config/packages/security.yaml
security:
    firewalls:
        main:
            access_token:
                token_handler:
                    cas:
                        validation_url: https://www.example.com/cas/validate
                        http_client: cas.client
// config/packages/security.php
namespace Symfony\Component\DependencyInjection\Loader\Configurator;

return App::config([
    'security' => [
    'firewalls' => [
            'main' => [
                'access_token' => [
                    'token_handler' => [
                        'cas' => [
                            'validation_url' => 'https://www.example.com/cas/validate',
                            'http_client' => 'cas.client',
                        ],
                    ],
                ],
            ],
        ],
    ],
]);

默认使用 cas 前缀读取验证 URL 返回的 XML 响应,也可以配置其他前缀:

# config/packages/security.yaml
security:
    firewalls:
        main:
            access_token:
                token_handler:
                    cas:
                        validation_url: https://www.example.com/cas/validate
                        prefix: cas-example
// config/packages/security.php
namespace Symfony\Component\DependencyInjection\Loader\Configurator;

return App::config([
    'security' => [
        'firewalls' => [
            'main' => [
                'access_token' => [
                    'token_handler' => [
                        'cas' => [
                            'validation_url' => 'https://www.example.com/cas/validate',
                            'prefix' => 'cas-example',
                        ],
                    ],
                ],
            ],
        ],
    ],
]);

从令牌创建用户

某些令牌(例如 OIDC)包含创建用户实体所需的全部信息,例如用户名和角色。这时不必使用从数据库加载用户的提供器:

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

// ...
class AccessTokenHandler implements AccessTokenHandlerInterface
{
    // ...

    public function getUserBadgeFrom(string $accessToken): UserBadge
    {
        // get the data from the token
        $payload = ...;

        return new UserBadge(
            $payload->getUserId(),
            fn (string $userIdentifier) => new User($userIdentifier, $payload->getRoles())
        );
    }
}

采用这种策略时,无状态防火墙可以省略 user_provider 配置。


来源:Symfony 文档:How to use Access Token Authentication,Symfony 文档贡献者。2026-10-03 核对 Symfony 8.1 当前文档。本中文版本完整翻译正文,并按顺序展开原文的 YAML/PHP 配置选项;代码原样保留,补充了二选一提示。全部内容(包括示例代码)遵循 Creative Commons Attribution-ShareAlike 3.0 Unported;改编版本也按同一许可提供。许可依据见 官方许可说明。示例包括应用自行实现的仓库、用户加载器和占位密钥,尚未在本机运行 Symfony 应用或 OIDC/CAS 服务。

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享
评论 抢沙发

请登录后发表评论

    暂无评论内容