如何编写自定义身份验证器
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'));
}
}