APIプラットフォームとJWT認証を使ってSymfony4でユーザー管理する方法を調べたのでその備忘録です。
まだAPIプラットフォームのすべてを理解していないので誤りがありましたらご了承下さい。
各種パッケージのインストール
以下のパッケージをcomposerでインストールして下さい。
composer require api composer require lexik/jwt-authentication-bundle composer require symfony/security composer require symfony/maker-bundle --dev
JWT認証の設定
JWT認証の設定は公式マニュアルをご確認下さい。
Userエンティティ作成
以下のコマンドでUserエンティティを作成して下さい。
bin/console make:entity
Userエンティティが出来たら以下のように修正して下さい。
<?php
namespace App\Entity;
use ApiPlatform\Core\Annotation\ApiResource;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity;
use Symfony\Component\Validator\Constraints as Assert;
use Symfony\Component\Serializer\Annotation\Groups;
/**
* API利用条件:
* ・ユーザー一覧取得は不可
* ・登録は誰でも可能
* ・ログイン時は自分のユーザー情報閲覧可能
* ・ログイン時は自分のユーザー情報更新可能
* ・ログイン時は自分のユーザー情報削除可能
*
* ・idは閲覧可能
* ・emailは閲覧・書き込み可能
* ・passwordは書き込み可能
* @ApiResource(
* collectionOperations={
* "post"
* },
* itemOperations={
* "get"={"access_control"="is_granted('ROLE_USER') and object == user"},
* "put"={"access_control"="is_granted('ROLE_USER') and object == user"},
* "delete"={"access_control"="is_granted('ROLE_USER') and object == user"}
* },
* normalizationContext={
* "groups"={"read"}
* },
* denormalizationContext={
* "groups"={"write"}
* }
* )
* @ORM\Entity(repositoryClass="App\Repository\UserRepository")
* @UniqueEntity("email")
*/
class User implements UserInterface
{
/**
* @ORM\Id()
* @ORM\GeneratedValue()
* @ORM\Column(type="integer")
* @Groups({"read"})
*/
private $id;
/**
* @ORM\Column(type="string", length=180, unique=true)
* @Assert\NotBlank()
* @Assert\Email()
* @Assert\Length(min=6, max=180)
* @Groups({"read", "write"})
*/
private $email;
/**
* @ORM\Column(type="json")
*/
private $roles = [];
/**
* @var string ハッシュ化されたパスワード
* @ORM\Column(type="string")
* @Assert\NotBlank()
* @Groups({"write"})
*/
private $password;
public function __construct()
{
$this->products = new ArrayCollection();
}
public function getId(): ?int
{
return $this->id;
}
public function getEmail(): ?string
{
return $this->email;
}
public function setEmail(string $email): self
{
$this->email = $email;
return $this;
}
/**
* A visual identifier that represents this user.
*
* @see UserInterface
*/
public function getUsername(): string
{
return (string) $this->email;
}
/**
* @see UserInterface
*/
public function getRoles(): array
{
$roles = $this->roles;
// guarantee every user at least has ROLE_USER
$roles[] = 'ROLE_USER';
return array_unique($roles);
}
public function setRoles(array $roles): self
{
$this->roles = $roles;
return $this;
}
/**
* @see UserInterface
*/
public function getPassword(): string
{
return (string) $this->password;
}
public function setPassword(string $password): self
{
$this->password = $password;
return $this;
}
/**
* @see UserInterface
*/
public function getSalt()
{
// not needed when using the "bcrypt" algorithm in security.yaml
}
/**
* @see UserInterface
*/
public function eraseCredentials()
{
// If you store any temporary, sensitive data on the user, clear it here
// $this->plainPassword = null;
}
}
@ApiResourceを指定することでユーザーのREST APIが利用できるようになります。
@ApiResourceではアクセス権限、読み込み・書き込み権限などを設定しています。
セキュリティ設定
以下のようにセキュリティ設定をして下さい。
security:
encoders:
App\Entity\User:
algorithm: bcrypt
cost: 12
# https://symfony.com/doc/current/security.html#where-do-users-come-from-user-providers
providers:
# used to reload user from session & other features (e.g. switch_user)
app_user_provider:
entity:
class: App\Entity\User
property: email
firewalls:
dev:
pattern: ^/(_(profiler|wdt)|css|images|js)/
security: false
api:
stateless: true
anonymous: true
provider: app_user_provider
json_login:
check_path: /authentication_token
username_path: email
password_path: password
success_handler: lexik_jwt_authentication.handler.authentication_success
failure_handler: lexik_jwt_authentication.handler.authentication_failure
guard:
authenticators:
- lexik_jwt_authentication.jwt_token_authenticator
# activate different ways to authenticate
# https://symfony.com/doc/current/security.html#firewalls-authentication
# https://symfony.com/doc/current/security/impersonating_user.html
# switch_user: true
# Easy way to control access for large sections of your site
# Note: Only the *first* access control that matches will be used
access_control:
# - { path: ^/admin, roles: ROLE_ADMIN }
# - { path: ^/profile, roles: ROLE_USER }
# - { path: ^/authentication_token, roles: IS_AUTHENTICATED_ANONYMOUSLY }
認証URLのパス設定
/authentication_tokenで認証処理を行うので以下のようにroutes.yamlにパスを設定して下さい。
#index:
# path: /
# controller: App\Controller\DefaultController::index
authentication_token:
path: /authentication_token
methods: ['POST']
ユーザー登録時にパスワードをハッシュ化して保存
ユーザー登録時にパスワードをハッシュ化して保存するために以下のようにEventSubscriberを用意して下さい。
<?php
namespace App\EventSubscriber;
use ApiPlatform\Core\EventListener\EventPriorities;
use App\Entity\User;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\Event\ViewEvent;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
use Symfony\Component\Security\Core\Encoder\UserPasswordEncoderInterface;
class UserEntitySubscriber implements EventSubscriberInterface
{
/**
* @var TokenStorageInterface
*/
private $tokenStorage;
/**
* @var UserPasswordEncoderInterface
*/
private $passwordEncoder;
public function __construct(TokenStorageInterface $tokenStorage, UserPasswordEncoderInterface $passwordEncoder)
{
$this->tokenStorage = $tokenStorage;
$this->passwordEncoder = $passwordEncoder;
}
/**
* 新規ユーザー登録時にパスワードをハッシュ化します
* ※ViewEventはSymfony4.3以上で動作します
*
* @param ViewEvent $event
*/
public function onKernelView(ViewEvent $event)
{
$entity = $event->getControllerResult();
$method = $event->getRequest()->getMethod();
$contentType = $event->getRequest()->getContentType();
if(Request::METHOD_POST !== $method || "json" !== $contentType) {
return;
}
if($entity instanceof User) {
$entity->setPassword($this->passwordEncoder->encodePassword($entity, $entity->getPassword()));
}
}
public static function getSubscribedEvents()
{
return [
'kernel.view' => ['onKernelView', EventPriorities::PRE_VALIDATE],
];
}
}
以上で完成です。
APIの使い方
ユーザー登録
curl -X POST -H "Content-Type: application/json" http://localhost:8000/api/users -d '{"email":"info@a-zumi.net","password":"password"}'
ログイン
curl -X POST -H "Content-Type: application/json" http://localhost:8000/authentication_token -d '{"email":"info@a-zumi.net","password":"password"}'
認証に成功すると以下のようなトークンが帰ってきます。
{
"token" : "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXUyJ9.eyJleHAiOjE0MzQ3Mjc1MzYsInVzZXJuYW1lIjoia29ybGVvbiIsImlhdCI6IjE0MzQ2NDExMzYifQ.nh0L_wuJy6ZKIQWh6OrW5hdLkviTs1_bau2GqYdDCB0Yqy_RplkFghsuqMpsFls8zKEErdX5TYCOR7muX0aQvQxGQ4mpBkvMDhJ4-pE4ct2obeMTr_s4X8nC00rBYPofrOONUOR4utbzvbd4d2xT_tj4TdR_0tsr91Y7VskCRFnoXAnNT-qQb7ci7HIBTbutb9zVStOFejrb4aLbr7Fl4byeIEYgp2Gd7gY"
}
ユーザー情報取得
curl -H "Content-Type: application/json" -H "Authorization: Bearer ****************token******************" http://localhost:8000/api/users/1
ユーザー情報更新
curl -X PUT -H "Content-Type: application/json" -H "Authorization: Bearer ****************token******************" http://localhost:8000/api/users/1
ユーザー情報削除
curl -X DELETE -H "Content-Type: application/json" -H "Authorization: Bearer ****************token******************" http://localhost:8000/api/users/1
APIの結果確認にはPostmanが便利です。
ユーザー情報が取得できない場合
JWT Token not foundとなりユーザー情報が取得できない場合は.htaccessに以下を追加してみて下さい。
<IfModule mod_rewrite.c>
RewriteEngine On
RewriteCond %{HTTP:Authorization} ^(.*)
RewriteRule .* - [e=HTTP_AUTHORIZATION:%1]
</IfModule>
