источник:
API реализован на PHP и фреймворке Symfony 2. Используются следующие SF2 бандлы :
Добавьте следующее в
Вы можете добавить большее через свойство access_control
Добавьте следующее в
Далее нам необходимо создать сущности для предоставления пользователю доступа к токенам и т.д. Давайте создадим для этой цели бандл.
Следующий шаг - создание сущностей.
Эта сущность требует FosUserBundle и будет также использовать FOSOAuthServerBundle Вы можете ознакомиться с докуметацией и делать практически всё с этим классом. , но со следующими изменениями:
Сейчас вы можете обновить вашу схему базы данных:
users;
Создание admin пользователя:
Сейчас мы можем создать REST контроллер для предоставления очень простого ресурса. Таким образом, мы должным образом можем пробовать нашу инсталляцию.
Здесь мы не получили доступа :( Сейчас, используя клиента и пользователя , созданных ранее, мы будем делать запрос для получения Токена Доступа (Access Token ). Внимание, параметр client_id - это конкатенация знака нижнего подчёркивания и id клиента:
Полученный токен мы просто берём и используем в нашем следующем запросе:
Далее, мы можем работать с пользователем
https://gist.github.com/tjamps/11d617a4b318d65ca583
API который мы создаём имеет следующие правила:- API возвращает только JSON ответы
- Все API маршруты требуют идентификации
- Аутентификация производится через OAuth2 только с паролем и типом доступа (при этом не надо авторизоваться на страницах).
- версии API управляются через субдомены (например v1.api.example.com)
API реализован на PHP и фреймворке Symfony 2. Используются следующие SF2 бандлы :
- https://github.com/FriendsOfSymfony/FOSRestBundle
- https://github.com/FriendsOfSymfony/FOSUserBundle
- https://github.com/FriendsOfSymfony/FOSOAuthServerBundle
- https://github.com/schmittjoh/JMSSerializerBundle
- https: //github.com/nelmio/NelmioApiDocBundle
Установка бандлов
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 для включения бандлов:
// app/AppKernel.php class AppKernel extends Kernel { public function registerBundles() { // ... new FOS\RestBundle\FOSRestBundle(), new FOS\UserBundle\FOSUserBundle(), new FOS\OAuthServerBundle\FOSOAuthServerBundle(), new JMS\SerializerBundle\JMSSerializerBundle(), new Nelmio\ApiDocBundle\NelmioApiDocBundle(), ); // ... } }
Конфигурирование бандлов
Добавить следующее в 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")
// src/Acme/ApiBundle/Entity/User.php namespace Acme\ApiBundle\Entity; use FOS\UserBundle\Entity\User as BaseUser; use Doctrine\ORM\Mapping as ORM; /** * User * * @ORM\Table("users") * @ORM\Entity */ class User extends BaseUser { /** * @var integer * * @ORM\Column(name="id", type="integer") * @ORM\Id * @ORM\GeneratedValue(strategy="AUTO") */ protected $id; /** * Get id * * @return integer */ public function getId() { return $this->id; } }
Другие сущности
Эти сущности требуются для FOSOAuthServerBundle. Это делается простым копированием (copy/paste ) из документации с соответствующим согласованным неймспейс (namespace). Таблицы имён тоже требуют согласования. Так же убедитесь , что параметр целевой сущности @ORM\ManyToOne указатель на сущность пользователя вы сделали в предыдущем шаге:
// src/Acme/ApiBundle/Entity/Client.php namespace Acme\ApiBundle\Entity; use FOS\OAuthServerBundle\Entity\Client as BaseClient; use Doctrine\ORM\Mapping as ORM; /** * @ORM\Table("oauth2_clients") * @ORM\Entity */ class Client extends BaseClient { /** * @ORM\Id * @ORM\Column(type="integer") * @ORM\GeneratedValue(strategy="AUTO") */ protected $id; public function __construct() { parent::__construct(); } }
// src/Acme/ApiBundle/Entity/AccessToken.php namespace Acme\ApiBundle\Entity; use FOS\OAuthServerBundle\Entity\AccessToken as BaseAccessToken; use Doctrine\ORM\Mapping as ORM; /** * @ORM\Table("oauth2_access_tokens") * @ORM\Entity */ class AccessToken extends BaseAccessToken { /** * @ORM\Id * @ORM\Column(type="integer") * @ORM\GeneratedValue(strategy="AUTO") */ protected $id; /** * @ORM\ManyToOne(targetEntity="Client") * @ORM\JoinColumn(nullable=false) */ protected $client; /** * @ORM\ManyToOne(targetEntity="User") */ protected $user; }
// src/Acme/ApiBundle/Entity/RefreshToken.php namespace Acme\ApiBundle\Entity; use FOS\OAuthServerBundle\Entity\RefreshToken as BaseRefreshToken; use Doctrine\ORM\Mapping as ORM; /** * @ORM\Table("oauth2_refresh_tokens") * @ORM\Entity */ class RefreshToken extends BaseRefreshToken { /** * @ORM\Id * @ORM\Column(type="integer") * @ORM\GeneratedValue(strategy="AUTO") */ protected $id; /** * @ORM\ManyToOne(targetEntity="Client") * @ORM\JoinColumn(nullable=false) */ protected $client; /** * @ORM\ManyToOne(targetEntity="User") */ protected $user; }
// src/Acme/ApiBundle/Entity/AuthCode.php namespace Acme\ApiBundle\Entity; use FOS\OAuthServerBundle\Entity\AuthCode as BaseAuthCode; use Doctrine\ORM\Mapping as ORM; /** * @ORM\Table("oauth2_auth_codes") * @ORM\Entity */ class AuthCode extends BaseAuthCode { /** * @ORM\Id * @ORM\Column(type="integer") * @ORM\GeneratedValue(strategy="AUTO") */ protected $id; /** * @ORM\ManyToOne(targetEntity="Client") * @ORM\JoinColumn(nullable=false) */ protected $client; /** * @ORM\ManyToOne(targetEntity="User") */ protected $user; }
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 контроллер для предоставления очень простого ресурса. Таким образом, мы должным образом можем пробовать нашу инсталляцию.
// src/Acme/ApiBundle/Controller/DemoController.php namespace Acme\ApiBundle\Controller; use FOS\RestBundle\Controller\FOSRestController; class DemoController extends FOSRestController { public function getDemosAction() { $view = $this->view($data); return $this->handleView($view); } }
Проверка работы 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" }
Далее, мы можем работать с пользователем
Информация пользователя
Взать информацию текущего пользователя
use use Symfony\Component\Security\Core\Exception\AccessDeniedException; // ... class DemoController extends FOSRestController { // ... public function getDemosAction() { $user = $this->get('security.context')->getToken()->getUser(); //... // Do something with the fully authenticated user. // ... } // ... }
Проверить существование конкретного доступа пользователя (user grants)
use use Symfony\Component\Security\Core\Exception\AccessDeniedException; // ... class DemoController extends FOSRestController { // ... public function getDemosAction() { if ($this->get('security.context')->isGranted('ROLE_JCVD') === FALSE) { throw new AccessDeniedException(); } // ... } // ... }
- Войдите или зарегистрируйтесь, чтобы оставлять комментарии