Symfony authenticator:在控制器中,尽管设置了一个值,但条令返回带有空字符串字段的用户项
我的数据库模式主要由以下实体组成:用户和身份验证令牌。每个用户可以有多个身份验证令牌 问题是:在控制器中选择当前经过身份验证的用户时,字符串字段saltedPasswordHash为空(“”),尽管数据库中设置了一个值。在ApiKeyAuthenticator.php中获取saltedPasswordHash是可行的(请查看两条TODO注释) 无论出于何种原因,选择email(字符串)或created(日期时间)字段都有效。使用saltedPasswordHash持久化新用户实体或选择任何其他用户项都可以 APIKeyAuthenticator正在处理授权。禁用防火墙和身份验证时,一切正常。我在下面包括了源文件 我正在使用PHP7.2.15-1和mysql版本15.1发行版10.3.13-MariaDB Security/ApiKeyAuthenticator.phpSymfony authenticator:在控制器中,尽管设置了一个值,但条令返回带有空字符串字段的用户项,symfony,doctrine-orm,doctrine,symfony-3.4,doctrine-query,Symfony,Doctrine Orm,Doctrine,Symfony 3.4,Doctrine Query,我的数据库模式主要由以下实体组成:用户和身份验证令牌。每个用户可以有多个身份验证令牌 问题是:在控制器中选择当前经过身份验证的用户时,字符串字段saltedPasswordHash为空(“”),尽管数据库中设置了一个值。在ApiKeyAuthenticator.php中获取saltedPasswordHash是可行的(请查看两条TODO注释) 无论出于何种原因,选择email(字符串)或created(日期时间)字段都有效。使用saltedPasswordHash持久化新用户实体或选择任何其他用
namespace App\Security;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Security\Core\Authentication\Token\PreAuthenticatedToken;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
use Symfony\Component\Security\Core\Exception\BadCredentialsException;
use Symfony\Component\Security\Core\User\UserProviderInterface;
use Symfony\Component\Security\Http\Authentication\SimplePreAuthenticatorInterface;
class ApiKeyAuthenticator implements SimplePreAuthenticatorInterface
{
public function createToken(Request $request, $providerKey)
{
$apiKey = $request->headers->get('authToken');
if (!$apiKey) {
throw new BadCredentialsException();
// or to just skip api key authentication
// return null;
}
return new PreAuthenticatedToken(
'anon.',
$apiKey,
$providerKey
);
}
public function supportsToken(TokenInterface $token, $providerKey)
{
return $token instanceof PreAuthenticatedToken && $token->getProviderKey() === $providerKey;
}
public function authenticateToken(TokenInterface $token, UserProviderInterface $userProvider, $providerKey)
{
if (!$userProvider instanceof ApiKeyUserProvider) {
throw new \InvalidArgumentException(
sprintf(
'The user provider must be an instance of ApiKeyUserProvider (%s was given).',
get_class($userProvider)
)
);
}
$apiKey = $token->getCredentials();
$username = $userProvider->getUsernameForApiKey($apiKey);
if (!$username) {
// CAUTION: this message will be returned to the client
// (so don't put any un-trusted messages / error strings here)
throw new BadCredentialsException(
sprintf('API Key "%s" does not exist.', $apiKey)
);
}
$user = $userProvider->loadUserByAuthToken($apiKey);
if (!isset($user)) {
throw new BadCredentialsException(
sprintf('API Key "%s" does not exist.', $apiKey)
);
}
// TODO: HERE, THE $user->getSaltedPasswordHash() RETURNS THE CORRECT VALUE!
return new PreAuthenticatedToken(
$user, // TODO: with "new User()" instead, it works!
$apiKey,
$providerKey,
$user->getRoles()
);
}
public function onAuthenticationFailure(Request $request, AuthenticationException $exception)
{
return new Response(
// this contains information about *why* authentication failed
// use it, or return your own message
strtr($exception->getMessageKey(), $exception->getMessageData()),
401
);
}
}
namespace App\Security;
use App\Entity\AuthToken;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\Security\Core\User\UserProviderInterface;
use Symfony\Component\Security\Core\User\User;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Security\Core\Exception\UnsupportedUserException;
class ApiKeyUserProvider implements UserProviderInterface
{
/**
* @var EntityManagerInterface
*/
private $em;
/**
* ApiKeyUserProvider constructor.
* @param EntityManagerInterface $em
*/
public function __construct(EntityManagerInterface $em)
{
$this->em = $em;
}
public function getUsernameForApiKey($apiKey)
{
return $apiKey;
}
public function loadUserByUsername($username)
{
// TODO: Implement loadUserByUsername() method.
}
/**
* Auth token is used as username
*
* @param string $authToken
* @return null|UserInterface
*/
public function loadUserByAuthToken($authToken): ?UserInterface
{
if (!isset($authToken)) {
return null;
}
$token = $this->em
->getRepository(AuthToken::class)
->findOneBy(['id' => AuthToken::hex2dec($authToken)]);
if (!isset($token)) {
return null;
}
return $token->getUser();
}
public
function refreshUser(UserInterface $user)
{
// this is used for storing authentication in the session
// but in this example, the token is sent in each request,
// so authentication can be stateless. Throwing this exception
// is proper to make things stateless
throw new UnsupportedUserException();
}
public
function supportsClass($class)
{
return User::class === $class;
}
}
namespace App\Entity;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Security\Core\User\EquatableInterface;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Validator\Constraints as Assert;
/**
* @ORM\Entity(repositoryClass="App\Repository\UserRepository")
*/
class User implements UserInterface, EquatableInterface
{
/**
* @ORM\Id()
* @ORM\GeneratedValue()
* @ORM\Column(type="integer")
*/
private $id;
/**
* This needs to be nullable because the email includes the id of newly created users, which can only be obtained after inserting the new record.
* @ORM\Column(type="string", length=255, nullable=true, unique=true)
* @Assert\Length(max=255)
* @Assert\NotBlank()
*/
private $email;
/**
* Set null to disable login
* @ORM\Column(type="string", length=255, nullable=true)
* @Assert\Length(max=255)
* @Assert\NotBlank()
*/
private $saltedPasswordHash;
/**
* @ORM\Column(type="datetime")
*/
private $created;
// ...
/**
* @ORM\OneToMany(targetEntity="App\Entity\AuthToken", mappedBy="user", fetch="LAZY")
*/
private $authTokens;
/**
* @ORM\Column(type="string", length=5)
*/
private $role;
// ...
public function __construct()
{
$this->role = 'user';
$this->saltedPasswordHash = null;
}
public function getId()
{
return $this->id;
}
public function getSaltedPasswordHash(): ?string
{
return $this->saltedPasswordHash;
}
public function setSaltedPasswordHash(?string $saltedPasswordHash): self
{
$this->saltedPasswordHash = $saltedPasswordHash;
return $this;
}
public function getEmail(): ?string
{
return $this->email;
}
public function setEmail(?string $email): self
{
$this->email = $email;
return $this;
}
public function getCreated(): ?\DateTimeInterface
{
return $this->created;
}
public function setCreated(\DateTimeInterface $created): self
{
$this->created = $created;
return $this;
}
/**
* @return ArrayCollection
*/
public function getAuthTokens()
{
return $this->authTokens;
}
/**
* @param ArrayCollection $authTokens
* @return User
*/
public function setAuthTokens(ArrayCollection $authTokens): User
{
$this->authTokens = $authTokens;
return $this;
}
/**
* @param AuthToken $authToken
* @return User
*/
public function addAuthToken(AuthToken $authToken): User
{
$this->authTokens->add($authToken);
return $this;
}
/**
* @param AuthToken $authToken
* @return User
*/
public function removeAuthToken(AuthToken $authToken): User
{
$this->authTokens->removeElement($authToken);
return $this;
}
// ...
/**
* Returns the password used to authenticate the user.
*
* This should be the encoded password. On authentication, a plain-text
* password will be salted, encoded, and then compared to this value.
*
* @return string The password
*/
public function getPassword()
{
return $this->getSaltedPasswordHash();
}
/**
* Returns the salt that was originally used to encode the password.
*
* This can return null if the password was not encoded using a salt.
*
* @return string|null The salt
*/
public function getSalt()
{
// TODO: Implement getSalt() method.
}
/**
* Returns the username used to authenticate the user.
*
* @return string The username
*/
public function getUsername()
{
return $this->getEmail();
}
/**
* Removes sensitive data from the user.
*
* This is important if, at any given point, sensitive information like
* the plain-text password is stored on this object.
*/
public function eraseCredentials()
{
$this->setSaltedPasswordHash('');
}
/**
* @return mixed
*/
public function getRole()
{
return $this->role;
}
/**
* @param mixed $role
* @return User
*/
public function setRole($role): User
{
$this->role = $role;
return $this;
}
/**
* @return string
*/
public function __toString()
{
return "User " . $this->email;
}
/**
* The equality comparison should neither be done by referential equality
* nor by comparing identities (i.e. getId() === getId()).
*
* However, you do not need to compare every attribute, but only those that
* are relevant for assessing whether re-authentication is required.
*
* Also implementation should consider that $user instance may implement
* the extended user interface `AdvancedUserInterface`.
*
* https://stackoverflow.com/a/39884792/6144818
*
* @param UserInterface $user
* @return bool
*/
public function isEqualTo(UserInterface $user)
{
return (
$this->getUsername() == $user->getUsername()
) && (
$this->getRoles() == $user->getRoles()
);
}
// ...
}
namespace App\Entity;
use Doctrine\ORM\Mapping as ORM;
/**
* @ORM\Entity(repositoryClass="App\Repository\AuthTokenRepository")
*/
class AuthToken
{
/**
* @ORM\Id()
* @ORM\Column(type="decimal", precision=32, scale=0, options={"unsigned": true})
*/
private $id;
/**
* @ORM\ManyToOne(targetEntity="App\Entity\User", inversedBy="authTokens")
*/
private $user;
/**
* @ORM\Column(type="datetime")
*/
private $added;
/**
* @ORM\Column(type="datetime", nullable=true)
*/
private $lastSeen;
/**
* @ORM\Column(type="string", length=12, nullable=true)
*/
private $apiVersion;
/**
* @return string
*/
public function getId(): string
{
return $this->id;
}
/**
* @return string
*/
public function getHexId(): string
{
return $this->dec2hex($this->id);
}
/**
* @param mixed $id
*/
public function setId($id): void
{
$this->id = $id;
}
/**
* @param mixed $id
* @throws \Exception
*/
public function generateId(): void
{
$length = 32;
$str = "";
$characters = range('0', '9');
$max = count($characters) - 1;
for ($i = 0; $i < $length; $i++) {
$rand = random_int(0, $max);
$str .= $characters[$rand];
}
$this->id = $str;
}
/**
* @return mixed
*/
public function getUser()
{
return $this->user;
}
/**
* @param mixed $user
*/
public function setUser($user): void
{
$this->user = $user;
}
/**
* @return mixed
*/
public function getAdded()
{
return $this->added;
}
/**
* @param mixed $added
*/
public function setAdded($added): void
{
$this->added = $added;
}
/**
* @return mixed
*/
public function getLastSeen()
{
return $this->lastSeen;
}
/**
* @param mixed $lastSeen
*/
public function setLastSeen($lastSeen): void
{
$this->lastSeen = $lastSeen;
}
public function getApiVersion(): ?string
{
return $this->apiVersion;
}
public function setApiVersion(string $apiVersion): self
{
$this->apiVersion = $apiVersion;
return $this;
}
public static function dec2hex(string $dec): string
{
$hex = '';
do {
$last = bcmod($dec, 16);
$hex = dechex($last) . $hex;
$dec = bcdiv(bcsub($dec, $last), 16);
} while ($dec > 0);
return $hex;
}
public static function hex2dec($hex)
{
$dec = '0';
$len = strlen($hex);
for ($i = 1; $i <= $len; $i++)
$dec = bcadd($dec, bcmul(strval(hexdec($hex[$i - 1])), bcpow('16', strval($len - $i))));
return $dec;
}
/**
* @return string
*/
public function __toString()
{
return "AuthToken " . $this->id . " (" . $this->user . ")";
}
}
Security/ApiKeyUserProvider.php
namespace App\Security;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Security\Core\Authentication\Token\PreAuthenticatedToken;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
use Symfony\Component\Security\Core\Exception\BadCredentialsException;
use Symfony\Component\Security\Core\User\UserProviderInterface;
use Symfony\Component\Security\Http\Authentication\SimplePreAuthenticatorInterface;
class ApiKeyAuthenticator implements SimplePreAuthenticatorInterface
{
public function createToken(Request $request, $providerKey)
{
$apiKey = $request->headers->get('authToken');
if (!$apiKey) {
throw new BadCredentialsException();
// or to just skip api key authentication
// return null;
}
return new PreAuthenticatedToken(
'anon.',
$apiKey,
$providerKey
);
}
public function supportsToken(TokenInterface $token, $providerKey)
{
return $token instanceof PreAuthenticatedToken && $token->getProviderKey() === $providerKey;
}
public function authenticateToken(TokenInterface $token, UserProviderInterface $userProvider, $providerKey)
{
if (!$userProvider instanceof ApiKeyUserProvider) {
throw new \InvalidArgumentException(
sprintf(
'The user provider must be an instance of ApiKeyUserProvider (%s was given).',
get_class($userProvider)
)
);
}
$apiKey = $token->getCredentials();
$username = $userProvider->getUsernameForApiKey($apiKey);
if (!$username) {
// CAUTION: this message will be returned to the client
// (so don't put any un-trusted messages / error strings here)
throw new BadCredentialsException(
sprintf('API Key "%s" does not exist.', $apiKey)
);
}
$user = $userProvider->loadUserByAuthToken($apiKey);
if (!isset($user)) {
throw new BadCredentialsException(
sprintf('API Key "%s" does not exist.', $apiKey)
);
}
// TODO: HERE, THE $user->getSaltedPasswordHash() RETURNS THE CORRECT VALUE!
return new PreAuthenticatedToken(
$user, // TODO: with "new User()" instead, it works!
$apiKey,
$providerKey,
$user->getRoles()
);
}
public function onAuthenticationFailure(Request $request, AuthenticationException $exception)
{
return new Response(
// this contains information about *why* authentication failed
// use it, or return your own message
strtr($exception->getMessageKey(), $exception->getMessageData()),
401
);
}
}
namespace App\Security;
use App\Entity\AuthToken;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\Security\Core\User\UserProviderInterface;
use Symfony\Component\Security\Core\User\User;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Security\Core\Exception\UnsupportedUserException;
class ApiKeyUserProvider implements UserProviderInterface
{
/**
* @var EntityManagerInterface
*/
private $em;
/**
* ApiKeyUserProvider constructor.
* @param EntityManagerInterface $em
*/
public function __construct(EntityManagerInterface $em)
{
$this->em = $em;
}
public function getUsernameForApiKey($apiKey)
{
return $apiKey;
}
public function loadUserByUsername($username)
{
// TODO: Implement loadUserByUsername() method.
}
/**
* Auth token is used as username
*
* @param string $authToken
* @return null|UserInterface
*/
public function loadUserByAuthToken($authToken): ?UserInterface
{
if (!isset($authToken)) {
return null;
}
$token = $this->em
->getRepository(AuthToken::class)
->findOneBy(['id' => AuthToken::hex2dec($authToken)]);
if (!isset($token)) {
return null;
}
return $token->getUser();
}
public
function refreshUser(UserInterface $user)
{
// this is used for storing authentication in the session
// but in this example, the token is sent in each request,
// so authentication can be stateless. Throwing this exception
// is proper to make things stateless
throw new UnsupportedUserException();
}
public
function supportsClass($class)
{
return User::class === $class;
}
}
namespace App\Entity;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Security\Core\User\EquatableInterface;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Validator\Constraints as Assert;
/**
* @ORM\Entity(repositoryClass="App\Repository\UserRepository")
*/
class User implements UserInterface, EquatableInterface
{
/**
* @ORM\Id()
* @ORM\GeneratedValue()
* @ORM\Column(type="integer")
*/
private $id;
/**
* This needs to be nullable because the email includes the id of newly created users, which can only be obtained after inserting the new record.
* @ORM\Column(type="string", length=255, nullable=true, unique=true)
* @Assert\Length(max=255)
* @Assert\NotBlank()
*/
private $email;
/**
* Set null to disable login
* @ORM\Column(type="string", length=255, nullable=true)
* @Assert\Length(max=255)
* @Assert\NotBlank()
*/
private $saltedPasswordHash;
/**
* @ORM\Column(type="datetime")
*/
private $created;
// ...
/**
* @ORM\OneToMany(targetEntity="App\Entity\AuthToken", mappedBy="user", fetch="LAZY")
*/
private $authTokens;
/**
* @ORM\Column(type="string", length=5)
*/
private $role;
// ...
public function __construct()
{
$this->role = 'user';
$this->saltedPasswordHash = null;
}
public function getId()
{
return $this->id;
}
public function getSaltedPasswordHash(): ?string
{
return $this->saltedPasswordHash;
}
public function setSaltedPasswordHash(?string $saltedPasswordHash): self
{
$this->saltedPasswordHash = $saltedPasswordHash;
return $this;
}
public function getEmail(): ?string
{
return $this->email;
}
public function setEmail(?string $email): self
{
$this->email = $email;
return $this;
}
public function getCreated(): ?\DateTimeInterface
{
return $this->created;
}
public function setCreated(\DateTimeInterface $created): self
{
$this->created = $created;
return $this;
}
/**
* @return ArrayCollection
*/
public function getAuthTokens()
{
return $this->authTokens;
}
/**
* @param ArrayCollection $authTokens
* @return User
*/
public function setAuthTokens(ArrayCollection $authTokens): User
{
$this->authTokens = $authTokens;
return $this;
}
/**
* @param AuthToken $authToken
* @return User
*/
public function addAuthToken(AuthToken $authToken): User
{
$this->authTokens->add($authToken);
return $this;
}
/**
* @param AuthToken $authToken
* @return User
*/
public function removeAuthToken(AuthToken $authToken): User
{
$this->authTokens->removeElement($authToken);
return $this;
}
// ...
/**
* Returns the password used to authenticate the user.
*
* This should be the encoded password. On authentication, a plain-text
* password will be salted, encoded, and then compared to this value.
*
* @return string The password
*/
public function getPassword()
{
return $this->getSaltedPasswordHash();
}
/**
* Returns the salt that was originally used to encode the password.
*
* This can return null if the password was not encoded using a salt.
*
* @return string|null The salt
*/
public function getSalt()
{
// TODO: Implement getSalt() method.
}
/**
* Returns the username used to authenticate the user.
*
* @return string The username
*/
public function getUsername()
{
return $this->getEmail();
}
/**
* Removes sensitive data from the user.
*
* This is important if, at any given point, sensitive information like
* the plain-text password is stored on this object.
*/
public function eraseCredentials()
{
$this->setSaltedPasswordHash('');
}
/**
* @return mixed
*/
public function getRole()
{
return $this->role;
}
/**
* @param mixed $role
* @return User
*/
public function setRole($role): User
{
$this->role = $role;
return $this;
}
/**
* @return string
*/
public function __toString()
{
return "User " . $this->email;
}
/**
* The equality comparison should neither be done by referential equality
* nor by comparing identities (i.e. getId() === getId()).
*
* However, you do not need to compare every attribute, but only those that
* are relevant for assessing whether re-authentication is required.
*
* Also implementation should consider that $user instance may implement
* the extended user interface `AdvancedUserInterface`.
*
* https://stackoverflow.com/a/39884792/6144818
*
* @param UserInterface $user
* @return bool
*/
public function isEqualTo(UserInterface $user)
{
return (
$this->getUsername() == $user->getUsername()
) && (
$this->getRoles() == $user->getRoles()
);
}
// ...
}
namespace App\Entity;
use Doctrine\ORM\Mapping as ORM;
/**
* @ORM\Entity(repositoryClass="App\Repository\AuthTokenRepository")
*/
class AuthToken
{
/**
* @ORM\Id()
* @ORM\Column(type="decimal", precision=32, scale=0, options={"unsigned": true})
*/
private $id;
/**
* @ORM\ManyToOne(targetEntity="App\Entity\User", inversedBy="authTokens")
*/
private $user;
/**
* @ORM\Column(type="datetime")
*/
private $added;
/**
* @ORM\Column(type="datetime", nullable=true)
*/
private $lastSeen;
/**
* @ORM\Column(type="string", length=12, nullable=true)
*/
private $apiVersion;
/**
* @return string
*/
public function getId(): string
{
return $this->id;
}
/**
* @return string
*/
public function getHexId(): string
{
return $this->dec2hex($this->id);
}
/**
* @param mixed $id
*/
public function setId($id): void
{
$this->id = $id;
}
/**
* @param mixed $id
* @throws \Exception
*/
public function generateId(): void
{
$length = 32;
$str = "";
$characters = range('0', '9');
$max = count($characters) - 1;
for ($i = 0; $i < $length; $i++) {
$rand = random_int(0, $max);
$str .= $characters[$rand];
}
$this->id = $str;
}
/**
* @return mixed
*/
public function getUser()
{
return $this->user;
}
/**
* @param mixed $user
*/
public function setUser($user): void
{
$this->user = $user;
}
/**
* @return mixed
*/
public function getAdded()
{
return $this->added;
}
/**
* @param mixed $added
*/
public function setAdded($added): void
{
$this->added = $added;
}
/**
* @return mixed
*/
public function getLastSeen()
{
return $this->lastSeen;
}
/**
* @param mixed $lastSeen
*/
public function setLastSeen($lastSeen): void
{
$this->lastSeen = $lastSeen;
}
public function getApiVersion(): ?string
{
return $this->apiVersion;
}
public function setApiVersion(string $apiVersion): self
{
$this->apiVersion = $apiVersion;
return $this;
}
public static function dec2hex(string $dec): string
{
$hex = '';
do {
$last = bcmod($dec, 16);
$hex = dechex($last) . $hex;
$dec = bcdiv(bcsub($dec, $last), 16);
} while ($dec > 0);
return $hex;
}
public static function hex2dec($hex)
{
$dec = '0';
$len = strlen($hex);
for ($i = 1; $i <= $len; $i++)
$dec = bcadd($dec, bcmul(strval(hexdec($hex[$i - 1])), bcpow('16', strval($len - $i))));
return $dec;
}
/**
* @return string
*/
public function __toString()
{
return "AuthToken " . $this->id . " (" . $this->user . ")";
}
}
Entity/User.php
namespace App\Security;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Security\Core\Authentication\Token\PreAuthenticatedToken;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
use Symfony\Component\Security\Core\Exception\BadCredentialsException;
use Symfony\Component\Security\Core\User\UserProviderInterface;
use Symfony\Component\Security\Http\Authentication\SimplePreAuthenticatorInterface;
class ApiKeyAuthenticator implements SimplePreAuthenticatorInterface
{
public function createToken(Request $request, $providerKey)
{
$apiKey = $request->headers->get('authToken');
if (!$apiKey) {
throw new BadCredentialsException();
// or to just skip api key authentication
// return null;
}
return new PreAuthenticatedToken(
'anon.',
$apiKey,
$providerKey
);
}
public function supportsToken(TokenInterface $token, $providerKey)
{
return $token instanceof PreAuthenticatedToken && $token->getProviderKey() === $providerKey;
}
public function authenticateToken(TokenInterface $token, UserProviderInterface $userProvider, $providerKey)
{
if (!$userProvider instanceof ApiKeyUserProvider) {
throw new \InvalidArgumentException(
sprintf(
'The user provider must be an instance of ApiKeyUserProvider (%s was given).',
get_class($userProvider)
)
);
}
$apiKey = $token->getCredentials();
$username = $userProvider->getUsernameForApiKey($apiKey);
if (!$username) {
// CAUTION: this message will be returned to the client
// (so don't put any un-trusted messages / error strings here)
throw new BadCredentialsException(
sprintf('API Key "%s" does not exist.', $apiKey)
);
}
$user = $userProvider->loadUserByAuthToken($apiKey);
if (!isset($user)) {
throw new BadCredentialsException(
sprintf('API Key "%s" does not exist.', $apiKey)
);
}
// TODO: HERE, THE $user->getSaltedPasswordHash() RETURNS THE CORRECT VALUE!
return new PreAuthenticatedToken(
$user, // TODO: with "new User()" instead, it works!
$apiKey,
$providerKey,
$user->getRoles()
);
}
public function onAuthenticationFailure(Request $request, AuthenticationException $exception)
{
return new Response(
// this contains information about *why* authentication failed
// use it, or return your own message
strtr($exception->getMessageKey(), $exception->getMessageData()),
401
);
}
}
namespace App\Security;
use App\Entity\AuthToken;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\Security\Core\User\UserProviderInterface;
use Symfony\Component\Security\Core\User\User;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Security\Core\Exception\UnsupportedUserException;
class ApiKeyUserProvider implements UserProviderInterface
{
/**
* @var EntityManagerInterface
*/
private $em;
/**
* ApiKeyUserProvider constructor.
* @param EntityManagerInterface $em
*/
public function __construct(EntityManagerInterface $em)
{
$this->em = $em;
}
public function getUsernameForApiKey($apiKey)
{
return $apiKey;
}
public function loadUserByUsername($username)
{
// TODO: Implement loadUserByUsername() method.
}
/**
* Auth token is used as username
*
* @param string $authToken
* @return null|UserInterface
*/
public function loadUserByAuthToken($authToken): ?UserInterface
{
if (!isset($authToken)) {
return null;
}
$token = $this->em
->getRepository(AuthToken::class)
->findOneBy(['id' => AuthToken::hex2dec($authToken)]);
if (!isset($token)) {
return null;
}
return $token->getUser();
}
public
function refreshUser(UserInterface $user)
{
// this is used for storing authentication in the session
// but in this example, the token is sent in each request,
// so authentication can be stateless. Throwing this exception
// is proper to make things stateless
throw new UnsupportedUserException();
}
public
function supportsClass($class)
{
return User::class === $class;
}
}
namespace App\Entity;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Security\Core\User\EquatableInterface;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Validator\Constraints as Assert;
/**
* @ORM\Entity(repositoryClass="App\Repository\UserRepository")
*/
class User implements UserInterface, EquatableInterface
{
/**
* @ORM\Id()
* @ORM\GeneratedValue()
* @ORM\Column(type="integer")
*/
private $id;
/**
* This needs to be nullable because the email includes the id of newly created users, which can only be obtained after inserting the new record.
* @ORM\Column(type="string", length=255, nullable=true, unique=true)
* @Assert\Length(max=255)
* @Assert\NotBlank()
*/
private $email;
/**
* Set null to disable login
* @ORM\Column(type="string", length=255, nullable=true)
* @Assert\Length(max=255)
* @Assert\NotBlank()
*/
private $saltedPasswordHash;
/**
* @ORM\Column(type="datetime")
*/
private $created;
// ...
/**
* @ORM\OneToMany(targetEntity="App\Entity\AuthToken", mappedBy="user", fetch="LAZY")
*/
private $authTokens;
/**
* @ORM\Column(type="string", length=5)
*/
private $role;
// ...
public function __construct()
{
$this->role = 'user';
$this->saltedPasswordHash = null;
}
public function getId()
{
return $this->id;
}
public function getSaltedPasswordHash(): ?string
{
return $this->saltedPasswordHash;
}
public function setSaltedPasswordHash(?string $saltedPasswordHash): self
{
$this->saltedPasswordHash = $saltedPasswordHash;
return $this;
}
public function getEmail(): ?string
{
return $this->email;
}
public function setEmail(?string $email): self
{
$this->email = $email;
return $this;
}
public function getCreated(): ?\DateTimeInterface
{
return $this->created;
}
public function setCreated(\DateTimeInterface $created): self
{
$this->created = $created;
return $this;
}
/**
* @return ArrayCollection
*/
public function getAuthTokens()
{
return $this->authTokens;
}
/**
* @param ArrayCollection $authTokens
* @return User
*/
public function setAuthTokens(ArrayCollection $authTokens): User
{
$this->authTokens = $authTokens;
return $this;
}
/**
* @param AuthToken $authToken
* @return User
*/
public function addAuthToken(AuthToken $authToken): User
{
$this->authTokens->add($authToken);
return $this;
}
/**
* @param AuthToken $authToken
* @return User
*/
public function removeAuthToken(AuthToken $authToken): User
{
$this->authTokens->removeElement($authToken);
return $this;
}
// ...
/**
* Returns the password used to authenticate the user.
*
* This should be the encoded password. On authentication, a plain-text
* password will be salted, encoded, and then compared to this value.
*
* @return string The password
*/
public function getPassword()
{
return $this->getSaltedPasswordHash();
}
/**
* Returns the salt that was originally used to encode the password.
*
* This can return null if the password was not encoded using a salt.
*
* @return string|null The salt
*/
public function getSalt()
{
// TODO: Implement getSalt() method.
}
/**
* Returns the username used to authenticate the user.
*
* @return string The username
*/
public function getUsername()
{
return $this->getEmail();
}
/**
* Removes sensitive data from the user.
*
* This is important if, at any given point, sensitive information like
* the plain-text password is stored on this object.
*/
public function eraseCredentials()
{
$this->setSaltedPasswordHash('');
}
/**
* @return mixed
*/
public function getRole()
{
return $this->role;
}
/**
* @param mixed $role
* @return User
*/
public function setRole($role): User
{
$this->role = $role;
return $this;
}
/**
* @return string
*/
public function __toString()
{
return "User " . $this->email;
}
/**
* The equality comparison should neither be done by referential equality
* nor by comparing identities (i.e. getId() === getId()).
*
* However, you do not need to compare every attribute, but only those that
* are relevant for assessing whether re-authentication is required.
*
* Also implementation should consider that $user instance may implement
* the extended user interface `AdvancedUserInterface`.
*
* https://stackoverflow.com/a/39884792/6144818
*
* @param UserInterface $user
* @return bool
*/
public function isEqualTo(UserInterface $user)
{
return (
$this->getUsername() == $user->getUsername()
) && (
$this->getRoles() == $user->getRoles()
);
}
// ...
}
namespace App\Entity;
use Doctrine\ORM\Mapping as ORM;
/**
* @ORM\Entity(repositoryClass="App\Repository\AuthTokenRepository")
*/
class AuthToken
{
/**
* @ORM\Id()
* @ORM\Column(type="decimal", precision=32, scale=0, options={"unsigned": true})
*/
private $id;
/**
* @ORM\ManyToOne(targetEntity="App\Entity\User", inversedBy="authTokens")
*/
private $user;
/**
* @ORM\Column(type="datetime")
*/
private $added;
/**
* @ORM\Column(type="datetime", nullable=true)
*/
private $lastSeen;
/**
* @ORM\Column(type="string", length=12, nullable=true)
*/
private $apiVersion;
/**
* @return string
*/
public function getId(): string
{
return $this->id;
}
/**
* @return string
*/
public function getHexId(): string
{
return $this->dec2hex($this->id);
}
/**
* @param mixed $id
*/
public function setId($id): void
{
$this->id = $id;
}
/**
* @param mixed $id
* @throws \Exception
*/
public function generateId(): void
{
$length = 32;
$str = "";
$characters = range('0', '9');
$max = count($characters) - 1;
for ($i = 0; $i < $length; $i++) {
$rand = random_int(0, $max);
$str .= $characters[$rand];
}
$this->id = $str;
}
/**
* @return mixed
*/
public function getUser()
{
return $this->user;
}
/**
* @param mixed $user
*/
public function setUser($user): void
{
$this->user = $user;
}
/**
* @return mixed
*/
public function getAdded()
{
return $this->added;
}
/**
* @param mixed $added
*/
public function setAdded($added): void
{
$this->added = $added;
}
/**
* @return mixed
*/
public function getLastSeen()
{
return $this->lastSeen;
}
/**
* @param mixed $lastSeen
*/
public function setLastSeen($lastSeen): void
{
$this->lastSeen = $lastSeen;
}
public function getApiVersion(): ?string
{
return $this->apiVersion;
}
public function setApiVersion(string $apiVersion): self
{
$this->apiVersion = $apiVersion;
return $this;
}
public static function dec2hex(string $dec): string
{
$hex = '';
do {
$last = bcmod($dec, 16);
$hex = dechex($last) . $hex;
$dec = bcdiv(bcsub($dec, $last), 16);
} while ($dec > 0);
return $hex;
}
public static function hex2dec($hex)
{
$dec = '0';
$len = strlen($hex);
for ($i = 1; $i <= $len; $i++)
$dec = bcadd($dec, bcmul(strval(hexdec($hex[$i - 1])), bcpow('16', strval($len - $i))));
return $dec;
}
/**
* @return string
*/
public function __toString()
{
return "AuthToken " . $this->id . " (" . $this->user . ")";
}
}
Entitiy/AuthToken.php
namespace App\Security;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Security\Core\Authentication\Token\PreAuthenticatedToken;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
use Symfony\Component\Security\Core\Exception\BadCredentialsException;
use Symfony\Component\Security\Core\User\UserProviderInterface;
use Symfony\Component\Security\Http\Authentication\SimplePreAuthenticatorInterface;
class ApiKeyAuthenticator implements SimplePreAuthenticatorInterface
{
public function createToken(Request $request, $providerKey)
{
$apiKey = $request->headers->get('authToken');
if (!$apiKey) {
throw new BadCredentialsException();
// or to just skip api key authentication
// return null;
}
return new PreAuthenticatedToken(
'anon.',
$apiKey,
$providerKey
);
}
public function supportsToken(TokenInterface $token, $providerKey)
{
return $token instanceof PreAuthenticatedToken && $token->getProviderKey() === $providerKey;
}
public function authenticateToken(TokenInterface $token, UserProviderInterface $userProvider, $providerKey)
{
if (!$userProvider instanceof ApiKeyUserProvider) {
throw new \InvalidArgumentException(
sprintf(
'The user provider must be an instance of ApiKeyUserProvider (%s was given).',
get_class($userProvider)
)
);
}
$apiKey = $token->getCredentials();
$username = $userProvider->getUsernameForApiKey($apiKey);
if (!$username) {
// CAUTION: this message will be returned to the client
// (so don't put any un-trusted messages / error strings here)
throw new BadCredentialsException(
sprintf('API Key "%s" does not exist.', $apiKey)
);
}
$user = $userProvider->loadUserByAuthToken($apiKey);
if (!isset($user)) {
throw new BadCredentialsException(
sprintf('API Key "%s" does not exist.', $apiKey)
);
}
// TODO: HERE, THE $user->getSaltedPasswordHash() RETURNS THE CORRECT VALUE!
return new PreAuthenticatedToken(
$user, // TODO: with "new User()" instead, it works!
$apiKey,
$providerKey,
$user->getRoles()
);
}
public function onAuthenticationFailure(Request $request, AuthenticationException $exception)
{
return new Response(
// this contains information about *why* authentication failed
// use it, or return your own message
strtr($exception->getMessageKey(), $exception->getMessageData()),
401
);
}
}
namespace App\Security;
use App\Entity\AuthToken;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\Security\Core\User\UserProviderInterface;
use Symfony\Component\Security\Core\User\User;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Security\Core\Exception\UnsupportedUserException;
class ApiKeyUserProvider implements UserProviderInterface
{
/**
* @var EntityManagerInterface
*/
private $em;
/**
* ApiKeyUserProvider constructor.
* @param EntityManagerInterface $em
*/
public function __construct(EntityManagerInterface $em)
{
$this->em = $em;
}
public function getUsernameForApiKey($apiKey)
{
return $apiKey;
}
public function loadUserByUsername($username)
{
// TODO: Implement loadUserByUsername() method.
}
/**
* Auth token is used as username
*
* @param string $authToken
* @return null|UserInterface
*/
public function loadUserByAuthToken($authToken): ?UserInterface
{
if (!isset($authToken)) {
return null;
}
$token = $this->em
->getRepository(AuthToken::class)
->findOneBy(['id' => AuthToken::hex2dec($authToken)]);
if (!isset($token)) {
return null;
}
return $token->getUser();
}
public
function refreshUser(UserInterface $user)
{
// this is used for storing authentication in the session
// but in this example, the token is sent in each request,
// so authentication can be stateless. Throwing this exception
// is proper to make things stateless
throw new UnsupportedUserException();
}
public
function supportsClass($class)
{
return User::class === $class;
}
}
namespace App\Entity;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Security\Core\User\EquatableInterface;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Validator\Constraints as Assert;
/**
* @ORM\Entity(repositoryClass="App\Repository\UserRepository")
*/
class User implements UserInterface, EquatableInterface
{
/**
* @ORM\Id()
* @ORM\GeneratedValue()
* @ORM\Column(type="integer")
*/
private $id;
/**
* This needs to be nullable because the email includes the id of newly created users, which can only be obtained after inserting the new record.
* @ORM\Column(type="string", length=255, nullable=true, unique=true)
* @Assert\Length(max=255)
* @Assert\NotBlank()
*/
private $email;
/**
* Set null to disable login
* @ORM\Column(type="string", length=255, nullable=true)
* @Assert\Length(max=255)
* @Assert\NotBlank()
*/
private $saltedPasswordHash;
/**
* @ORM\Column(type="datetime")
*/
private $created;
// ...
/**
* @ORM\OneToMany(targetEntity="App\Entity\AuthToken", mappedBy="user", fetch="LAZY")
*/
private $authTokens;
/**
* @ORM\Column(type="string", length=5)
*/
private $role;
// ...
public function __construct()
{
$this->role = 'user';
$this->saltedPasswordHash = null;
}
public function getId()
{
return $this->id;
}
public function getSaltedPasswordHash(): ?string
{
return $this->saltedPasswordHash;
}
public function setSaltedPasswordHash(?string $saltedPasswordHash): self
{
$this->saltedPasswordHash = $saltedPasswordHash;
return $this;
}
public function getEmail(): ?string
{
return $this->email;
}
public function setEmail(?string $email): self
{
$this->email = $email;
return $this;
}
public function getCreated(): ?\DateTimeInterface
{
return $this->created;
}
public function setCreated(\DateTimeInterface $created): self
{
$this->created = $created;
return $this;
}
/**
* @return ArrayCollection
*/
public function getAuthTokens()
{
return $this->authTokens;
}
/**
* @param ArrayCollection $authTokens
* @return User
*/
public function setAuthTokens(ArrayCollection $authTokens): User
{
$this->authTokens = $authTokens;
return $this;
}
/**
* @param AuthToken $authToken
* @return User
*/
public function addAuthToken(AuthToken $authToken): User
{
$this->authTokens->add($authToken);
return $this;
}
/**
* @param AuthToken $authToken
* @return User
*/
public function removeAuthToken(AuthToken $authToken): User
{
$this->authTokens->removeElement($authToken);
return $this;
}
// ...
/**
* Returns the password used to authenticate the user.
*
* This should be the encoded password. On authentication, a plain-text
* password will be salted, encoded, and then compared to this value.
*
* @return string The password
*/
public function getPassword()
{
return $this->getSaltedPasswordHash();
}
/**
* Returns the salt that was originally used to encode the password.
*
* This can return null if the password was not encoded using a salt.
*
* @return string|null The salt
*/
public function getSalt()
{
// TODO: Implement getSalt() method.
}
/**
* Returns the username used to authenticate the user.
*
* @return string The username
*/
public function getUsername()
{
return $this->getEmail();
}
/**
* Removes sensitive data from the user.
*
* This is important if, at any given point, sensitive information like
* the plain-text password is stored on this object.
*/
public function eraseCredentials()
{
$this->setSaltedPasswordHash('');
}
/**
* @return mixed
*/
public function getRole()
{
return $this->role;
}
/**
* @param mixed $role
* @return User
*/
public function setRole($role): User
{
$this->role = $role;
return $this;
}
/**
* @return string
*/
public function __toString()
{
return "User " . $this->email;
}
/**
* The equality comparison should neither be done by referential equality
* nor by comparing identities (i.e. getId() === getId()).
*
* However, you do not need to compare every attribute, but only those that
* are relevant for assessing whether re-authentication is required.
*
* Also implementation should consider that $user instance may implement
* the extended user interface `AdvancedUserInterface`.
*
* https://stackoverflow.com/a/39884792/6144818
*
* @param UserInterface $user
* @return bool
*/
public function isEqualTo(UserInterface $user)
{
return (
$this->getUsername() == $user->getUsername()
) && (
$this->getRoles() == $user->getRoles()
);
}
// ...
}
namespace App\Entity;
use Doctrine\ORM\Mapping as ORM;
/**
* @ORM\Entity(repositoryClass="App\Repository\AuthTokenRepository")
*/
class AuthToken
{
/**
* @ORM\Id()
* @ORM\Column(type="decimal", precision=32, scale=0, options={"unsigned": true})
*/
private $id;
/**
* @ORM\ManyToOne(targetEntity="App\Entity\User", inversedBy="authTokens")
*/
private $user;
/**
* @ORM\Column(type="datetime")
*/
private $added;
/**
* @ORM\Column(type="datetime", nullable=true)
*/
private $lastSeen;
/**
* @ORM\Column(type="string", length=12, nullable=true)
*/
private $apiVersion;
/**
* @return string
*/
public function getId(): string
{
return $this->id;
}
/**
* @return string
*/
public function getHexId(): string
{
return $this->dec2hex($this->id);
}
/**
* @param mixed $id
*/
public function setId($id): void
{
$this->id = $id;
}
/**
* @param mixed $id
* @throws \Exception
*/
public function generateId(): void
{
$length = 32;
$str = "";
$characters = range('0', '9');
$max = count($characters) - 1;
for ($i = 0; $i < $length; $i++) {
$rand = random_int(0, $max);
$str .= $characters[$rand];
}
$this->id = $str;
}
/**
* @return mixed
*/
public function getUser()
{
return $this->user;
}
/**
* @param mixed $user
*/
public function setUser($user): void
{
$this->user = $user;
}
/**
* @return mixed
*/
public function getAdded()
{
return $this->added;
}
/**
* @param mixed $added
*/
public function setAdded($added): void
{
$this->added = $added;
}
/**
* @return mixed
*/
public function getLastSeen()
{
return $this->lastSeen;
}
/**
* @param mixed $lastSeen
*/
public function setLastSeen($lastSeen): void
{
$this->lastSeen = $lastSeen;
}
public function getApiVersion(): ?string
{
return $this->apiVersion;
}
public function setApiVersion(string $apiVersion): self
{
$this->apiVersion = $apiVersion;
return $this;
}
public static function dec2hex(string $dec): string
{
$hex = '';
do {
$last = bcmod($dec, 16);
$hex = dechex($last) . $hex;
$dec = bcdiv(bcsub($dec, $last), 16);
} while ($dec > 0);
return $hex;
}
public static function hex2dec($hex)
{
$dec = '0';
$len = strlen($hex);
for ($i = 1; $i <= $len; $i++)
$dec = bcadd($dec, bcmul(strval(hexdec($hex[$i - 1])), bcpow('16', strval($len - $i))));
return $dec;
}
/**
* @return string
*/
public function __toString()
{
return "AuthToken " . $this->id . " (" . $this->user . ")";
}
}
namespace-App\Entity;
使用条令\ORM\Mapping作为ORM;
/**
*@ORM\Entity(repositoryClass=“App\Repository\AuthTokenRepository”)
*/
类AuthToken
{
/**
*@ORM\Id()
*@ORM\Column(type=“decimal”,精度=32,刻度=0,选项={“unsigned”:true})
*/
私人$id;
/**
*@ORM\manytone(targetEntity=“App\Entity\User”,inversedBy=“authTokens”)
*/
私人用户;
/**
*@ORM\Column(type=“datetime”)
*/
私人股本增加$;
/**
*@ORM\Column(type=“datetime”,nullable=true)
*/
私用$lastSeen;
/**
*@ORM\Column(type=“string”,length=12,nullable=true)
*/
私人版本;
/**
*@返回字符串
*/
公共函数getId():字符串
{
返回$this->id;
}
/**
*@返回字符串
*/
公共函数getHexId():字符串
{
返回$this->dec2hex($this->id);
}
/**
*@param混合$id
*/
公共函数setId($id):无效
{
$this->id=$id;
}
/**
*@param混合$id
*@throws\Exception
*/
公共函数generateId():void
{
$length=32;
$str=”“;
$characters=范围('0','9');
$max=计数($characters)-1;
对于($i=0;$i<$length;$i++){
$rand=random_int(0,$max);
$str.=$characters[$rand];
}
$this->id=$str;
}
/**
*@返回混合
*/
公共函数getUser()
{
返回$this->user;
}
/**
*@param混合$user
*/
公共函数setUser($user):无效
{
$this->user=$user;
}
/**
*@返回混合
*/
公共函数getAdded()
{
返回$this->added;
}
/**
*@param mixed$added
*/
公共函数setAdded($added):无效
{
$this->added=$added;
}
/**
*@返回混合
*/
公共函数getLastSeen()
{
返回$this->lastseed;
}
/**
*@param mixed$lastSeen
*/
公共函数setLastSeen($lastSeen):无效
{
$this->lastSeen=$lastSeen;
}
公共函数getApiVersion():?字符串
{
返回$this->apipversion;
}
公共函数setApiVersion(字符串$apiVersion):self
{
$this->apiVersion=$apiVersion;
退还$this;
}
公共静态函数dec2hex(字符串$dec):字符串
{
$hex='';
做{
$last=bcmod(12月16日);
$hex=dechex($last)。$hex;
$dec=bcdiv(bcsub($dec$last),16);
}而($dec>0);
返回$hex;
}
公共静态函数hex2dec($hex)
{
$dec='0';
$len=strlen($hex);
对于($i=1;$i id.)(“$this->user.”);
}
}
您的实体/User.php的此函数是您行为的原因:
/**
* Removes sensitive data from the user.
*
* This is important if, at any given point, sensitive information like
* the plain-text password is stored on this object.
*/
public function eraseCredentials()
{
$this->setSaltedPasswordHash('');
}
当身份验证在Symfony上起作用时,在身份验证之后,它不会泄漏敏感信息,甚至更糟糕的是,敏感信息不会在会话中结束
只要试着在该函数中对setter进行注释,您就会得到您期望的结果。非常有用!非常感谢!