APIプラットフォームとJWT認証を使ってSymfony4でユーザー管理する方法

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認証の設定は公式マニュアルをご確認下さい。

https://github.com/lexik/LexikJWTAuthenticationBundle/blob/master/Resources/doc/index.md#installation

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>

 

お気軽にコメントをどうぞ

このサイトはスパムを低減するために Akismet を使っています。コメントデータの処理方法の詳細はこちらをご覧ください