Symfony 2 - Базовый RESTful API c Symfony 2 +FOSRestBundle (только JSON формат) + FOSUserBundle + FOSOauthServer

Отправлено admin от вт, 02.08.2016, 14:53
источник:

https://gist.github.com/tjamps/11d617a4b318d65ca583

API который мы создаём имеет следующие правила:
  • API возвращает только JSON ответы
  • Все API маршруты требуют идентификации
  • Аутентификация производится через OAuth2 только с паролем и типом доступа (при этом не надо авторизоваться на страницах).
  • версии API управляются через субдомены (например v1.api.example.com)

API реализован на PHP и фреймворке Symfony 2. Используются следующие SF2 бандлы :

Установка бандлов

composer require friendsofsymfony/rest-bundle
composer require jms/serializer-bundle
composer require nelmio/api-doc-bundle
composer require friendsofsymfony/user-bundle
composer require friendsofsymfony/oauth-server-bundle
Добавьте следующие строки в файл app/AppKernel.php для включения бандлов:
  1. // app/AppKernel.php
  2. class AppKernel extends Kernel
  3. {
  4. public function registerBundles()
  5. {
  6. $bundles = array(
  7. // ...
  8. new FOS\RestBundle\FOSRestBundle(),
  9. new FOS\UserBundle\FOSUserBundle(),
  10. new FOS\OAuthServerBundle\FOSOAuthServerBundle(),
  11. new JMS\SerializerBundle\JMSSerializerBundle(),
  12. new Nelmio\ApiDocBundle\NelmioApiDocBundle(),
  13. );
  14.  
  15. // ...
  16. }
  17. }

Конфигурирование бандлов

Добавить следующее в app/config/config.yml :
# app/config/config.yml
nelmio_api_doc: ~

fos_rest:
    routing_loader:
        default_format: json                            # All responses should be JSON formated
        include_format: false                           # We do not include format in request, so that all responses
                                                        # will eventually be JSON formated
?>
fos_user:
    db_driver: orm
    firewall_name: api                                  # Seems to be used when registering user/reseting password,
                                                        # but since there is no "login", as so it seems to be useless in
                                                        # our particular context, but still required by "FOSUserBundle"
    user_class: Acme\ApiBundle\Entity\User

fos_oauth_server:
    db_driver:           orm
    client_class:        Acme\ApiBundle\Entity\Client
    access_token_class:  Acme\ApiBundle\Entity\AccessToken
    refresh_token_class: Acme\ApiBundle\Entity\RefreshToken
    auth_code_class:     Acme\ApiBundle\Entity\AuthCode
    service:
        user_provider: fos_user.user_manager             # This property will be used when valid credentials 

Безопасность


Добавьте следующее в
app/config/security.yml
# app/config/security.yml

security:
    encoders:
        FOS\UserBundle\Model\UserInterface: sha512

    providers:
        fos_userbundle:
            id: fos_user.user_provider.username        # fos_user.user_provider.username_email does not seem to work (OAuth-spec related ("username + password") ?)
    firewalls:
        oauth_token:                                   # Everyone can access the access token URL.
            pattern: ^/oauth/v2/token
            security: false
        api:
            pattern: ^/                                # All URLs are protected
            fos_oauth: true                            # OAuth2 protected resource
            stateless: true                            # Do no set session cookies
            anonymous: false                           # Anonymous access is not allowed

Вы можете добавить большее через свойство access_control

Маршрутизация


Добавьте следующее в
app/config/routing.yml

# app/config/routing.yml
NelmioApiDocBundle:
    resource: "@NelmioApiDocBundle/Resources/config/routing.yml"
    prefix:   /api/doc

fos_oauth_server_token:
    resource: "@FOSOAuthServerBundle/Resources/config/routing/token.xml"

API Бандл


Внимание:

Это не строго рекомендуемый шаг, вы можете организовать свой код так как вы пожелаете. Я здесь использую только один бандл для простоты, будьте свободны в вашем выборе так, как вам подсказывает ваше сердце ;)
Далее нам необходимо создать сущности для предоставления пользователю доступа к токенам и т.д. Давайте создадим для этой цели бандл.
php app/console generate:bundle --namespace=Acme/ApiBundle

Следующий шаг - создание сущностей.

Сущность пользователя


Эта сущность требует FosUserBundle и будет также использовать FOSOAuthServerBundle Вы можете ознакомиться с докуметацией и делать практически всё с этим классом. , но со следующими изменениями:

  • Это расширение FOS\UserBundle\Entity\User, а не FOS\UserBundle\Model\User (иначе изменения схемы доктрины не будут работать в дальнейшем)
  • Имя пользовательской таблицы: @ORM\Table("users")
  1. // src/Acme/ApiBundle/Entity/User.php
  2.  
  3. namespace Acme\ApiBundle\Entity;
  4.  
  5. use FOS\UserBundle\Entity\User as BaseUser;
  6. use Doctrine\ORM\Mapping as ORM;
  7.  
  8. /**
  9.  * User
  10.  *
  11.  * @ORM\Table("users")
  12.  * @ORM\Entity
  13.  */
  14. class User extends BaseUser
  15. {
  16. /**
  17.   * @var integer
  18.   *
  19.   * @ORM\Column(name="id", type="integer")
  20.   * @ORM\Id
  21.   * @ORM\GeneratedValue(strategy="AUTO")
  22.   */
  23. protected $id;
  24.  
  25.  
  26. /**
  27.   * Get id
  28.   *
  29.   * @return integer
  30.   */
  31. public function getId()
  32. {
  33. return $this->id;
  34. }
  35. }

Другие сущности

Эти сущности требуются для FOSOAuthServerBundle. Это делается простым копированием (copy/paste ) из документации с соответствующим согласованным неймспейс (namespace). Таблицы имён тоже требуют согласования. Так же убедитесь , что параметр целевой сущности @ORM\ManyToOne указатель на сущность пользователя вы сделали в предыдущем шаге:
  1. // src/Acme/ApiBundle/Entity/Client.php
  2.  
  3. namespace Acme\ApiBundle\Entity;
  4.  
  5. use FOS\OAuthServerBundle\Entity\Client as BaseClient;
  6. use Doctrine\ORM\Mapping as ORM;
  7.  
  8. /**
  9.  * @ORM\Table("oauth2_clients")
  10.  * @ORM\Entity
  11.  */
  12. class Client extends BaseClient
  13. {
  14. /**
  15.   * @ORM\Id
  16.   * @ORM\Column(type="integer")
  17.   * @ORM\GeneratedValue(strategy="AUTO")
  18.   */
  19. protected $id;
  20.  
  21. public function __construct()
  22. {
  23. parent::__construct();
  24. }
  25. }

  1. // src/Acme/ApiBundle/Entity/AccessToken.php
  2.  
  3. namespace Acme\ApiBundle\Entity;
  4.  
  5. use FOS\OAuthServerBundle\Entity\AccessToken as BaseAccessToken;
  6. use Doctrine\ORM\Mapping as ORM;
  7.  
  8. /**
  9.  * @ORM\Table("oauth2_access_tokens")
  10.  * @ORM\Entity
  11.  */
  12. class AccessToken extends BaseAccessToken
  13. {
  14. /**
  15.   * @ORM\Id
  16.   * @ORM\Column(type="integer")
  17.   * @ORM\GeneratedValue(strategy="AUTO")
  18.   */
  19. protected $id;
  20.  
  21. /**
  22.   * @ORM\ManyToOne(targetEntity="Client")
  23.   * @ORM\JoinColumn(nullable=false)
  24.   */
  25. protected $client;
  26.  
  27. /**
  28.   * @ORM\ManyToOne(targetEntity="User")
  29.   */
  30. protected $user;
  31. }
  1. // src/Acme/ApiBundle/Entity/RefreshToken.php
  2.  
  3. namespace Acme\ApiBundle\Entity;
  4.  
  5. use FOS\OAuthServerBundle\Entity\RefreshToken as BaseRefreshToken;
  6. use Doctrine\ORM\Mapping as ORM;
  7.  
  8. /**
  9.  * @ORM\Table("oauth2_refresh_tokens")
  10.  * @ORM\Entity
  11.  */
  12. class RefreshToken extends BaseRefreshToken
  13. {
  14. /**
  15.   * @ORM\Id
  16.   * @ORM\Column(type="integer")
  17.   * @ORM\GeneratedValue(strategy="AUTO")
  18.   */
  19. protected $id;
  20.  
  21. /**
  22.   * @ORM\ManyToOne(targetEntity="Client")
  23.   * @ORM\JoinColumn(nullable=false)
  24.   */
  25. protected $client;
  26.  
  27. /**
  28.   * @ORM\ManyToOne(targetEntity="User")
  29.   */
  30. protected $user;
  31. }
  1. // src/Acme/ApiBundle/Entity/AuthCode.php
  2.  
  3. namespace Acme\ApiBundle\Entity;
  4.  
  5. use FOS\OAuthServerBundle\Entity\AuthCode as BaseAuthCode;
  6. use Doctrine\ORM\Mapping as ORM;
  7.  
  8. /**
  9.  * @ORM\Table("oauth2_auth_codes")
  10.  * @ORM\Entity
  11.  */
  12. class AuthCode extends BaseAuthCode
  13. {
  14. /**
  15.   * @ORM\Id
  16.   * @ORM\Column(type="integer")
  17.   * @ORM\GeneratedValue(strategy="AUTO")
  18.   */
  19. protected $id;
  20.  
  21. /**
  22.   * @ORM\ManyToOne(targetEntity="Client")
  23.   * @ORM\JoinColumn(nullable=false)
  24.   */
  25. protected $client;
  26.  
  27. /**
  28.   * @ORM\ManyToOne(targetEntity="User")
  29.   */
  30. protected $user;
  31. }
Сейчас вы можете обновить вашу схему базы данных:
php app/console doctrine:schema:update --force
В результате вы получите следующие созданные таблицы (пример для PostgreSQL):
users;
CREATE TABLE users
(
  id integer NOT NULL,
  username character varying(255) NOT NULL,
  username_canonical character varying(255) NOT NULL,
  email character varying(255) NOT NULL,
  email_canonical character varying(255) NOT NULL,
  enabled boolean NOT NULL,
  salt character varying(255) NOT NULL,
  password character varying(255) NOT NULL,
  last_login timestamp(0) without time zone DEFAULT NULL::timestamp without time zone,
  locked boolean NOT NULL,
  expired boolean NOT NULL,
  expires_at timestamp(0) without time zone DEFAULT NULL::timestamp without time zone,
  confirmation_token character varying(255) DEFAULT NULL::character varying,
  password_requested_at timestamp(0) without time zone DEFAULT NULL::timestamp without time zone,
  roles text NOT NULL, -- (DC2Type:array)
  credentials_expired boolean NOT NULL,
  credentials_expire_at timestamp(0) without time zone DEFAULT NULL::timestamp without time zone,
  CONSTRAINT users_pkey PRIMARY KEY (id)
) 
oauth2_clients;
 CREATE TABLE oauth2_clients
(
  id integer NOT NULL,
  random_id character varying(255) NOT NULL,
  redirect_uris text NOT NULL, -- (DC2Type:array)
  secret character varying(255) NOT NULL,
  allowed_grant_types text NOT NULL, -- (DC2Type:array)
  CONSTRAINT oauth2_clients_pkey PRIMARY KEY (id)
)
 
oauth2_access_tokens;
CREATE TABLE oauth2_access_tokens
(
  id integer NOT NULL,
  client_id integer NOT NULL,
  user_id integer,
  token character varying(255) NOT NULL,
  expires_at integer,
  scope character varying(255) DEFAULT NULL::character varying,
  CONSTRAINT oauth2_access_tokens_pkey PRIMARY KEY (id),
  CONSTRAINT fk_d247a21b19eb6921 FOREIGN KEY (client_id)
      REFERENCES oauth2_clients (id) MATCH SIMPLE
      ON UPDATE NO ACTION ON DELETE NO ACTION,
  CONSTRAINT fk_d247a21ba76ed395 FOREIGN KEY (user_id)
      REFERENCES users (id) MATCH SIMPLE
      ON UPDATE NO ACTION ON DELETE NO ACTION
)
oauth2_auth_codes;
CREATE TABLE oauth2_auth_codes
(
  id integer NOT NULL,
  client_id integer NOT NULL,
  user_id integer,
  token character varying(255) NOT NULL,
  redirect_uri text NOT NULL,
  expires_at integer,
  scope character varying(255) DEFAULT NULL::character varying,
  CONSTRAINT oauth2_auth_codes_pkey PRIMARY KEY (id),
  CONSTRAINT fk_a018a10d19eb6921 FOREIGN KEY (client_id)
      REFERENCES oauth2_clients (id) MATCH SIMPLE
      ON UPDATE NO ACTION ON DELETE NO ACTION,
  CONSTRAINT fk_a018a10da76ed395 FOREIGN KEY (user_id)
      REFERENCES users (id) MATCH SIMPLE
      ON UPDATE NO ACTION ON DELETE NO ACTION
)
oauth2_refresh_tokens;
CREATE TABLE oauth2_refresh_tokens
(
  id integer NOT NULL,
  client_id integer NOT NULL,
  user_id integer,
  token character varying(255) NOT NULL,
  expires_at integer,
  scope character varying(255) DEFAULT NULL::character varying,
  CONSTRAINT oauth2_refresh_tokens_pkey PRIMARY KEY (id),
  CONSTRAINT fk_d394478c19eb6921 FOREIGN KEY (client_id)
      REFERENCES oauth2_clients (id) MATCH SIMPLE
      ON UPDATE NO ACTION ON DELETE NO ACTION,
  CONSTRAINT fk_d394478ca76ed395 FOREIGN KEY (user_id)
      REFERENCES users (id) MATCH SIMPLE
      ON UPDATE NO ACTION ON DELETE NO ACTION
)

Добавление Oauth2 клиента

Следующий шаг состоит из добавления Oauth2 клиента. Документация не полностью раскрывает этот шаг - Следующий код может быть вставлен в команду создания нового клиента В нашем случае нам нужен только один клиент, так что мы можем добавить клиент выполнив просто SQL - запрос:
INSERT INTO `oauth2_clients` VALUES (NULL, '3bcbxd9e24g0gk4swg0kwgcwg4o8k8g4g888kwc44gcc0gwwk4', 'a:0:{}', '4ok2x70rlfokc8g0wws8c8kwcokw80k44sg48goc0ok4w0so0k', 'a:1:{i:0;s:8:"password";}');

Создание admin пользователя:
$ php app/console fos:user:create
Please choose a username:admin
Please choose an email:admin@example.com
Please choose a password:admin
Created user admin

Создание REST контроллера


Сейчас мы можем создать REST контроллер для предоставления очень простого ресурса. Таким образом, мы должным образом можем пробовать нашу инсталляцию.
  1. // src/Acme/ApiBundle/Controller/DemoController.php
  2.  
  3. namespace Acme\ApiBundle\Controller;
  4.  
  5. use FOS\RestBundle\Controller\FOSRestController;
  6.  
  7. class DemoController extends FOSRestController
  8. {
  9. public function getDemosAction()
  10. {
  11. $data = array("hello" => "world");
  12. $view = $this->view($data);
  13. return $this->handleView($view);
  14. }
  15. }

Проверка работы Oauth2


$ http GET http://localhost:8000/app_dev.php/links
HTTP/1.1 401 Unauthorized
Cache-Control: no-store, private
Connection: close
Content-Type: application/json
...

{
    "error": "access_denied",
    "error_description": "OAuth2 authentication required"
}

Здесь мы не получили доступа :( Сейчас, используя клиента и пользователя , созданных ранее, мы будем делать запрос для получения Токена Доступа (Access Token ). Внимание, параметр client_id - это конкатенация знака нижнего подчёркивания и id клиента:
$ http POST http://localhost:8000/app_dev.php/oauth/v2/token \
    grant_type=password \
    client_id=1_3bcbxd9e24g0gk4swg0kwgcwg4o8k8g4g888kwc44gcc0gwwk4 \
    client_secret=4ok2x70rlfokc8g0wws8c8kwcokw80k44sg48goc0ok4w0so0k \
    username=admin \
    password=admin
HTTP/1.1 200 OK
Cache-Control: no-store, private
Connection: close
Content-Type: application/json
...

{
    "access_token": "MDFjZGI1MTg4MTk3YmEwOWJmMzA4NmRiMTgxNTM0ZDc1MGI3NDgzYjIwNmI3NGQ0NGE0YTQ5YTVhNmNlNDZhZQ",
    "expires_in": 3600,
    "refresh_token": "ZjYyOWY5Yzg3MTg0MDU4NWJhYzIwZWI4MDQzZTg4NWJjYzEyNzAwODUwYmQ4NjlhMDE3OGY4ZDk4N2U5OGU2Ng",
    "scope": null,
    "token_type": "bearer"
}

Полученный токен мы просто берём и используем в нашем следующем запросе:
 $ http GET http://ledzep.dev:8000/app_dev.php/links \
    "Authorization:Bearer MDFjZGI1MTg4MTk3YmEwOWJmMzA4NmRiMTgxNTM0ZDc1MGI3NDgzYjIwNmI3NGQ0NGE0YTQ5YTVhNmNlNDZhZQ"
HTTP/1.1 200 OK
Cache-Control: no-cache
Connection: close
Content-Type: application/json
...

{
    "hello": "world"
}
 

Далее, мы можем работать с пользователем

Информация пользователя


Взать информацию текущего пользователя
  1. use use Symfony\Component\Security\Core\Exception\AccessDeniedException;
  2.  
  3. // ...
  4. class DemoController extends FOSRestController
  5. {
  6. // ...
  7. public function getDemosAction()
  8. {
  9. $user = $this->get('security.context')->getToken()->getUser();
  10.  
  11. //...
  12. // Do something with the fully authenticated user.
  13. // ...
  14. }
  15. // ...
  16. }

Проверить существование конкретного доступа пользователя (user grants)

  1. use use Symfony\Component\Security\Core\Exception\AccessDeniedException;
  2.  
  3. // ...
  4. class DemoController extends FOSRestController
  5. {
  6. // ...
  7. public function getDemosAction()
  8. {
  9. if ($this->get('security.context')->isGranted('ROLE_JCVD') === FALSE) {
  10. throw new AccessDeniedException();
  11. }
  12.  
  13. // ...
  14. }
  15. // ...
  16. }
Теги