lexik/jwt-authentication-bundleを利用してECCUBE4にJWTを用いたAPIを実装しました。
こちらの記事を参考にしております。
環境
• ECCUBEバージョン: 4.0.5
• PHPバージョン: 7.4
• Symfonyバージョン: 3.4.49
jwt-authentication-bundleを導入
まず、JWT Authentication Bundleをプロジェクトに導入します。
composer require lexik/jwt-authentication-bundle:^2.1
キーペアの作成
キーペアをECCUBEのディレクトリ構成に合わせて作成します。
mkdir -p app/config/eccube/jwt
openssl genrsa -out app/config/eccube/jwt/private.pem
openssl rsa -in app/config/eccube/jwt/private.pem -pubout > app/config/eccube/jwt/public.pem
envファイルに追記
###> lexik/jwt-authentication-bundle ###
JWT_SECRET_KEY=%kernel.project_dir%/app/config/eccube/jwt/private.pem
JWT_PUBLIC_KEY=%kernel.project_dir%/app/config/eccube/jwt/public.pem
JWT_PASSPHRASE=**********
###< lexik/jwt-authentication-bundle ###
lexik_jwt_authentication.yamlを作成
app/config/eccube/packages/lexik_jwt_authentication.yamlを作成し、以下を追加します。
以下の設定ではトークンの有効期限は1日です。
lexik_jwt_authentication:
secret_key: '%env(resolve:JWT_SECRET_KEY)%'
public_key: '%env(resolve:JWT_PUBLIC_KEY)%'
pass_phrase: '%env(JWT_PASSPHRASE)%'
token_ttl: 86400
ユーザー情報
API接続用のユーザーテーブルを追加しました。以下はサンプルのApiUserクラスです。
ApiUser.php
<?php
namespace Eccube\Entity;
use Eccube\Repository\ApiUserRepository;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Security\Core\User\UserInterface;
if (!class_exists('\Eccube\Entity\ApiUser')) {
/**
* ApiUser
* @ORM\Table(name="dtb_api_user")
* @ORM\Entity(repositoryClass=ApiUserRepository::class)
* @ORM\InheritanceType("SINGLE_TABLE")
* @ORM\DiscriminatorColumn(name="type", type="string", length=255)
* @ORM\DiscriminatorMap({"api_user" = "ApiUser"})
*/
class ApiUser implements UserInterface
{
/**
* @ORM\Id
* @ORM\GeneratedValue
* @ORM\Column(type="integer")
*/
private $id;
/**
* @ORM\Column(type="string", length=180, unique=true)
*/
private $username;
/**
* @var string The hashed password
* @ORM\Column(type="string")
*/
private $password;
public function getId(): ?int
{
return $this->id;
}
/**
* A visual identifier that represents this user.
*
* @see UserInterface
*/
public function getUsername(): string
{
return (string) $this->username;
}
public function setUsername(string $username): self
{
$this->username = $username;
return $this;
}
/**
* 使用しない
* @see UserInterface
*/
public function getRoles(): array
{
return [];
}
/**
* @see UserInterface
*/
public function getPassword(): string
{
return (string) $this->password;
}
public function setPassword(string $password): self
{
$this->password = $password;
return $this;
}
/**
* 使用しない
* Returning a salt is only needed, if you are not using a modern
* hashing algorithm (e.g. bcrypt or sodium) in your security.yaml.
*
* @see UserInterface
*/
public function getSalt(): ?string
{
return null;
}
/**
* 使用しない
* @see UserInterface
*/
public function eraseCredentials()
{
// If you store any temporary, sensitive data on the user, clear it here
// $this->plainPassword = null;
}
}
}
ApiUserRepository.php
<?php
namespace Eccube\Repository;
use Eccube\Entity\ApiUser;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Persistence\ManagerRegistry;
/**
* @method ApiUser|null find($id, $lockMode = null, $lockVersion = null)
* @method ApiUser|null findOneBy(array $criteria, array $orderBy = null)
* @method ApiUser[] findAll()
* @method ApiUser[] findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null)
*/
class ApiUserRepository extends ServiceEntityRepository
{
public function __construct(ManagerRegistry $registry)
{
parent::__construct($registry, ApiUser::class);
}
/**
* @param int $id
*
* @return ApiUser
*/
public function get($id = 1)
{
return $this->find($id);
}
}
マイグレーション
差分を取りマイグレーションを実行します。
php bin/console doctrine:migrations:diff
php bin/console doctrine:migrations:migrate
ApiUserの登録画面を追加しましたが、ここでは割愛します。INSERT文で直接登録してもOKです。
firewalls の設定
app/config/eccube/packages/security.yamlにfirewallsの設定を追記します。今回はget_token、api_1およびapi_2を追加します。
security:
providers:
api_get_token:
pattern: ^/%api_route%/get_token
stateless: true
json_login:
check_path: /%api_route%/get_token
success_handler: lexik_jwt_authentication.handler.authentication_success
failure_handler: lexik_jwt_authentication.handler.authentication_failure
api_1:
pattern: ^/%api_route%/api_1
stateless: true
guard:
authenticators:
- lexik_jwt_authentication.jwt_token_authenticator
api_2:
pattern: ^/%api_route%/api_2
stateless: true
guard:
authenticators:
- lexik_jwt_authentication.jwt_token_authenticator
access_control:
- { path: ^/%api_route%/get_token, roles: PUBLIC_ACCESS }
- { path: ^/%api_route%/api_1, roles: PUBLIC_ACCESS }
- { path: ^/%api_route%/api_2, roles: PUBLIC_ACCESS }
api_routeの設定
.envファイル
API_ROUTE="your_api_route"
app/config/eccube/packages/eccube.yaml
# APIルート
api_route: '%env(API_ROUTE)%'
認証の仕組み
今回はECCUBEで設定したApiUserのusernameとpasswordをget_tokenの認証に使用しtokenを発行します。
get_tokenで発行したtokenを利用しECCUBE上での処理を行います。
ApiControllerの設定
get_token
json形式でapi_routeに設定したパスに{“username”:“hoge”,“password”:“huga”}をPOSTします。ApiUserと照合し、usernameとpasswordの組み合わせが正しければトークンを発行します。
ApiController
<?php
namespace Eccube\Controller;
use Lexik\Bundle\JWTAuthenticationBundle\Services\JWTTokenManagerInterface;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\HttpFoundation\Request;
use Eccube\Repository\ApiUserRepository;
use Lexik\Bundle\JWTAuthenticationBundle\Encoder\JWTEncoderInterface;
use Lexik\Bundle\JWTAuthenticationBundle\Exception\JWTDecodeFailureException;
class ApiController extends AbstractController
{
/**
* @var JWTTokenManagerInterface
*/
private $JWTManager;
/**
* @var ApiUserRepository
*/
protected $apiUserRepository;
/**
* @var JWTEncoderInterface
*/
private $JWTEncoder;
public function __construct(
ApiUserRepository $apiUserRepository,
JWTTokenManagerInterface $JWTManager,
JWTEncoderInterface $JWTEncoder
) {
$this->apiUserRepository = $apiUserRepository;
$this->JWTManager = $JWTManager;
$this->JWTEncoder = $JWTEncoder;
}
/**
* @Route("/%api_route%/get_token", name="get_token", methods={"POST"})
*/
public function getToken(Request $request)
{
$data = json_decode($request->getContent(), true);
$username = $data['username'] ?? null;
$password = $data['password'] ?? null;
if(!$username || !$password){
$massage = !$username ? 'username is required' : 'password is required';
return $this->json([
'statusCode' => JsonResponse::HTTP_BAD_REQUEST,
'error' => 'Bad Request',
'message' => $massage,
], JsonResponse::HTTP_BAD_REQUEST);
}
$user = $this->apiUserRepository->findOneBy(['username' => $username]);
if (!$user || $password !== $user->getPassword()) {
return $this->json([
'statusCode' => JsonResponse::HTTP_UNAUTHORIZED,
'error' => 'Unauthorized',
'message' => 'username or password is incorrect',
], JsonResponse::HTTP_UNAUTHORIZED);
}
$token = $this->JWTManager->create($user);
return $this->json([
'token' => $token,
]);
}
リクエスト例
curl -k -X POST https://your_domain/your_api_route/get_token -H "Content-Type: application/json" -d '{"username":"hoge","password":"huga"}'
トークンを検証しapiを実行
ヘッダーのBearer tokenを検証します。認証できた場合、リクエストボディのパラメータを利用して処理を行います。
ApiController
/**
* @Route("/%api_route%/api_1", name="api_1", methods={"POST"})
*/
public function api_1(Request $request)
{
$token = $request->headers->get('Authorization');
if (!$token) {
return $this->json([
'statusCode' => JsonResponse::HTTP_UNAUTHORIZED,
'error' => 'Unauthorized',
'message' => 'Token not found',
], JsonResponse::HTTP_UNAUTHORIZED);
}
$token = str_replace('Bearer ', '', $token);
try {
$this->JWTEncoder->decode($token);
} catch (JWTDecodeFailureException $e) {
return $this->json([
'statusCode' => JsonResponse::HTTP_UNAUTHORIZED,
'error' => 'Unauthorized',
'message' => $e->getMessage(),
], JsonResponse::HTTP_UNAUTHORIZED);
}
// トークンが有効な場合の処理
$data = json_decode($request->getContent());
//$dataを利用した処理
// 成功の処理
$message = 'message';
return new JsonResponse(['message' => $message]);
}
/**
* @Route("/%api_route%/api_2", name="api_1", methods={"POST"})
*/
public function api_2(Request $request)
{
//api_2の処理
...
リクエスト例
curl -k -X POST https://your_domain/your_api_route/api_1 -H "Authorization: Bearer ey......" -H "Content-Type: application/json" -d '{"hoge":"huga"}'