Bu örneğimizde, API uygulamamızı kullanabilmeleri için iki farklı kullanıcı çeşidini doğrulayacağız. Biri "Internal" diğeri ise "API" olacak. "Internal" versiyonu "Basic", "API" versiyonu ise "API Keys" doğrulama (authentication) kullanacak. Her iki doğrulama aynı URL'yi kullanıyor.


API adresleri



Eğer isterseniz daha fazla API adresi ekleyebilirsiniz ve yukarıdakinin aksine, sadece istediğiniz "role" yarattığınız adreslere erişebilir.


routing.yml


customer:
resource: "@CustomerBundle/Controller/"
type: annotation
prefix: /v1/

security.yml


security:

encoders:
CustomerBundle\Entity\User:
algorithm: bcrypt
cost: 12

role_hierarchy:
ROLE_INTERNAL_USER: ROLE_USER
ROLE_API_USER: ROLE_USER
ROLE_ADMIN_USER: ROLE_USER

providers:
basic_user_provider:
entity:
class: CustomerBundle:User
property: username
api_key_user_provider:
id: customer.security.api_key_user_provider

firewalls:
secured:
pattern: ^/
stateless: true
simple_preauth:
authenticator: customer.security.api_key_authenticator
provider: api_key_user_provider
http_basic:
realm: "Internal Services"
provider: basic_user_provider

CustomerController


namespace CustomerBundle\Controller;

use CustomerBundle\Security\ApiUserHelper;
use CustomerBundle\Service\CustomerService;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Method;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;

/**
* @Route("/customers", service="customer.controller.customer")
*/
class CustomerController
{
private $apiUserHelper;
private $customerService;

public function __construct(
ApiUserHelper $apiUserHelper,
CustomerService $customerService
) {
$this->apiUserHelper = $apiUserHelper;
$this->customerService = $customerService;
}

/**
* @Method({"GET"})
* @Route("")
* @Security("has_role('ROLE_INTERNAL_USER') or has_role('ROLE_API_USER')")
*
* @return Response
*/
public function getAllAction()
{
$result = $this->customerService->getAll();

return new Response(json_encode(['user' => $this->getUser(), 'result' => $result]));
}

private function getUser()
{
$user = $this->apiUserHelper->get();
$user = [
'roles' => $user->getRoles(),
];

return $user;
}
}

services:
customer.controller.customer:
class: CustomerBundle\Controller\CustomerController
arguments:
- "@customer.security.api_user_helper"
- "@customer.service.customer"

Entityler


User


namespace CustomerBundle\Entity;

use Doctrine\ORM\Mapping as ORM;
use Serializable;
use Symfony\Component\Security\Core\User\UserInterface;

/**
* @ORM\Entity(repositoryClass="CustomerBundle\Repository\UserRepository")
* @ORM\Table(
* name="user",
* uniqueConstraints={
* @ORM\UniqueConstraint(name="unq_username", columns={"username"}),
* @ORM\UniqueConstraint(name="unq_email", columns={"email"})
* }
* )
*/
class User implements UserInterface, Serializable
{
/**
* @ORM\Column(type="integer")
* @ORM\Id
* @ORM\GeneratedValue(strategy="AUTO")
*/
private $id;

/**
* @ORM\Column(name="username", type="string", length=100)
*/
private $username;

/**
* @ORM\Column(name="password", type="string", length=100)
*/
private $password;

/**
* @ORM\Column(name="email", type="string", length=100)
*/
private $email;

/**
* @ORM\Column(name="roles", type="array")
*/
private $roles;

/**
* @ORM\Column(name="is_active", type="boolean")
*/
private $isActive = true;

/**
* @return integer
*/
public function getId()
{
return $this->id;
}

public function getUsername()
{
return $this->username;
}

public function setUsername($username)
{
$this->username = $username;

return $this;
}

public function getPassword()
{
return $this->password;
}

public function setPassword($password)
{
$this->password = $password;

return $this;
}

public function getEmail()
{
return $this->email;
}

public function setEmail($email)
{
$this->email = $email;

return $this;
}

public function getIsActive()
{
return $this->isActive;
}

public function setIsActive($isActive)
{
$this->isActive = $isActive;

return $this;
}

public function getRoles()
{
return $this->roles;
}

public function setRoles(array $roles)
{
return $this->roles = $roles;
}

public function getSalt()
{
return null;
}

public function eraseCredentials()
{
}

public function serialize()
{
return serialize([$this->id, $this->username, $this->password]);
}

public function unserialize($serialized)
{
list ($this->id, $this->username, $this->password) = unserialize($serialized);
}
}

ApiUser


namespace CustomerBundle\Entity;

use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Security\Core\User\UserInterface;

/**
* @ORM\Entity(repositoryClass="CustomerBundle\Repository\ApiUserRepository")
* @ORM\Table(
* name="api_user",
* uniqueConstraints={
* @ORM\UniqueConstraint(name="unq_api_key", columns={"api_key"})
* }
* )
*/
class ApiUser implements UserInterface
{
/**
* @ORM\Id
* @ORM\GeneratedValue(strategy="AUTO")
* @ORM\Column(name="id", type="integer")
*/
private $id;

/**
* @var string
*
* @ORM\Column(name="api_key", type="string", length=36)
*/
private $apiKey;

/**
* @var array
*
* @ORM\Column(name="roles", type="array")
*/
private $roles;

public function __construct($apiKey, array $roles)
{
$this->apiKey = $apiKey;
$this->roles = $roles;
}

public function getId()
{
return $this->id;
}

public function getApiKey()
{
return $this->apiKey;
}

public function getRoles()
{
return $this->roles;
}

public function getUsername()
{
}

public function getPassword()
{
}

public function getSalt()
{
}

public function eraseCredentials()
{
}
}

Repositoryler


UserRepository


namespace CustomerBundle\Repository;

use Doctrine\ORM\EntityRepository;

class UserRepository extends EntityRepository
{
}

services:

customer.repository.user:
class: CustomerBundle\Repository\UserRepository
factory: ["@doctrine.orm.entity_manager", "getRepository"]
arguments:
- CustomerBundle\Entity\User

API'ye gönderilen her istek için aşağıdaki sorgu çalıştırılır. Eğer sadece username alanı değilde, onun haricinde farklı alanlar da kullanmak isterseniz, kendi sorgunuzu oluşturmanız gerekecektir. Daha fazla bilgi için Using a Custom Query to Load the User adresini ziyaret edin..


SELECT id, username, password, email, roles, is_active FROM user WHERE username = ? LIMIT 1 ["admin"] []

ApiUserRepository


namespace CustomerBundle\Repository;

use Doctrine\ORM\EntityRepository;
use Doctrine\ORM\Query;

class ApiUserRepository extends EntityRepository
{
public function findOneByApiKey($apiKey)
{
return $this->createQueryBuilder('a')
->where('a.apiKey = :apiKey')
->setParameter('apiKey', $apiKey)
->getQuery()
->setHint(Query::HINT_FORCE_PARTIAL_LOAD, true)
->getOneOrNullResult(Query::HYDRATE_SIMPLEOBJECT);
}
}

services:

customer.repository.api_user:
class: CustomerBundle\Repository\ApiUserRepository
factory: [ "@doctrine.orm.entity_manager", getRepository ]
arguments:
- CustomerBundle\Entity\ApiUser

ApiUserHelper


namespace CustomerBundle\Security;

use CustomerBundle\Entity\ApiUser;
use CustomerBundle\Exception\ApiException;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
use Symfony\Component\Security\Core\User\UserInterface;

class ApiUserHelper
{
private $tokenStorage;

public function __construct(
TokenStorageInterface $tokenStorage
) {
$this->tokenStorage = $tokenStorage;
}

/**
* @return UserInterface|ApiUser
*/
public function get()
{
$user = $this->tokenStorage->getToken()->getUser();
if (!$user instanceof UserInterface) {
throw new ApiException('API user not found.');
}

return $user;
}
}

services:

customer.security.api_user_helper:
class: CustomerBundle\Security\ApiUserHelper
arguments:
- "@security.token_storage"

ApiException


namespace CustomerBundle\Exception;

use RuntimeException;

class ApiException extends RuntimeException
{
}

CreateUserCommand


Bu sadece "Basic" kullanıcılar içindir.


namespace CustomerBundle\Command;

use CustomerBundle\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('customer:create-user')
->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);
}
}

services:

customer.command.create_user:
class: CustomerBundle\Command\CreateUserCommand
tags:
- { name: console.command }
arguments:
- "@doctrine.orm.entity_manager"
- "@security.password_encoder"

İki tane örnek "Basic" kullanıcı yaratalım.


$ php bin/console customer:create-user --username="basic1" --email="basic1@domain.com" --roles="ROLE_USER" --roles="ROLE_INTERNAL_USER"

CREDENTIALS
-----------
Username: basic1
Password: 762ba21d2966c9a14a9ba50593df0013

$ php bin/console customer:create-user --username="basic2" --email="basic2@domain.com" --roles="ROLE_USER" --roles="ROLE_INTERNAL_USER" --roles="ROLE_ADMIN"

CREDENTIALS
-----------
Username: basic2
Password: 401466926aa4cd3be43a10c580d260cb

Security classları


Bu sadece "API Keys" kullanıcılar içindir.


ApiKeyAuthenticator


namespace CustomerBundle\Security;

use CustomerBundle\Entity\ApiUser;
use InvalidArgumentException;
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\Exception\CustomUserMessageAuthenticationException;
use Symfony\Component\Security\Core\User\UserProviderInterface;
use Symfony\Component\Security\Http\Authentication\AuthenticationFailureHandlerInterface;
use Symfony\Component\Security\Http\Authentication\SimplePreAuthenticatorInterface;

class ApiKeyAuthenticator implements SimplePreAuthenticatorInterface, AuthenticationFailureHandlerInterface
{
public function createToken(Request $request, $providerKey)
{
$apiKey = $request->headers->get('x-auth-token');
if (!$apiKey) {
// Use this to ensure that authentication will fallback to following firewall in security.yml
// In this case, the following firewall is "http_basic"
return;

// Use this only if there is only one firewall for given "pattern" in security.yml
//throw new BadCredentialsException();
}

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. The "%s" was given.',
get_class($userProvider)
)
);
}

$user = $userProvider->loadUserByUsername($token->getCredentials());
if (!$user instanceof ApiUser) {
throw new CustomUserMessageAuthenticationException();
}

return new PreAuthenticatedToken($user, $user->getApiKey(), $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;
}
}

ApiKeyUserProvider


namespace CustomerBundle\Security;

use CustomerBundle\Repository\ApiUserRepository;
use Symfony\Component\Security\Core\Exception\UnsupportedUserException;
use Symfony\Component\Security\Core\User\User;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Security\Core\User\UserProviderInterface;

class ApiKeyUserProvider implements UserProviderInterface
{
private $apiUserRepository;

public function __construct(ApiUserRepository $apiUserRepository)
{
$this->apiUserRepository = $apiUserRepository;
}

public function loadUserByUsername($apiKey)
{
return $this->apiUserRepository->findOneByApiKey($apiKey);
}

public function refreshUser(UserInterface $user)
{
throw new UnsupportedUserException();
}

public function supportsClass($class)
{
return User::class === $class;
}
}

securities.yml


services:
customer.security.api_key_authenticator:
class: CustomerBundle\Security\ApiKeyAuthenticator

customer.security.api_key_user_provider:
class: CustomerBundle\Security\ApiKeyUserProvider
arguments:
- "@customer.repository.api_user"

Veritabanı


User


Kullanıcıları yukarıdaki komut ile yaratmıştık.


CREATE TABLE user (
id INTEGER NOT NULL,
username VARCHAR (100) NOT NULL,
password VARCHAR (100) NOT NULL,
email VARCHAR (100) NOT NULL,
roles CLOB NOT NULL,
is_active BOOLEAN NOT NULL,
PRIMARY KEY (
id
)
);

sqlite> SELECT * FROM user;

1|basic1|$2y$12$.iNsKbo24APWC3ZMD2tk7edx2JjYZN65w3VyhQNgCn8jzOVWfBD.O|basic1@domain.com|a:2:{i:0;s:9:"ROLE_USER";i:1;s:18:"ROLE_INTERNAL_USER";}|1
2|basic2|$2y$12$kK/LWy7ihK66OGUdaRtEmeLZTrVig/6a5o/Q3dT/AHiQtWqlYG6uq|basic2@domain.com|a:3:{i:0;s:9:"ROLE_USER";i:1;s:18:"ROLE_INTERNAL_USER";i:2;s:10:"ROLE_ADMIN";}|1

ApiUser


Kullanıcıları isterseniz benim yaptığım gibi menuel olarak, isterseniz de komut yazarak yaratabilirsiniz. Aşağıda'da gördüğümüz gibi api_key alanı "UUID" tipi veri tutuyor.


CREATE TABLE api_user (
id INTEGER NOT NULL,
api_key VARCHAR (36) NOT NULL,
roles CLOB NOT NULL,
PRIMARY KEY (
id
)
);

sqlite> SELECT * FROM api_user;

1|12345-449c-4e49-87ff-82db2da75715|a:2:{i:0;s:9:"ROLE_USER";i:1;s:13:"ROLE_API_USER";}
2|123-449c-4e49-87ff-82db2da75715|a:3:{i:0;s:9:"ROLE_USER";i:1;s:13:"ROLE_API_USER";i:2;s:15:"ROLE_ADMIN_USER";}

Testler


Basic


Aşağıdaki Basic authentication kodunu YmFzaWMxOjc2MmJhMjFkMjk2NmM5YTE0YTliYTUwNTkzZGYwMDEz yaratmak için "basic1" kullanıcısının username basic1 ve password 762ba21d2966c9a14a9ba50593df0013 bilgilerini kullandım. Eğer Basic Authentication Header Generator yaratmak isterseniz, Internet'te bulunan servisleri kullanabilirsiniz.


$ curl -I -X GET \
http://192.168.99.10:8081/app_test.php/v1/customers \
-H 'Authorization: Basic YmFzaWMxOjc2MmJhMjFkMjk2NmM5YTE0YTliYTUwNTkzZGYwMDEz'

ApiKey


Aşağıda'da gördüğümüz gibi x-auth-token ikinci api user'in bilgisini kullanıyor.


$ curl -I -X GET \
http://192.168.99.10:8081/app_test.php/v1/customers \
-H 'x-auth-token: 12345-449c-4e49-87ff-82db2da75715'