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ログインなども上記を参考に修正すれば実装できるとかと思います。
