EC-CUBE4でYahoo!ID連携を実装する方法です。
細かい説明はしていませんのでご了承ください。
ファイル設置場所やファイル名はネームスペースやクラス名をご確認ください。
必要なライブラリをComposerでインストール
bin/console eccube:composer:require knpuniversity/oauth2-client-bundle
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] ];
YConnectプロバイダーを作成
<?php namespace Customize\Security\OAuth2\Client\Provider\Yahoo; 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 YConnect extends AbstractProvider { use BearerAuthorizationTrait; /** * @inheritDoc */ public function getBaseAuthorizationUrl() { return 'https://auth.login.yahoo.co.jp/yconnect/v2/authorization'; } /** * @inheritDoc */ public function getBaseAccessTokenUrl(array $params) { return 'https://auth.login.yahoo.co.jp/yconnect/v2/token'; } /** * @inheritDoc */ protected function getAccessTokenOptions(array $params) { $options = parent::getAccessTokenOptions([ 'grant_type' => 'authorization_code', 'code' => $params['code'], 'redirect_uri' => $params['redirect_uri'] ]); $options['headers']['Authorization'] = 'Basic '.base64_encode($params['client_id'].':'.$params['client_secret']); return $options; } /** * @inheritDoc */ public function getResourceOwnerDetailsUrl(AccessToken $token) { return 'https://userinfo.yahooapis.jp/yconnect/v2/attribute'; } /** * @inheritDoc */ protected function getDefaultScopes() { return [ 'openid' ]; } /** * @inheritDoc */ protected function checkResponse(ResponseInterface $response, $data) { if(isset($data['error'])) { $message = $data['error']; if(isset($data['error_description'])) { $message .= ":".$data['error_description']; } throw new IdentityProviderException($message, $response->getStatusCode(), $data); } } /** * @inheritDoc */ protected function createResourceOwner(array $response, AccessToken $token) { return new YConnectResourceOwner($response); } }
ユーザー情報を取得するYConnectResourceOwnerを作成
<?php namespace Customize\Security\OAuth2\Client\Provider\Yahoo; use League\OAuth2\Client\Provider\ResourceOwnerInterface; class YConnectResourceOwner implements ResourceOwnerInterface { /** * @var array */ protected $response; 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("sub"); } /** * @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を追加
YahooJapan!デベロッパーネットワークのアプリケーションの管理にてClient IDとシークレットを取得して設定してください。
knpu_oauth2_client: clients: yconnect_client: type: generic provider_class: Customize\Security\OAuth2\Client\Provider\Yahoo\YConnect client_id: ***************************************** client_secret: ************************************* redirect_route: yahoo_callback
Customerエンティティにyahoo_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 $yahoo_user_id; public function getYahooUserId(): ?string { return $this->yahoo_user_id; } public function setYahooUserId(?string $yahoo_user_id): self { $this->yahoo_user_id = $yahoo_user_id; return $this; } }
YahooAuthenticatorを用意
<?php namespace Customize\Security\Authenticator; 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 League\OAuth2\Client\Provider\Exception\IdentityProviderException; 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\Authentication\Token\UsernamePasswordToken; use Symfony\Component\Security\Core\Exception\AuthenticationException; use Symfony\Component\Security\Core\User\UserInterface; use Symfony\Component\Security\Core\User\UserProviderInterface; use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface; class YahooAuthenticator extends SocialAuthenticator { const OAUTH2_PROVIDER = "yahoo"; /** * @var ClientRegistry */ private $clientRegistry; /** * @var EntityManagerInterface */ private $entityManager; /** * @var RouterInterface */ private $router; /** * @var TokenStorageInterface */ private $tokenStorage; public function __construct( ClientRegistry $clientRegistry, EntityManagerInterface $entityManager, RouterInterface $router, TokenStorageInterface $tokenStorage ) { $this->clientRegistry = $clientRegistry; $this->entityManager = $entityManager; $this->router = $router; $this->tokenStorage = $tokenStorage; } public function supports(Request $request) { return $request->attributes->get('_route') === 'yahoo_callback'; } /** * @inheritDoc */ public function start(Request $request, AuthenticationException $authException = null) { return new RedirectResponse( $this->router->generate("yahoo"), Response::HTTP_TEMPORARY_REDIRECT ); } /** * @inheritDoc */ public function getCredentials(Request $request) { try { return $this->fetchAccessToken($this->getYahooClient()); } catch (AuthenticationException $e) { throw new $e; } } /** * @inheritDoc */ public function getUser($credentials, UserProviderInterface $userProvider) { $userInfo = $this->getYahooClient() ->fetchUserFromToken($credentials); // Yahooでメールアドレス認証していない場合がある if (!$userInfo->getEmailVerified()) { throw new IdentityProviderException('Yahooでメールアドレスが認証されていません。'); } // ヤフー連携済みの場合 $Customer = $this->entityManager->getRepository(Customer::class) ->findOneBy(['yahoo_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(), ["provider" => self::OAUTH2_PROVIDER])); } // 通常の会員登録済みの場合はユーザー識別子を保存 $Customer->setYahooUserId($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 getYahooClient() { return $this->clientRegistry ->getClient('yconnect_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\YahooAuthenticator access_decision_manager: strategy: unanimous allow_if_all_abstain: false
Yahoo!ID連携用のコントローラーを用意
<?php namespace Customize\Controller; use Eccube\Controller\AbstractController; use Symfony\Component\Routing\Annotation\Route; use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface; /** * @Route("/yahoo") * * Class YahooController * @package Customize\Controller */ class YahooController extends AbstractController { /** * @var TokenStorageInterface */ private $tokenStorage; public function __construct( TokenStorageInterface $tokenStorage ) { $this->tokenStorage = $tokenStorage; } /** * @Route("/", name="yahoo") * * @return \Symfony\Component\HttpFoundation\RedirectResponse */ public function index() { return $this->get('oauth2.registry') ->getClient('yconnect_client') ->redirect([ "scope" => "openid profile email address" ]); } /** * @Route("/callback", name="yahoo_callback") */ public function callback() { if($this->isGranted("IS_AUTHENTICATED_FULLY")) { return $this->redirectToRoute("mypage"); }else{ return $this->redirectToRoute("yahoo"); } } }
EntryTypeを拡張
Yahoo!ID連携経由で会員登録する場合はyahoo_user_idを登録するためEntryTypeを拡張します。
<?php namespace Customize\Form\Extension; use Customize\Security\Authenticator\YahooAuthenticator; 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 KnpU\OAuth2ClientBundle\Security\Helper\FinishRegistrationBehavior; use Symfony\Component\Form\AbstractTypeExtension; use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\Form\FormEvent; use Symfony\Component\Form\FormEvents; use Symfony\Component\HttpFoundation\RequestStack; class EntryTypeExtension extends AbstractTypeExtension { use FinishRegistrationBehavior; /** * @var RequestStack */ private $requestStack; /** * @var PrefRepository */ private $prefRepository; public function __construct( RequestStack $requestStack, PrefRepository $prefRepository ) { $this->requestStack = $requestStack; $this->prefRepository = $prefRepository; } public function buildForm(FormBuilderInterface $builder, array $options) { $userInfo = $this->getUserInfoFromSession($this->requestStack->getCurrentRequest()); if($userInfo && $userInfo["provider"] === YahooAuthenticator::OAUTH2_PROVIDER) { // メールアドレスをセット $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->setYahooUserId($userInfo["sub"]); } }); } } /** * {@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 %}
以上で完成のはずです。
Facebookログインなども上記を参考に修正すれば実装できるとかと思います。