跳到内容

如何编写自定义身份验证器

编辑此页

Symfony 自带许多身份验证器,第三方bundles也实现了更复杂的情况,例如 JWT 和 oAuth 2.0。但是,有时您需要实现尚不存在的自定义身份验证机制,或者您需要自定义一个。 在这种情况下,您必须创建和使用自己的身份验证器。

身份验证器应该实现 AuthenticatorInterface。您也可以扩展 AbstractAuthenticator,它为 createToken() 方法提供了一个默认实现,该实现适用于大多数用例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
// src/Security/ApiKeyAuthenticator.php
namespace App\Security;

use Symfony\Component\HttpFoundation\JsonResponse;
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\Exception\CustomUserMessageAuthenticationException;
use Symfony\Component\Security\Http\Authenticator\AbstractAuthenticator;
use Symfony\Component\Security\Http\Authenticator\Passport\Badge\UserBadge;
use Symfony\Component\Security\Http\Authenticator\Passport\Passport;
use Symfony\Component\Security\Http\Authenticator\Passport\SelfValidatingPassport;

class ApiKeyAuthenticator extends AbstractAuthenticator
{
    /**
     * Called on every request to decide if this authenticator should be
     * used for the request. Returning `false` will cause this authenticator
     * to be skipped.
     */
    public function supports(Request $request): ?bool
    {
        // "auth-token" is an example of a custom, non-standard HTTP header used in this application
        return $request->headers->has('auth-token');
    }

    public function authenticate(Request $request): Passport
    {
        $apiToken = $request->headers->get('auth-token');
        if (null === $apiToken) {
            // The token header was empty, authentication fails with HTTP Status
            // Code 401 "Unauthorized"
            throw new CustomUserMessageAuthenticationException('No API token provided');
        }

        // implement your own logic to get the user identifier from `$apiToken`
        // e.g. by looking up a user in the database using its API key
        $userIdentifier = /** ... */;

        return new SelfValidatingPassport(new UserBadge($userIdentifier));
    }

    public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $firewallName): ?Response
    {
        // on success, let the request continue
        return null;
    }

    public function onAuthenticationFailure(Request $request, AuthenticationException $exception): ?Response
    {
        $data = [
            // you may want to customize or obfuscate the message first
            'message' => strtr($exception->getMessageKey(), $exception->getMessageData())

            // or to translate this message
            // $this->translator->trans($exception->getMessageKey(), $exception->getMessageData())
        ];

        return new JsonResponse($data, Response::HTTP_UNAUTHORIZED);
    }
}

提示

如果您的自定义身份验证器是登录表单,您可以从 AbstractLoginFormAuthenticator 类扩展,以使您的工作更轻松。

可以使用 custom_authenticators 设置启用身份验证器

1
2
3
4
5
6
7
8
# config/packages/security.yaml
security:

    # ...
    firewalls:
        main:
            custom_authenticators:
                - App\Security\ApiKeyAuthenticator

提示

您可能希望您的身份验证器实现 AuthenticationEntryPointInterface。 这定义了发送给用户以启动身份验证的响应(例如,当他们访问受保护页面时)。 在入口点:帮助用户启动身份验证中阅读更多相关信息。

authenticate() 方法是身份验证器最重要的一个方法。 它的工作是从 Request 对象中提取凭据(例如,用户名和密码或 API 令牌),并将这些凭据转换为安全 Passport(安全 passports 将在本文后面解释)。

身份验证过程结束后,用户要么已通过身份验证,要么出现问题(例如,密码不正确)。 身份验证器可以定义在这些情况下会发生什么

onAuthenticationSuccess(Request $request, TokenInterface $token, string $firewallName): ?Response

如果身份验证成功,则使用已通过身份验证的 $token 调用此方法。

此方法可以返回响应(例如,将用户重定向到某个页面)。

如果返回 null,则当前请求将继续(并且用户将通过身份验证)。 这对于 API 路由很有用,其中每个路由都受 API 密钥标头保护。

onAuthenticationFailure(Request $request, AuthenticationException $exception): ?Response

如果身份验证失败(例如,错误的用户名密码),则使用抛出的 AuthenticationException 调用此方法。

此方法可以返回响应(例如,在 API 路由中发送 401 Unauthorized)。

如果返回 null,则请求继续(但用户将不会通过身份验证)。 这对于登录表单很有用,其中登录控制器再次与登录错误一起运行。

如果您使用的是登录限制,您可以检查 $exception 是否是 TooManyLoginAttemptsAuthenticationException 的实例(例如,显示适当的消息)。

注意:永远不要对 AuthenticationException 实例使用 $exception->getMessage()。 此消息可能包含您不想公开的敏感信息。 相反,使用 $exception->getMessageKey()$exception->getMessageData(),如上面的完整示例所示。 如果您想设置自定义错误消息,请使用 CustomUserMessageAuthenticationException

提示

如果您的登录方法是交互式的,这意味着用户主动登录到您的应用程序,您可能希望您的身份验证器实现 InteractiveAuthenticatorInterface,以便它分派 InteractiveLoginEvent

安全 Passport

passport 是一个对象,其中包含将要通过身份验证的用户以及其他信息,例如是否应检查密码或是否应启用“记住我”功能。

默认的 Passport 需要用户和某种“凭据”(例如密码)。

使用 UserBadge 将用户附加到 passport。 UserBadge 需要用户标识符(例如用户名或电子邮件),该标识符用于使用用户提供器加载用户

1
2
3
4
use Symfony\Component\Security\Http\Authenticator\Passport\Badge\UserBadge;

// ...
$passport = new Passport(new UserBadge($email), $credentials);

注意

用户标识符允许的最大长度为 4096 个字符,以防止 会话存储泛洪攻击。

注意

您可以选择将用户加载器作为第二个参数传递给 UserBadge。 此可调用对象接收 $userIdentifier,并且必须返回 UserInterface 对象(否则会抛出 UserNotFoundException

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// src/Security/CustomAuthenticator.php
namespace App\Security;

use App\Repository\UserRepository;
// ...

class CustomAuthenticator extends AbstractAuthenticator
{
    public function __construct(
        private UserRepository $userRepository,
    ) {
    }

    public function authenticate(Request $request): Passport
    {
        // ...

        return new Passport(
            new UserBadge($email, function (string $userIdentifier): ?UserInterface {
                return $this->userRepository->findOneBy(['email' => $userIdentifier]);
            }),
            $credentials
        );
    }
}

默认情况下支持以下凭据类

PasswordCredentials

这需要一个明文 $password,它使用为用户配置的密码编码器进行验证

1
2
3
4
use Symfony\Component\Security\Http\Authenticator\Passport\Credentials\PasswordCredentials;

// ...
return new Passport(new UserBadge($email), new PasswordCredentials($plaintextPassword));
CustomCredentials

允许自定义闭包来检查凭据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Security\Http\Authenticator\Passport\Credentials\CustomCredentials;

// ...
return new Passport(new UserBadge($email), new CustomCredentials(
    // If this function returns anything else than `true`, the credentials
    // are marked as invalid.
    // The $credentials parameter is equal to the next argument of this class
    function (string $credentials, UserInterface $user): bool {
        return $user->getApiToken() === $credentials;
    },

    // The custom credentials
    $apiToken
));

自验证 Passport

如果您不需要检查任何凭据(例如,使用 API 令牌时),可以使用 SelfValidatingPassport。 此类仅需要 UserBadge 对象,并且可以选择 Passport 徽章

Passport 徽章

Passport 还允许您选择添加安全徽章。 徽章将更多数据附加到 passport(以扩展安全性)。 默认情况下,支持以下徽章

RememberMeBadge
当此徽章添加到 passport 时,身份验证器指示支持记住我。 是否实际使用记住我取决于特殊的 remember_me 配置。 阅读 如何添加“记住我”登录功能 以获取更多信息。
PasswordUpgradeBadge
用于在成功登录后自动将密码升级到新的哈希(如果需要)。 此徽章需要明文密码和密码升级器(例如,用户存储库)。 请参阅密码哈希和验证
CsrfTokenBadge
在身份验证期间自动验证此身份验证器的 CSRF 令牌。 构造函数需要令牌 ID(每个表单唯一)和 CSRF 令牌(每个请求唯一)。 请参阅 如何实现 CSRF 保护
PreAuthenticatedUserBadge
指示此用户已预先身份验证(即在 Symfony 初始化之前)。 这会跳过预身份验证用户检查器

注意

如果 passport 具有 PasswordCredentials,则 PasswordUpgradeBadge 会自动添加到 passport。

例如,如果您想将 CSRF 添加到您的自定义身份验证器,您可以像这样初始化 passport

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// src/Service/LoginAuthenticator.php
namespace App\Service;

// ...
use Symfony\Component\Security\Http\Authenticator\AbstractAuthenticator;
use Symfony\Component\Security\Http\Authenticator\Passport\Badge\CsrfTokenBadge;
use Symfony\Component\Security\Http\Authenticator\Passport\Badge\UserBadge;
use Symfony\Component\Security\Http\Authenticator\Passport\Passport;

class LoginAuthenticator extends AbstractAuthenticator
{
    public function authenticate(Request $request): Passport
    {
        $password = $request->getPayload()->get('password');
        $username = $request->getPayload()->get('username');
        $csrfToken = $request->getPayload()->get('csrf_token');

        // ...

        return new Passport(
            new UserBadge($username),
            new PasswordCredentials($password),
            [new CsrfTokenBadge('login', $csrfToken)]
        );
    }
}

Passport 属性

除了徽章之外,passports 还可以定义属性,这允许 authenticate() 方法在 passport 中存储任意信息,以便从其他身份验证器方法(例如 createToken())访问它

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// ...
use Symfony\Component\Security\Http\Authenticator\Passport\Badge\UserBadge;

class LoginAuthenticator extends AbstractAuthenticator
{
    // ...

    public function authenticate(Request $request): Passport
    {
        // ... process the request

        $passport = new SelfValidatingPassport(new UserBadge($username), []);

        // set a custom attribute (e.g. scope)
        $passport->setAttribute('scope', $oauthScope);

        return $passport;
    }

    public function createToken(Passport $passport, string $firewallName): TokenInterface
    {
        // read the attribute value
        return new CustomOauthToken($passport->getUser(), $passport->getAttribute('scope'));
    }
}
本作品,包括代码示例,根据 Creative Commons BY-SA 3.0 许可获得许可。
目录
    版本