lexik/jwt-authentication-bundle を利用して API のセキュリティを JWT で構成します。
- Symfony のバージョンは 5.3 を使っています。(5.2 以前とは設定が一部違うようです)
- MakerBundle が必要です。
jwt-authentication-bundle の導入
Getting started の手順に従って導入を進めます。
composer.phar require lexik/jwt-authentication-bundle
上記を実行すると依存関係を解決して、Symfony Security Bundle も導入されます。
flex のおかげで、bundles.php への追加や、各種設定ファイルの更新なども行ってくれます。
SSL キーの生成
次のコマンドを実行します。
php bin/console lexik:jwt:generate-keypair
.env ファイルに次の行が追加されます
###> lexik/jwt-authentication-bundle ###
JWT_SECRET_KEY=%kernel.project_dir%/config/jwt/private.pem
JWT_PUBLIC_KEY=%kernel.project_dir%/config/jwt/public.pem
JWT_PASSPHRASE=****************
###< lexik/jwt-authentication-bundle ###
config/packages/lexik_jwt_authentication.yaml が作成されます。
lexik_jwt_authentication:
secret_key: '%env(resolve:JWT_SECRET_KEY)%'
public_key: '%env(resolve:JWT_PUBLIC_KEY)%'
pass_phrase: '%env(JWT_PASSPHRASE)%'
これらの設定ファイルが更新された状態から、後ほど設定を変更して行きます。
ユーザー情報
jwt-authentication-bundle の設定を進める前に、ユーザー情報の作成をしておきます。
ここでは一般的な、 データベースに保存されたユーザー情報を使う Entity プロバイダーを設定します。
ログインユーザー情報を作成します。
ユーザー Entity の作成
プロジェクトのルートで次のコマンドを実行し、ユーザー Entity を作成します。
php bin/console make:user
実行するとプロンプトが表示されますので、質問に答えます。
The name of the security user class (e.g. User) [User]:
> User
Do you want to store user data in the database (via Doctrine)? (yes/no) [yes]:
> yes
Enter a property name that will be the unique "display" name for the user (e.g. email, username, uuid) [email]:
> username
Will this app need to hash/check user passwords? Choose No if passwords are not needed or will be checked/hashed by some other system (e.g. a single sign-on server).
Does this app need to hash/check user passwords? (yes/no) [yes]:
> yes
created: src/Entity/User.php
created: src/Repository/UserRepository.php
updated: src/Entity/User.php
updated: config/packages/security.yaml
User エンティティとリポジトリが作成されます。
同時に、security.yaml も更新されます。
テーブルの作成
bin/console doctrine:schema:update --force
もちろん migration を使って作成しても構いません。
ユーザー情報の登録
ユーザー情報のレコードを登録します。
php bin/console make:fixtures UserFixtures
src/DataFixtures/UserFixtures.php が作成されます。
その内容を次のように設定します。
<?php
namespace App\DataFixtures;
use App\Entity\User;
use Doctrine\Bundle\FixturesBundle\Fixture;
use Doctrine\Persistence\ObjectManager;
use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface;
class UserFixtures extends Fixture
{
public function __construct(
private UserPasswordHasherInterface $passwordHasher
) {
}
public function load(ObjectManager $manager): void
{
$user = new User();
$user
->setUsername('admin')
->setPassword($this->passwordHasher->hashPassword($user, 'password'))
->setRoles(['ROLL_ADMIN', 'ROLL_USER']);
$manager->persist($user);
$manager->flush();
}
}
- パスワードをハッシュするために constructor に
UserPasswordHasherInterface
を渡すようにしています。Symfony の DI が注入してくれます - ユーザーを1件登録します
データをDBに登録させます。
php bin/console doctrine:fixtures:load
Careful, database "foo" will be purged. Do you want to continue? (yes/no) [no]:
> yes
> purging database
> loading App\DataFixtures\UserFixtures
以上でユーザー情報の作成は完了です。jwt-authentication-bundle の設定に戻ります。
firewalls
の設定
config/packages/security.yaml
を編集します
security:
enable_authenticator_manager: true
password_hashers:
Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface: 'auto'
App\Entity\User:
algorithm: auto
providers:
app_user_provider:
entity:
class: App\Entity\User
property: username
firewalls:
dev:
pattern: ^/(_(profiler|wdt)|css|images|js)/
security: false
login:
pattern: ^/api/login
stateless: true
json_login:
check_path: /api/login_check
success_handler: lexik_jwt_authentication.handler.authentication_success
failure_handler: lexik_jwt_authentication.handler.authentication_failure
api:
pattern: ^/api
stateless: true
jwt: ~
access_control:
- { path: ^/api/login, roles: PUBLIC_ACCESS }
- { path: ^/api, roles: IS_AUTHENTICATED_FULLY }
Getting started に従って、firewalls
キーと access_control
キーを設定しました。
ここまでの設定ができたら、ログイン認証が行えます。
以下のリクエストは JetBrains IDE の http client の形式で記述します。
PHPStorm などで Tools > Http Client > Create Request in Http Client で開いた Http Client に貼り付けて実行することができます。
POST http://localhost:8000/api/login_check
Content-Type: application/json
{"username":"admin","password":"password"}
このリクエストに対して、
{
"token": "......"
}
とレスポンスが返ってきたらとりあえず設定完了です。あとでここで返された token を使います。
簡単な api を作成して呼び出してみます。
src/Controller/EchoController.php
<?php
namespace App\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\Routing\Annotation\Route;
class EchoController extends AbstractController
{
#[Route('/api/echo/{message}',
name : 'echo',
methods: ['GET']
)]
public function index(string $message): JsonResponse
{
return $this->json([
'message' => $message
]);
}
}
呼び出します。
GET http://localhost:8000/api/echo/Hello!
次のように Unauthorized エラーが返ります。token を渡していないからです。
{
"code": 401,
"message": "JWT Token not found"
}
Authorization
ヘッダーにログインの時に返された token を渡します。
GET http://localhost:8000/api/echo/Hello!
Authorization: Bearer *******ThisIsToken***********
次のようにレスポンスが返されるようになります。
{
"message": "Hello!"
}