【EC-CUBE4】Lineログインを実装する方法

EC-CUBE4でLineログインを実装する方法です。

細かい説明はしていませんのでご了承ください。

ファイル設置場所やファイル名はネームスペースやクラス名をご確認ください。

必要なライブラリをComposerでインストール

bin/console eccube:composer:require knpuniversity/oauth2-client-bundle
bin/console eccube:composer:require firebase/php-jwt

bundles.phpにライブラリを追加

<?php
// app/config/eccube/bundles.php

/*
 * This file is part of EC-CUBE
 *
 * Copyright(c) EC-CUBE CO.,LTD. All Rights Reserved.
 *
 * http://www.ec-cube.co.jp/
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

return [
   // ...
    KnpU\OAuth2ClientBundle\KnpUOAuth2ClientBundle::class => ['all' => true]
];

LineとOauth2通信するためのプロバイダーを作成

<?php


namespace Customize\Security\OAuth2\Client\Provider\Line;


use League\OAuth2\Client\Provider\AbstractProvider;
use League\OAuth2\Client\Provider\Exception\IdentityProviderException;
use League\OAuth2\Client\Token\AccessToken;
use League\OAuth2\Client\Tool\BearerAuthorizationTrait;
use Psr\Http\Message\ResponseInterface;

class Line extends AbstractProvider
{
    use BearerAuthorizationTrait;

    /**
     * @inheritDoc
     */
    public function getBaseAuthorizationUrl()
    {
        return 'https://access.line.me/oauth2/v2.1/authorize';
    }

    /**
     * @inheritDoc
     */
    public function getBaseAccessTokenUrl(array $params)
    {
        return 'https://api.line.me/oauth2/v2.1/token';
    }

    /**
     * @inheritDoc
     */
    public function getResourceOwnerDetailsUrl(AccessToken $token)
    {
        return 'https://api.line.me/v2/profile';
    }

    /**
     * @inheritDoc
     */
    protected function getDefaultScopes()
    {
        return [
            'openid'
        ];
    }

    /**
     * @inheritDoc
     */
    protected function checkResponse(ResponseInterface $response, $data)
    {
        if(!empty($data['error'])) {
            $code = 0;
            $error = $data['error'];

            if(is_array($error)) {
                $code = $error['code'];
                $message = $error['message'];
            }

            throw new IdentityProviderException($message, $response->getStatusCode(), $data);
        }
    }

    /**
     * @inheritDoc
     */
    protected function createResourceOwner(array $response, AccessToken $token)
    {
        return new LineResourceOwner($response);
    }
}

ユーザー情報を取得するためのLineResourceOwnerを用意

<?php


namespace Customize\Security\OAuth2\Client\Provider\Line;


use Firebase\JWT\JWT;
use League\OAuth2\Client\Provider\ResourceOwnerInterface;
use League\OAuth2\Client\Token\AccessToken;

class LineResourceOwner implements ResourceOwnerInterface
{

    /**
     * @var array
     */
    protected $response;

    /**
     * @var AccessToken
     */
    protected $token;

    public function __construct(array $response)
    {
        $this->response = $response;
    }

    public function __call($name, $arguments)
    {
        $get = substr($name,0, 3);
        if ($get !== 'get') {
            return null;
        }
        $parameter = function($name) {
            return ltrim(strtolower(preg_replace('/[A-Z]/', '_\0', str_replace("get", "", $name))), '_');
        };
        return $this->getResource($parameter($name));
    }

    /**
     * @inheritDoc
     */
    public function getId()
    {
        return $this->getResource("userId");
    }

    public function getEmail() {
        $id_token = $this->token->getValues()["id_token"];
        $tokens = explode(".", $id_token);

        $payload = JWT::urlsafeB64Decode($tokens[1]);
        return isset(json_decode($payload)->email) ? json_decode($payload)->email : null;
    }

    /**
     * @inheritDoc
     */
    public function toArray()
    {
        return $this->response;
    }

    protected function getResource(string $name)
    {
        return isset($this->response[$name]) ? $this->response[$name] : null;
    }
}

knpu_oauth2_client.yamlにLine用の設定を追加

line_client:
  type: generic
  provider_class: Customize\Security\OAuth2\Client\Provider\Line\Line
  client_id: '***************************************'
  client_secret: '***********************************'
  redirect_route: line_callback

Customerエンティティにline_user_idプロパティを追加

<?php
 
namespace Customize\Entity;
 
use Customize\Entity\Master\CustomerType;
use Doctrine\ORM\Mapping as ORM;
use Eccube\Annotation\EntityExtension;
 
/**
* @EntityExtension("Eccube\Entity\Customer")
*/
trait CustomerTrait
{
    /**
     * @ORM\Column(type="string", length=255, nullable=true)
     */
    private $line_user_id;
 
    public function getLineUserId(): ?string
    {
        return $this->line_user_id;
    }
 
    public function setLineUserId(?string $line_user_id): self
    {
        $this->line_user_id = $line_user_id;
 
        return $this;
    }
}

LineAuthenticatorを用意

<?php


namespace Customize\Security\Authenticator;


use Customize\Security\OAuth2\Client\Provider\Line\Line;
use Doctrine\ORM\EntityManagerInterface;
use Eccube\Entity\Customer;
use KnpU\OAuth2ClientBundle\Client\ClientRegistry;
use KnpU\OAuth2ClientBundle\Security\Authenticator\SocialAuthenticator;
use KnpU\OAuth2ClientBundle\Security\Exception\FinishRegistrationException;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\RouterInterface;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
use Symfony\Component\Security\Core\User\UserProviderInterface;

class LineAuthenticator extends SocialAuthenticator
{
    const OAUTH2_PROVIDER = "line";

    /**
     * @var ClientRegistry
     */
    private $clientRegistry;

    /**
     * @var EntityManagerInterface
     */
    private $entityManager;

    /**
     * @var RouterInterface
     */
    private $router;

    public function __construct(
        ClientRegistry $clientRegistry,
        EntityManagerInterface $entityManager,
        RouterInterface $router
    ) {
        $this->clientRegistry = $clientRegistry;
        $this->entityManager = $entityManager;
        $this->router = $router;
    }

    public function supports(Request $request)
    {
        return $request->attributes->get('_route') === 'line_callback';
    }

    /**
     * @inheritDoc
     */
    public function start(Request $request, AuthenticationException $authException = null)
    {
        return new RedirectResponse(
            $this->router->generate("line"),
            Response::HTTP_TEMPORARY_REDIRECT
        );
    }

    /**
     * @inheritDoc
     */
    public function getCredentials(Request $request)
    {
        try {
            return $this->fetchAccessToken($this->getLineClient());
        } catch (AuthenticationException $e) {
            throw new $e;
        }
    }

    /**
     * @inheritDoc
     */
    public function getUser($credentials, UserProviderInterface $userProvider)
    {
        $userInfo = $this->getLineClient()
            ->fetchUserFromToken($credentials);

        // Line連携済みの場合
        $Customer = $this->entityManager->getRepository(Customer::class)
            ->findOneBy(['line_user_id' => $userInfo->getId()]);
        if($Customer) {
            return $Customer;
        }

        $Customer = $this->entityManager->getRepository(Customer::class)
            ->findOneBy(['email' => $userInfo->getEmail()]);

        // 会員登録していない場合、会員登録ページへ
        if(!$Customer) {
            throw new FinishRegistrationException(array_merge($userInfo->toArray(), ["email" => $userInfo->getEmail(), "provider" => self::OAUTH2_PROVIDER]));
        }

        // 通常の会員登録済みの場合はユーザー識別子を保存
        $Customer->setLineUserId($userInfo->getId());
        $this->entityManager->persist($Customer);
        $this->entityManager->flush();

        return $Customer;
    }

    /**
     * @inheritDoc
     */
    public function onAuthenticationFailure(Request $request, AuthenticationException $exception)
    {
        // 会員登録していない場合
        if ($exception instanceof FinishRegistrationException) {
            $this->saveUserInfoToSession($request, $exception);
            return new RedirectResponse($this->router->generate("entry"));
        } else {
            $this->saveAuthenticationErrorToSession($request, $exception);
            return new RedirectResponse($this->router->generate("mypage_login"));
        }
    }

    /**
     * @inheritDoc
     */
    public function onAuthenticationSuccess(Request $request, TokenInterface $token, $providerKey)
    {
        $targetUrl = $this->router->generate("mypage");

        return new RedirectResponse($targetUrl);
    }

    /**
     * EC-CUBEがUsernamePasswordTokenなので合わせる
     *
     * @param UserInterface $user
     * @param string $providerKey
     * @return UsernamePasswordToken|\Symfony\Component\Security\Guard\Token\PostAuthenticationGuardToken
     */
    public function createAuthenticatedToken(UserInterface $user, $providerKey)
    {
        if ($user instanceof Customer && $providerKey === 'customer') {
            return new UsernamePasswordToken($user, null, $providerKey, ['ROLE_USER']);
        }
 
        return parent::createAuthenticatedToken($user, $providerKey);
    }

    private function getLineClient()
    {
        return $this->clientRegistry
            ->getClient('line_client');
    }
}

security.yamlを修正

guardを追加しています。

security:
    encoders:
        # Our user class and the algorithm we'll use to encode passwords
        # https://symfony.com/doc/current/security.html#c-encoding-the-user-s-password
        Eccube\Entity\Member:
          id: Eccube\Security\Core\Encoder\PasswordEncoder
        Eccube\Entity\Customer:
          id: Eccube\Security\Core\Encoder\PasswordEncoder
    providers:
        # https://symfony.com/doc/current/security.html#b-configuring-how-users-are-loaded
        # In this example, users are stored via Doctrine in the database
        # To see the users at src/App/DataFixtures/ORM/LoadFixtures.php
        # To load users from somewhere else: https://symfony.com/doc/current/security/custom_provider.html
        member_provider:
            id: Eccube\Security\Core\User\MemberProvider
        customer_provider:
            id: Eccube\Security\Core\User\CustomerProvider
    # https://symfony.com/doc/current/security.html#initial-security-yml-setup-authentication
    firewalls:
        dev:
            pattern: ^/(_(profiler|wdt)|css|images|js)/
            security: false
        admin:
            pattern: '^/%eccube_admin_route%/'
            anonymous: true
            provider: member_provider
            form_login:
                check_path: admin_login
                login_path: admin_login
                csrf_token_generator: security.csrf.token_manager
                default_target_path: admin_homepage
                username_parameter: 'login_id'
                password_parameter: 'password'
                use_forward: true
                success_handler: eccube.security.success_handler
                failure_handler: eccube.security.failure_handler
            logout:
                path: admin_logout
                target: admin_login
        customer:
            pattern: ^/
            anonymous: true
            provider: customer_provider
            remember_me:
                secret: '%kernel.secret%'
                lifetime: 3600
                name: eccube_remember_me
                remember_me_parameter: 'login_memory'
            form_login:
                check_path: mypage_login
                login_path: mypage_login
                csrf_token_generator: security.csrf.token_manager
                default_target_path: homepage
                username_parameter: 'login_email'
                password_parameter: 'login_pass'
                use_forward: true
                success_handler: eccube.security.success_handler
                failure_handler: eccube.security.failure_handler
            logout:
                path: logout
                target: homepage
            guard:
                authenticators:
                    - Customize\Security\Authenticator\LineAuthenticator

    access_decision_manager:
        strategy: unanimous
        allow_if_all_abstain: false

Lineログイン用のコントローラーを用意

<?php


namespace Customize\Controller;


use Eccube\Controller\AbstractController;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Security\Csrf\TokenStorage\TokenStorageInterface;

/**
 * @Route("/line")
 *
 * Class LineController
 * @package Customize\Controller
 */
class LineController extends AbstractController
{
    /**
     * @var TokenStorageInterface
     */
    private $tokenStorage;

    public function __construct(
        TokenStorageInterface $tokenStorage
    ) {
        $this->tokenStorage = $tokenStorage;
    }

    /**
     * @Route("/", name="line")
     *
     * @return \Symfony\Component\HttpFoundation\RedirectResponse
     */
    public function index()
    {
        return $this->get('oauth2.registry')
            ->getClient('line_client')
            ->redirect([
                "scope" => "openid profile email"
            ]);
    }

    /**
     * @Route("/callback", name="line_callback")
     *
     * @return \Symfony\Component\HttpFoundation\RedirectResponse
     */
    public function callback()
    {
        if($this->isGranted("IS_AUTHENTICATED_FULLY")) {
            return $this->redirectToRoute("mypage");
        }else{
            return $this->redirectToRoute("line");
        }
    }
}

EntryTypeを拡張

Line連携経由で会員登録する場合はline_user_idを登録するためEntryTypeを拡張します。

<?php

namespace Customize\Form\Extension;

use Customize\Security\OAuth2\Client\Provider\Line\Line;
use Eccube\Entity\Customer;
use Eccube\Form\Type\AddressType;
use Eccube\Form\Type\Front\EntryType;
use Eccube\Form\Type\NameType;
use Eccube\Form\Type\PostalType;
use Eccube\Form\Type\RepeatedEmailType;
use Eccube\Repository\Master\PrefRepository;
use Symfony\Component\Form\AbstractTypeExtension;
use Symfony\Component\Form\Extension\Core\Type\HiddenType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Form\FormEvent;
use Symfony\Component\Form\FormEvents;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\Security\Core\Encoder\EncoderFactoryInterface;

class EntryTypeExtension extends AbstractTypeExtension
{
    use FinishRegistrationBehavior;

    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $userInfo = $this->getUserInfoFromSession($this->requestStack->getCurrentRequest());
        if($userInfo) {
            // メールアドレスをセット
            $builder
                ->addEventListener(FormEvents::POST_SET_DATA, function(FormEvent $event) use ($userInfo) {
                    $form = $event->getForm();

                    $form['email']->setData($userInfo["email"]);
                });

            // ユーザー識別子をCustomerにセット
            $builder
                ->addEventListener(FormEvents::POST_SUBMIT, function(FormEvent $event) use ($userInfo) {
                    $Customer = $event->getData();
                    if($Customer instanceof Customer) {
                        $Customer->setLineUserId($userInfo["userId"]);
                    }
                });
        }
    }

    /**
    * {@inheritdoc}
    */
    public function getExtendedType()
    {
        return EntryType::class;
    }
}

ソーシャルログインのときは会員登録ページでメールアドレスを編集不可にする

Entry/index.twigを編集してください。

{% if app.session.get('guard.finish_registration.user_information') %}
                        <dl>
                            <dt>
                                {{ form_label(form.email, 'common.mail_address', {'label_attr': {'class': 'ec-label'}}) }}
                            </dt>
                            <dd>
                                {{ form.email.vars.data }}
                                {{ form_widget(form.email.first, { type : 'hidden' }) }}
                                {{ form_widget(form.email.second, { type : 'hidden' }) }}
                            </dd>
                        </dl>
{% else %}
                        <dl>
                            <dt>
                                {{ form_label(form.email, 'common.mail_address', { 'label_attr': { 'class': 'ec-label' }}) }}
                            </dt>
                            <dd>
                                <div class="ec-input{{ has_errors(form.email.first) ? ' error' }}">
                                    {{ form_widget(form.email.first, { 'attr': { 'placeholder': 'common.mail_address_sample' }}) }}
                                    {{ form_errors(form.email.first) }}
                                </div>
                                <div class="ec-input{{ has_errors(form.email.second) ? ' error' }}">
                                    {{ form_widget(form.email.second, { 'attr': { 'placeholder': 'common.repeated_confirm' }}) }}
                                    {{ form_errors(form.email.second) }}
                                </div>
                            </dd>
                        </dl>
{% endif %}

以上で完成です。

2件のコメント

  1. bundles.phpにKnpUOAuth2ClientBundle登録した時点でエラーが出てしまいます。

    Fatal error: Uncaught Error: Class ‘KnpU\OAuth2ClientBundle\KnpUOAuth2ClientBundle’ not found in /var/www/shachihata-id/src/Eccube/Kernel.php:74 Stack trace: #0 /var/www/shachihata-id/vendor/symfony/http-kernel/Kernel.php(488): Eccube\Kernel->registerBundles() #1 /var/www/shachihata-id/vendor/symfony/http-kernel/Kernel.php(132): Symfony\Component\HttpKernel\Kernel->initializeBundles() #2 /var/www/shachihata-id/src/Eccube/Kernel.php(89): Symfony\Component\HttpKernel\Kernel->boot() #3 /var/www/shachihata-id/vendor/symfony/http-kernel/Kernel.php(195): Eccube\Kernel->boot() #4 /var/www/shachihata-id/index.php(78): Symfony\Component\HttpKernel\Kernel->handle(Object(Symfony\Component\HttpFoundation\Request)) #5 {main} thrown in /var/www/shachihata-id/src/Eccube/Kernel.php on line 74

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

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