18/08/2018 - SYMFONY
Lexik gibi üçüncü şahıs kimlik doğrulama paketlerini kullanmak istemeyenler, API'lerinda JWT kimlik doğrulamasını uygulamak için bu örneği kullanabilirler.
500
yerine daha uygun cevap kodları gönderebilirsiniz.$ bin/console debug:router
----------------------- -------- -------- ------ -------------------
Name Method Scheme Host Path
----------------------- -------- -------- ------ -------------------
app_country_getall GET ANY ANY /api/v1/countries # SECURED
app_country_createone POST ANY ANY /api/v1/countries # SECURED
home GET ANY ANY / # UNSECURED
login POST ANY ANY /login # UNSECURED
----------------------- -------- -------- ------ -------------------
Diğer bilinen paketlerin haricinde annotations
, sensio/framework-extra-bundle
, symfony/security
, symfony/expression-language
, firebase/php-jwt
ve ramsey/uuid
paketlerinide yüklemelisiniz.
$ ssh-keygen -t rsa -b 4096 -f jwt/rsa_256 # Hit enter for all questions
$ openssl rsa -in jwt/rsa_256 -pubout -outform PEM -out jwt/rsa_256.pub
Güvenlik nedeniyle bu dosyaları versiyon kontrolüne eklemeyin.
sensio_framework_extra:
router:
annotations: false
security:
annotations: true
home:
path: /
methods: [GET]
controller: App\Controller\HomeController::home
login:
path: /login
methods: [POST]
controller: App\Controller\LoginController::login
controllers:
resource: ../../src/Controller/
type: annotation
prefix: /api/v1
security:
encoders:
App\Entity\User:
algorithm: bcrypt
cost: 13
role_hierarchy:
ROLE_USER: ROLE_USER
ROLE_ADMIN: ROLE_USER
providers:
jwt_user_provider:
id: App\Security\JwtUserProvider
firewalls:
jwt:
pattern: ^/api
stateless: true
simple_preauth:
authenticator: App\Security\JwtUserAuthenticator
provider: jwt_user_provider
imports:
- { resource: services/* }
parameters:
jwt_ttl: '+1 hour'
jwt_algorithm: 'RS256'
jwt_private_key: '%kernel.project_dir%/jwt/rsa_256'
jwt_public_key: '%kernel.project_dir%/jwt/rsa_256.pub'
services:
_defaults:
autowire: true
autoconfigure: true
public: false
App\:
resource: '../src/*'
exclude: '../src/{Entity,Command,Controller,Repository,Security,Service,Util,Kernel.php}'
declare(strict_types=1);
namespace App\Controller;
use Symfony\Component\HttpFoundation\Response;
class HomeController
{
public function home(): Response
{
return new Response('Welcome to the API application.');
}
}
Her şeyi bu controller içinde yapmamın tek nedeni örneği kısa kesmek istememdir, ama bildiğiniz gibi bu tarz şeyler "kötü" şeylerdir o nedenle siz bunu yapmayın. Gördüğünüz gibi isteği bile kontrol etmiyorum. Size tavsiyem kodun bir kısmını servis class içine, bir kısmını ise factory class içine almanız olacaktır.
declare(strict_types=1);
namespace App\Controller;
use App\Entity\Token;
use App\Entity\User;
use App\Repository\UserRepositoryInterface;
use App\Util\JwtUtilInterface;
use DateTime;
use Doctrine\ORM\EntityManagerInterface;
use Ramsey\Uuid\Uuid;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Exception\UnauthorizedHttpException;
use Symfony\Component\Security\Core\Encoder\UserPasswordEncoderInterface;
class LoginController
{
private $userRepository;
private $entityManager;
private $userPasswordEncoder;
private $jwtUtil;
private $jwtTtl;
public function __construct(
UserRepositoryInterface $userRepository,
EntityManagerInterface $entityManager,
UserPasswordEncoderInterface $userPasswordEncoder,
JwtUtilInterface $jwtUtil,
string $jwtTtl
) {
$this->userRepository = $userRepository;
$this->entityManager = $entityManager;
$this->userPasswordEncoder = $userPasswordEncoder;
$this->jwtUtil = $jwtUtil;
$this->jwtTtl = $jwtTtl;
}
public function login(Request $request): Response
{
$data = json_decode($request->getContent(), true);
$user = $this->userRepository->findOneActiveByUsername($data['username']);
if (
!$user instanceof User ||
!$this->userPasswordEncoder->isPasswordValid($user, $data['password'])
) {
throw new UnauthorizedHttpException('Basic realm="API Login"', 'Invalid credentials.');
}
$id = Uuid::uuid4()->toString();
$createdAt = (new DateTime())->format(DATE_ISO8601);
$expiresAt = (new DateTime())->modify($this->jwtTtl)->format(DATE_ISO8601);
$tokenData = [
'id' => $id,
'created_at' => $createdAt,
'expires_at' => $expiresAt,
'user' => [
'id' => $user->getId(),
'roles' => $user->getRoles(),
],
];
$token = new Token();
$token->setId($id);
$token->setCreatedAt($createdAt);
$token->setExpiresAt($expiresAt);
$token->setUser($user);
$token->setData($this->jwtUtil->encode($tokenData));
$this->entityManager->persist($token);
$this->entityManager->flush();
return new Response($token->getData(), Response::HTTP_CREATED);
}
}
declare(strict_types=1);
namespace App\Controller;
use App\Security\JwtUserInterface;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
/**
* @Route("/countries")
*/
class CountryController
{
private $jwtUser;
public function __construct(JwtUserInterface $jwtUser)
{
$this->jwtUser = $jwtUser;
}
/**
* @Route("", methods="GET")
* @Security("has_role('ROLE_USER')")
*/
public function getAll(): Response
{
return new Response($this->getUserData());
}
/**
* @Route("", methods="POST")
* @Security("has_role('ROLE_ADMIN')")
*/
public function createOne(): Response
{
return new Response($this->getUserData());
}
private function getUserData(): string
{
return json_encode([
'user_id' => $this->jwtUser->get()->getId(),
'user_roles' => $this->jwtUser->get()->getRoles(),
]);
}
}
# config/services/controllers.yaml
services:
_defaults:
autowire: true
autoconfigure: true
public: true
App\Controller\:
resource: '../../src/Controller'
tags: ['controller.service_arguments']
App\Controller\LoginController:
arguments:
$jwtTtl: '%jwt_ttl%'
Bir user birden fazla token sahibi olabilir.
declare(strict_types=1);
namespace App\Entity;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
use Ramsey\Uuid\Uuid;
use Symfony\Component\Security\Core\User\UserInterface;
/**
* @ORM\Entity
* @ORM\Table(name="users")
*/
class User implements UserInterface
{
/**
* @ORM\Id
* @ORM\Column(name="id", type="string", length=36, nullable=false)
* @ORM\GeneratedValue(strategy="NONE")
*/
private $id;
/**
* @ORM\Column(name="username", type="string", length=100, nullable=false, unique=true)
*/
private $username;
/**
* @ORM\Column(name="password", type="string", length=100, nullable=false)
*/
private $password;
/**
* @ORM\Column(name="email", type="string", length=100, nullable=false, unique=true)
*/
private $email;
/**
* @ORM\Column(name="roles", type="array")
*/
private $roles;
/**
* @ORM\Column(name="is_active", type="boolean", nullable=false)
*/
private $isActive = true;
/**
* @ORM\OneToMany(targetEntity="Token", mappedBy="user", cascade={"persist", "remove"})
*/
private $tokens;
public function __construct()
{
$this->id = Uuid::uuid4()->toString();
$this->tokens = new ArrayCollection();
}
public function getId(): string
{
return $this->id;
}
public function getUsername()
{
return $this->username;
}
public function setUsername(string $username): self
{
$this->username = $username;
return $this;
}
public function getPassword()
{
return $this->password;
}
public function setPassword(string $password): self
{
$this->password = $password;
return $this;
}
public function getEmail(): string
{
return $this->email;
}
public function setEmail(string $email): self
{
$this->email = $email;
return $this;
}
public function getIsActive(): bool
{
return $this->isActive;
}
public function setIsActive(bool $isActive): self
{
$this->isActive = $isActive;
return $this;
}
public function getRoles()
{
return $this->roles;
}
public function setRoles(iterable $roles): self
{
$this->roles = $roles;
return $this;
}
public function getSalt()
{
}
public function eraseCredentials()
{
}
public function addToken(Token $token): self
{
$this->tokens[] = $token;
return $this;
}
public function removeToken(Token $token): bool
{
return $this->tokens->removeElement($token);
}
public function getTokens(): Collection
{
return $this->tokens;
}
}
declare(strict_types=1);
namespace App\Entity;
use Doctrine\ORM\Mapping as ORM;
/**
* @ORM\Entity
* @ORM\Table(name="tokens")
*/
class Token
{
/**
* @ORM\Id
* @ORM\Column(name="id", type="string", length=36, nullable=false)
* @ORM\GeneratedValue(strategy="NONE")
*/
private $id;
/**
* @ORM\Column(name="data", type="text", nullable=false)
*/
private $data;
/**
* @ORM\Column(name="created_at", type="string", length=30, nullable=false)
*/
private $createdAt;
/**
* @ORM\Column(name="expires_at", type="string", length=30, nullable=false)
*/
private $expiresAt;
/**
* @ORM\ManyToOne(targetEntity="User", inversedBy="tokens", cascade={"persist"})
* @ORM\JoinColumn(name="user_id", referencedColumnName="id", nullable=true)
*/
private $user;
public function setId(string $id): self
{
$this->id = $id;
return $this;
}
public function getId(): string
{
return $this->id;
}
public function setData(string $data): self
{
$this->data = $data;
return $this;
}
public function getData(): string
{
return $this->data;
}
public function setCreatedAt(string $createdAt): self
{
$this->createdAt = $createdAt;
return $this;
}
public function getCreatedAt(): string
{
return $this->createdAt;
}
public function setExpiresAt(string $expiresAt): self
{
$this->expiresAt = $expiresAt;
return $this;
}
public function getExpiresAt(): string
{
return $this->expiresAt;
}
public function setUser(User $user): self
{
$this->user = $user;
return $this;
}
public function getUser(): User
{
return $this->user;
}
}
declare(strict_types=1);
namespace App\Repository;
use App\Entity\User;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\EntityRepository;
use Doctrine\ORM\Query;
class UserRepository implements UserRepositoryInterface
{
private $entityManager;
private $entityRepository;
public function __construct(
EntityManagerInterface $entityManager,
EntityRepository $entityRepository
) {
$this->entityManager = $entityManager;
$this->entityRepository = $entityRepository;
}
public function findOneActiveById(string $id): ?User
{
return $this->entityRepository
->createQueryBuilder('u')
->where('u.id = :id')
->andWhere('u.isActive = true')
->setParameter('id', $id)
->getQuery()
->getOneOrNullResult(Query::HYDRATE_SIMPLEOBJECT);
}
public function findOneActiveByUsername(string $username): ?User
{
return $this->entityRepository
->createQueryBuilder('u')
->where('u.username = :username')
->andWhere('u.isActive = true')
->setParameter('username', $username)
->getQuery()
->setHint(Query::HINT_FORCE_PARTIAL_LOAD, true)
->getOneOrNullResult(Query::HYDRATE_SIMPLEOBJECT);
}
}
declare(strict_types=1);
namespace App\Repository;
use App\Entity\User;
interface UserRepositoryInterface
{
public function findOneActiveById(string $id): ?User;
public function findOneActiveByUsername(string $username): ?User;
}
# config/services/repositories.yaml
services:
_defaults:
autowire: true
autoconfigure: true
public: false
App\Repository\:
resource: '../../src/Repository'
app.entity_repository.user:
class: Doctrine\ORM\EntityRepository
factory: ['@doctrine.orm.entity_manager', getRepository]
arguments:
- App\Entity\User
App\Repository\UserRepository:
arguments:
$entityRepository: '@app.entity_repository.user'
declare(strict_types=1);
namespace App\Command;
use App\Entity\User;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Security\Core\Encoder\UserPasswordEncoderInterface;
class CreateUserCommand extends Command
{
private $entityManager;
private $userPasswordEncoder;
public function __construct(
EntityManagerInterface $entityManager,
UserPasswordEncoderInterface $userPasswordEncoder
) {
parent::__construct();
$this->entityManager = $entityManager;
$this->userPasswordEncoder = $userPasswordEncoder;
}
protected function configure()
{
$this
->setName('app:create-user')
->setDescription('Creates a user entry in database.')
->addOption(
'username',
null,
InputOption::VALUE_REQUIRED,
'The username of the user.'
)
->addOption(
'email',
null,
InputOption::VALUE_REQUIRED,
'The email of the user.'
)
->addOption(
'roles',
null,
InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY,
'The roles of the user.'
);
}
protected function execute(InputInterface $input, OutputInterface $output)
{
$password = bin2hex(random_bytes(16));
$this->createUser($input, $password);
$this->outputCredentials($output, $input->getOption('username'), $password);
}
private function createUser(InputInterface $input, $password)
{
$user = new User();
$user->setUsername($input->getOption('username'));
$user->setEmail($input->getOption('email'));
$user->setRoles($input->getOption('roles'));
$password = $this->userPasswordEncoder->encodePassword($user, $password);
$user->setPassword($password);
$this->entityManager->persist($user);
$this->entityManager->flush();
}
private function outputCredentials(OutputInterface $output, $username, $password)
{
$output->writeln(PHP_EOL.'CREDENTIALS');
$output->writeln('-----------');
$output->writeln('Username: '.$username);
$output->writeln('Password: '.$password.PHP_EOL);
}
}
# config/services/commands.yaml
services:
_defaults:
autowire: true
autoconfigure: true
public: false
App\Command\:
resource: '../../src/Command'
app.command.create_user:
class: App\Command\CreateUserCommand
tags:
- { name: console.command }
$ bin/console app:create-user --username="basic" --email="basic@domain.com" --roles="ROLE_USER"
CREDENTIALS
-----------
Username: basic
Password: 5b18898f343e6a763105a2fab26b9d5b
$ bin/console app:create-user --username="admin" --email="admin@domain.com" --roles="ROLE_USER" --roles="ROLE_ADMIN"
CREDENTIALS
-----------
Username: admin
Password: b5e71d52c22c80d25dee18ad1d7fd441
Kullanıcı /login
adresini çağırıp token almak istediğinde, yukarıdaki kullanıcı bilgilerini POST
ile kullanacaklar.
declare(strict_types=1);
namespace App\Security;
use App\Entity\User;
use App\Util\JwtUtilInterface;
use DateTime;
use Exception;
use InvalidArgumentException;
use stdClass;
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\CustomUserMessageAuthenticationException;
use Symfony\Component\Security\Core\User\UserProviderInterface;
use Symfony\Component\Security\Http\Authentication\AuthenticationFailureHandlerInterface;
use Symfony\Component\Security\Http\Authentication\SimplePreAuthenticatorInterface;
class JwtUserAuthenticator implements SimplePreAuthenticatorInterface, AuthenticationFailureHandlerInterface
{
private $jwtUtil;
public function __construct(JwtUtilInterface $jwtUtil)
{
$this->jwtUtil = $jwtUtil;
}
public function createToken(Request $request, $providerKey)
{
$token = $request->headers->get('Authorization');
if (!$token) {
throw new CustomUserMessageAuthenticationException('Missing token.');
}
return new PreAuthenticatedToken('anon.', $token, $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 JwtUserProvider) {
throw new InvalidArgumentException('Invalid provider.');
}
$tokenData = $this->validateToken($token);
$user = $userProvider->loadUserByUsername($tokenData->user->id);
if (!$user instanceof User) {
throw new CustomUserMessageAuthenticationException('User not found.');
}
return new PreAuthenticatedToken($user, $user->getUsername(), $providerKey, $user->getRoles());
}
public function onAuthenticationFailure(Request $request, AuthenticationException $exception)
{
return new Response($exception->getMessageKey(), Response::HTTP_UNAUTHORIZED);
}
public function onAuthenticationSuccess(Request $request, TokenInterface $token, $providerKey)
{
return null;
}
private function validateToken(TokenInterface $token): stdClass
{
preg_match('/^Bearer\s(\S+)/i', $token->getCredentials(), $matches);
if (!$matches) {
throw new CustomUserMessageAuthenticationException('Invalid token.');
}
try {
$tokenData = $this->jwtUtil->decode($matches[1]);
} catch (Exception $e) {
throw new CustomUserMessageAuthenticationException('Invalid token.');
}
$expiresAt = new DateTime($tokenData->expires_at);
if ($expiresAt < new DateTime()) {
throw new CustomUserMessageAuthenticationException('Token expired.');
}
return $tokenData;
}
}
declare(strict_types=1);
namespace App\Security;
use App\Entity\User;
use App\Repository\UserRepositoryInterface;
use Symfony\Component\Security\Core\Exception\UnsupportedUserException;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Security\Core\User\UserProviderInterface;
class JwtUserProvider implements UserProviderInterface
{
private $userRepository;
public function __construct(UserRepositoryInterface $userRepository)
{
$this->userRepository = $userRepository;
}
public function loadUserByUsername($id)
{
return $this->userRepository->findOneActiveById($id);
}
public function refreshUser(UserInterface $user)
{
throw new UnsupportedUserException();
}
public function supportsClass($class)
{
return User::class === $class;
}
}
declare(strict_types=1);
namespace App\Security;
use App\Entity\User;
interface JwtUserInterface
{
public function get(): User;
}
declare(strict_types=1);
namespace App\Security;
use App\Entity\User;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
use Symfony\Component\Security\Core\Exception\TokenNotFoundException;
class JwtUser implements JwtUserInterface
{
private $tokenStorage;
public function __construct(
TokenStorageInterface $tokenStorage
) {
$this->tokenStorage = $tokenStorage;
}
public function get(): User
{
$user = $this->tokenStorage->getToken()->getUser();
if (!$user instanceof User) {
throw new TokenNotFoundException('User not found.');
}
return $user;
}
}
# config/services/securities.yaml
services:
_defaults:
autowire: true
autoconfigure: true
public: false
App\Security\:
resource: '../../src/Security'
declare(strict_types=1);
namespace App\Util;
use Firebase\JWT\JWT;
use stdClass;
class JwtUtil implements JwtUtilInterface
{
private $jwtAlgorithm;
private $jwtPrivateKey;
private $jwtPublicKey;
public function __construct(
string $jwtAlgorithm,
string $jwtPrivateKey,
string $jwtPublicKey
) {
$this->jwtAlgorithm = $jwtAlgorithm;
$this->jwtPrivateKey = $jwtPrivateKey;
$this->jwtPublicKey = $jwtPublicKey;
}
public function encode(iterable $tokenData): string
{
return JWT::encode($tokenData, file_get_contents($this->jwtPrivateKey), $this->jwtAlgorithm);
}
public function decode(string $tokenString): stdClass
{
return JWT::decode($tokenString, file_get_contents($this->jwtPublicKey), [$this->jwtAlgorithm]);
}
}
declare(strict_types=1);
namespace App\Util;
use stdClass;
interface JwtUtilInterface
{
public function encode(iterable $tokenData): string;
public function decode(string $tokenString): stdClass;
}
# config/services/utils.yaml
services:
_defaults:
autowire: true
autoconfigure: true
public: false
App\Util\:
resource: '../../src/Util'
App\Util\JwtUtil:
arguments:
$jwtAlgorithm: '%jwt_algorithm%'
$jwtPrivateKey: '%jwt_private_key%'
$jwtPublicKey: '%jwt_public_key%'
mysql> SELECT * FROM users;
+--------------------------------------+----------+--------------------------------------------------------------+------------------+--------------------------------------------------+-----------+
| id | username | password | email | roles | is_active |
+--------------------------------------+----------+--------------------------------------------------------------+------------------+--------------------------------------------------+-----------+
| cdac445a-27cf-4a3c-aa5d-2fcff32ad135 | basic | $2y$13$OsqUKZBUEl9995BjQp4spux00TXvdnJHss4QQricpUCZh2NhIO0S. | basic@domain.com | a:1:{i:0;s:9:"ROLE_USER";} | 1 |
| f53bf3d5-f3e6-4c18-b9a4-4b45921b7b94 | admin | $2y$13$X73aR26ctEUQn5byVtiheeMfGyYhoERN1cjY5Dtt3847.KjffQwHa | admin@domain.com | a:2:{i:0;s:9:"ROLE_USER";i:1;s:10:"ROLE_ADMIN";} | 1 |
+--------------------------------------+----------+--------------------------------------------------------------+------------------+--------------------------------------------------+-----------+
2 rows in set (0.00 sec)
$ curl -i -X POST http://192.168.99.20:81/login
-H 'Content-Type: application/json'
-d '{"username": "basic","password":"5b18898f343e6a763105a2fab26b9d5b"}'
HTTP/1.1 201 Created
Server: nginx/1.6.2
Content-Type: text/html; charset=UTF-8
Transfer-Encoding: chunked
Connection: keep-alive
Cache-Control: no-cache, private
Date: Sat, 18 Aug 2018 20:56:32 GMT
eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJpZCI6ImZlODAzO...........
Eğer JWT token içeriğini kontrol etmek isterseniz https://jwt.io/ adresini kullanabilirsiniz.
$ curl -i -X GET http://192.168.99.20:81/api/v1/countries
-H 'Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJpZCI6ImZlODAzO...........'
HTTP/1.1 200 OK
Server: nginx/1.6.2
Content-Type: text/html; charset=UTF-8
Transfer-Encoding: chunked
Connection: keep-alive
Cache-Control: no-cache, private
Date: Sat, 18 Aug 2018 20:58:58 GMT
{"user_id":"cdac445a-27cf-4a3c-aa5d-2fcff32ad135","user_roles":["ROLE_USER"]}