2019年7月現在、巷では7payの不正利用が話題となっています ECCUBEも構築次第ではクレジットカードやその他決済方法を登録できるわけで、7payの事例も他山の石とすべきでしょう
不正アクセス対策には、いろんな段階でのいろんな対策がありますが、bot対策もその一つとなります パスワードリスト攻撃などではbotを使ってログインの試行を繰り返しますので、botだと疑われる場合は(たとえパスワードが正しくても)ログインを拒否する、という施策です
reCAPTCHA v3
そこで登場するのがGoogle様謹製のreCAPTCHA 「私はロボットではありません」というチェックボックスがあって、たまに画像を選ばされたりするアレですね そのチェックボックスがあるタイプはv2なのですが、v3ではチェックボックはなく、ユーザーの行動からbotか人間かを判別する仕組みになっています
以下はそのreCAPTCHA v3をECCUBE4で利用する手順です コマンドラインを使用したり、普段のカスタマイズでは触らないようなファイルを変更したりするので、多少ハードルは高めかもしれません
サイトを登録
公式サイト でreCAPTCHAを利用したいサイトの情報を登録します reCAPTCHAタイプではv3を選びます 登録したドメインは、そのサブドメインも登録対象となります(例: ruco.laを登録しておけばblog.ruco.laも対象に)
登録したらサイトキー (フロント側で使用)とシークレットキー (システム側で使用)が表示されます これがあとで必要になります
ECCUBEに導入する
この記事を執筆時の最新バージョンEC-CUBE 4.0.2 への導入手順です (バージョンが違うと細部が変わっているかもしれません)
composerを使ってライブラリのインストール
ECCUBE4で使用しているフレームワークSymfony用のreCAPTCHAライブラリをインストールします ここでは Login Recaptcha Bundle for Symfony 3 を使用します
サーバにログインしてec-cubeディレクトリまで移動した後にcomposerを使ってインストールします
$ composer require syspay/login-recaptcha-bundle
しばらく待っているといろいろメッセージが出てきますが
- WARNING google/recaptcha (>=1.1): From github.com/symfony/recipes-contrib:master
The recipe for this package comes from the "contrib" repository, which is open to community contributions.
Review the recipe at https://github.com/symfony/recipes-contrib/tree/master/google/recaptcha/1.1
Do you want to execute this recipe?
[y] Yes
[n] No
[a] Yes for all packages, only for the current installation session
[p] Yes permanently, never ask again for this project
(defaults to n):
ここはデフォルト(n)で大丈夫でした(ただエンターキーを押せばOK)
ECCUBE システム側の設定
今インストールしたbundle(ライブラリ)をECCUBEで使用できるように登録します
app/config/eccube/bundles.php
return [
Symfony\Bundle\FrameworkBundle\FrameworkBundle::class => ['all' => true],
Symfony\Bundle\SecurityBundle\SecurityBundle::class => ['all' => true],
Doctrine\Bundle\DoctrineCacheBundle\DoctrineCacheBundle::class => ['all' => true],
Doctrine\Bundle\DoctrineBundle\DoctrineBundle::class => ['all' => true],
Doctrine\Bundle\MigrationsBundle\DoctrineMigrationsBundle::class => ['all' => true],
Doctrine\Bundle\FixturesBundle\DoctrineFixturesBundle::class => ['all' => true],
Sensio\Bundle\FrameworkExtraBundle\SensioFrameworkExtraBundle::class => ['all' => true],
Symfony\Bundle\MonologBundle\MonologBundle::class => ['all' => true],
Symfony\Bundle\SwiftmailerBundle\SwiftmailerBundle::class => ['all' => true],
Symfony\Bundle\TwigBundle\TwigBundle::class => ['all' => true],
Symfony\Bundle\DebugBundle\DebugBundle::class => ['dev' => true, 'test' => true, 'install' => true],
Symfony\Bundle\WebServerBundle\WebServerBundle::class => ['dev' => true, 'install' => true],
Symfony\Bundle\WebProfilerBundle\WebProfilerBundle::class => ['dev' => true, 'test' => true, 'install' => true],
DAMA\DoctrineTestBundle\DAMADoctrineTestBundle::class => ['test' => true],
Symfony\Bundle\MakerBundle\MakerBundle::class => ['dev' => true],
SunCat\MobileDetectBundle\MobileDetectBundle::class => ['all' => true],
Knp\Bundle\PaginatorBundle\KnpPaginatorBundle::class => ['all' => true],
// 末尾に追加
LoginRecaptcha\Bundle\LoginRecaptchaBundle::class => ['all' => true],
];
次にログイン認証でこのライブラリを使うように変更します ここで先程サイト登録して得られたシークレットキー が必要になります
app/config/eccube/packages/security.yaml
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_captcha:
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
google_recaptcha_secret: XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
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_captcha:
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
google_recaptcha_secret: XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
logout:
path: logout
target: homepage
access_decision_manager:
strategy: unanimous
allow_if_all_abstain: false
form_loginの項目名をform_login_captchaに変更して、その下にgoogle_recaptcha_secretという項目を追加しています google_recaptcha_secretの値には先程の取得したシークレットキー を記入します(XXXXXXXの箇所) 上記ではこの変更をadmin(管理者ログイン)、customer(お客様ログイン)の両方に施しています もしどちらか片方だけでいい場合はreCAPTCHAしたいほうだけを変更してください
この状態でログインを試してみます まだフロント側の対応をしていないのでこの段階ではログインに失敗するのが正解です
無事(という言い方はおかしいけど笑)、ログインに失敗しました!
フロント側の対応
フロント側(twig)を対応させます 今度はサイトキー のほうが必要となります
twig(デザインテンプレート)はデフォルトのものはsrc/Eccube/Resource/template以下にあります カスタマイズする場合はデフォルトは触らずにapp/templateにコピーしてから行うのが推奨されて います
管理者ログインapp/template/admin/login.twig
{% extends '@admin/login_frame.twig' %}
{% form_theme form '@admin/Form/bootstrap_4_horizontal_layout.html.twig' %}
{% block javascript %}
<script src="https://www.google.com/recaptcha/api.js?render=XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"></script>
<script>
grecaptcha.ready(function(){
$('#form1 button').on('click',function(e){
e.preventDefault();
grecaptcha.execute('XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX', {action: 'login'}).then(function(token) {
$('#form1').prepend('<input type="hidden" name="g-recaptcha-response" value="' + token + '">');
$('#form1').submit();
});
});
});
</script>
{% endblock %}
{% block main %}
<div class="container" style="margin-top: 150px;">
<div class="row">
<div class="col-12 col-md-6 offset-md-3 col-lg-4 offset-lg-4">
<div class="text-center p-5 bg-white">
{{ include('@admin/alert.twig') }}
<form name="form1" id="form1" method="post" action="{{ path('admin_login') }}">
<input type="hidden" name="_csrf_token" value="{{ csrf_token('authenticate') }}">
<p><img src="{{ asset('assets/img/logo2.png', 'admin') }}" width="106"></p>
<div class="form-group">
{{ form_widget(form.login_id, {'id': 'login_id', 'attr': {'placeholder': 'admin.login.login_id', 'autofocus': true}}) }}
</div>
<div class="form-group">
{{ form_widget(form.password, {'attr': {'placeholder': 'admin.login.password'}}) }}
</div>
{% if error %}
<div class="form-group">
<span class="text-danger">{{ error.messageKey|trans(error.messageData, 'validators')|nl2br }}</span>
</div>
{% endif %}
<button type="submit" class="btn btn-primary btn-lg btn-block">{{ 'admin.login.login'|trans }}</button>
{{ form_rest(form) }}
</form>
</div>
</div>
<div class="col-12">
<p class="text-center mt-3">
<small>Copyright © 2000-{{ "now"|date("Y") }} EC-CUBE CO.,LTD. All Rights Reserved.</small>
</p>
</div>
</div>
</div>
{% endblock %}
MYページログインapp/template/default/Mypage/login.twig
{% extends 'default_frame.twig' %}
{% set body_class = 'cart_page' %}
{% block javascript %}
<script src="https://www.google.com/recaptcha/api.js?render=XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"></script>
<script>
grecaptcha.ready(function(){
$('#shopping_login button').on('click',function(e){
e.preventDefault();
grecaptcha.execute('XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX', {action: 'ecommerce'}).then(function(token) {
$('#shopping_login').prepend('<input type="hidden" name="g-recaptcha-response" value="' + token + '">');
$('#shopping_login').submit();
});
});
});
</script>
{% endblock %}
{% block main %}
<div class="ec-role">
<div class="ec-pageHeader">
<h1>{{ 'common.login'|trans }}</h1>
</div>
</div>
<div class="ec-role">
<div class="ec-grid3">
<div class="ec-grid3__cell2">
<form name="shopping_login" id="shopping_login" method="post" action="{{ url('mypage_login') }}">
<input type="hidden" name="_target_path" value="shopping" />
<input type="hidden" name="_failure_path" value="shopping_login" />
<input type="hidden" name="_csrf_token" value="{{ csrf_token('authenticate') }}">
<div class="ec-login">
<div class="ec-login__icon">
<div class="ec-icon"><img src="{{ asset('assets/icon/user.svg') }}" alt=""></div>
</div>
<div class="ec-login__input">
<div class="ec-input">
{{ form_widget(form.login_email, { attr: { 'style' : 'ime-mode: disabled;', placeholder: 'common.mail_address'|trans, 'autofocus': true }}) }}
{{ form_widget(form.login_pass, { attr: { placeholder: 'common.password'|trans }}) }}
</div>
{% if BaseInfo.option_remember_me %}
<div class="ec-checkbox">
<label>
{% if is_granted('IS_AUTHENTICATED_REMEMBERED') %}
<input type="hidden" name="login_memory" value="1">
{% else %}
{{ form_widget(form.login_memory, { 'label': 'common.remember_me'|trans }) }}
{% endif %}
</label>
</div>
{% endif %}
</div>
{% if error %}
<p class="ec-errorMessage">{{ error.messageKey|trans(error.messageData, 'validators')|nl2br }}</p>
{% endif %}
<div class="ec-grid2">
<div class="ec-grid2__cell">
<div class="ec-login__actions">
<button type="submit" class="ec-blockBtn--cancel">{{ 'common.login'|trans}}</button>
</div>
</div>
<div class="ec-grid2__cell">
<div class="ec-login__link"><a class="ec-link" href="{{ url('forgot') }}">{{ 'common.forgot_login'|trans}}</a>
</div>
<div class="ec-login__link"><a class="ec-link" href="{{ url('entry') }}">{{ 'common.signup'|trans}}</a>
</div>
</div>
</div>
</div>
</form>
</div>
{% if is_granted('IS_AUTHENTICATED_REMEMBERED') == false %}
<div class="ec-grid3__cell">
<div class="ec-guest">
<div class="ec-guest__inner">
<p>{{ 'front.shopping.guest_purchase_message'|trans }}</p>
<div class="ec-guest__actions"><a class="ec-blockBtn--cancel" href="{{ url('shopping_nonmember') }}">{{ 'front.shopping.guest_purchase'|trans }}</a>
</div>
</div>
</div>
</div>
{% endif %}
</div>
</div>
{% endblock %}
商品購入ログインapp/template/default/Shopping/login.twig
{% extends 'default_frame.twig' %}
{% set body_class = 'cart_page' %}
{% block javascript %}
<script src="https://www.google.com/recaptcha/api.js?render=XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"></script>
<script>
grecaptcha.ready(function(){
$('#shopping_login button').on('click',function(e){
e.preventDefault();
grecaptcha.execute('XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX', {action: 'ecommerce'}).then(function(token) {
$('#shopping_login').prepend('<input type="hidden" name="g-recaptcha-response" value="' + token + '">');
$('#shopping_login').submit();
});
});
});
</script>
{% endblock %}
{% block main %}
<div class="ec-role">
<div class="ec-pageHeader">
<h1>{{ 'common.login'|trans }}</h1>
</div>
</div>
<div class="ec-role">
<div class="ec-grid3">
<div class="ec-grid3__cell2">
<form name="shopping_login" id="shopping_login" method="post" action="{{ url('mypage_login') }}">
<input type="hidden" name="_target_path" value="shopping" />
<input type="hidden" name="_failure_path" value="shopping_login" />
<input type="hidden" name="_csrf_token" value="{{ csrf_token('authenticate') }}">
<div class="ec-login">
<div class="ec-login__icon">
<div class="ec-icon"><img src="{{ asset('assets/icon/user.svg') }}" alt=""></div>
</div>
<div class="ec-login__input">
<div class="ec-input">
{{ form_widget(form.login_email, { attr: { 'style' : 'ime-mode: disabled;', placeholder: 'common.mail_address'|trans, 'autofocus': true }}) }}
{{ form_widget(form.login_pass, { attr: { placeholder: 'common.password'|trans }}) }}
</div>
{% if BaseInfo.option_remember_me %}
<div class="ec-checkbox">
<label>
{% if is_granted('IS_AUTHENTICATED_REMEMBERED') %}
<input type="hidden" name="login_memory" value="1">
{% else %}
{{ form_widget(form.login_memory, { 'label': 'common.remember_me'|trans }) }}
{% endif %}
</label>
</div>
{% endif %}
</div>
{% if error %}
<p class="ec-errorMessage">{{ error.messageKey|trans(error.messageData, 'validators')|nl2br }}</p>
{% endif %}
<div class="ec-grid2">
<div class="ec-grid2__cell">
<div class="ec-login__actions">
<button type="submit" class="ec-blockBtn--cancel">{{ 'common.login'|trans}}</button>
</div>
</div>
<div class="ec-grid2__cell">
<div class="ec-login__link"><a class="ec-link" href="{{ url('forgot') }}">{{ 'common.forgot_login'|trans}}</a>
</div>
<div class="ec-login__link"><a class="ec-link" href="{{ url('entry') }}">{{ 'common.signup'|trans}}</a>
</div>
</div>
</div>
</div>
</form>
</div>
{% if is_granted('IS_AUTHENTICATED_REMEMBERED') == false %}
<div class="ec-grid3__cell">
<div class="ec-guest">
<div class="ec-guest__inner">
<p>{{ 'front.shopping.guest_purchase_message'|trans }}</p>
<div class="ec-guest__actions"><a class="ec-blockBtn--cancel" href="{{ url('shopping_nonmember') }}">{{ 'front.shopping.guest_purchase'|trans }}</a>
</div>
</div>
</div>
</div>
{% endif %}
</div>
</div>
{% endblock %}
長々と全体を掲載していますが、3つどれもjavascriptブロックを追加しただけで、その他の箇所は変更していませんサイトキー を記入する場所はそれぞれ2箇所づつあります(XXXXXXXの箇所)
以上で完了です! 動作確認の前には、念のためキャッシュをクリアしておくのがおすすめです
$ bin/console cache:clear --no-warmup
管理者ログイン
MYページログイン
右下にreCAPTCHAのアイコンが出ているのが目印です 問題なくログインできるか試してみてください