1概要

この記事では、リンクを続ける:/spring-security-registration[Spring Securityの登録]シリーズでは、登録プロセスに

Google


reCAPTCHA

を追加して、人間とボットを区別します。


2 GoogleのreCAPTCHA

を統合する

GoogleのreCAPTCHAウェブサービスを統合するには、まずサイトをサービスに登録し、そのライブラリをページに追加してから、ユーザーのキャプチャ応答をウェブサービスで確認する必要があります。


2.1. APIキーペアの保存

キーは__application.propertiesに格納します。

google.recaptcha.key.site=6LfaHiITAAAA...
google.recaptcha.key.secret=6LfaHiITAAAA...

そして、__ @ ConfigurationPropertiesのアノテーションが付けられたBeanを使用してそれらをSpringに公開します。

@Component
@ConfigurationProperties(prefix = "google.recaptcha.key")
public class CaptchaSettings {

    private String site;
    private String secret;

   //standard getters and setters
}


2.2. ウィジェットを表示する

シリーズのチュートリアルを基にして、Googleのライブラリを含めるように

registration.html

を変更します。

登録フォームの中に、属性

data-sitekey



site-key

が含まれることを期待するreCAPTCHAウィジェットを追加します。

ウィジェットは送信時にリクエストパラメータ

g-recaptcha-response

を追加します** :

<!DOCTYPE html>
<html>
<head>

...

<script src='https://www.google.com/recaptcha/api.js'></script>
</head>
<body>

    ...

    <form action="/" method="POST" enctype="utf8">
        ...

        <div class="g-recaptcha col-sm-5"
          th:attr="data-sitekey=${@captchaSettings.getSite()}"></div>
        <span id="captchaError" class="alert alert-danger col-sm-4"
          style="display:none"></span>


3サーバー側の検証

新しいリクエストパラメータには、サイトキーと、チャレンジが正常に完了したことを示す一意の文字列がエンコードされています。

しかし、私たちは自分自身を見分けることができないので、ユーザーが投稿したものが正当であることを信頼することはできません。 WebサービスAPIを使用して

captcha response

を検証するためにサーバー側の要求が行われます。

エンドポイントは、クエリパラメータ


secret





response


、および__ ** remoteipを使用して、URL

https://www.google.com/recaptcha/api/siteverifyでHTTPリクエストを受け入れます。スキーマを持つjsonレスポンス

:

{
    "success": true|false,
    "challenge__ts": timestamp,
    "hostname": string,
    "error-codes":[...]}


3.1. ユーザーの回答を取得する

reCAPTCHAチャレンジに対するユーザーの応答は、

HttpServletRequest

を使用して要求パラメーター

g-recaptcha-response

から取得され、

CaptchaService

で検証されます。応答の処理中にスローされた例外は、残りの登録ロジックを中止します。

public class RegistrationController {

    @Autowired
    private ICaptchaService captchaService;

    ...

    @RequestMapping(value = "/user/registration", method = RequestMethod.POST)
    @ResponseBody
    public GenericResponse registerUserAccount(@Valid UserDto accountDto, HttpServletRequest request) {
        String response = request.getParameter("g-recaptcha-response");
        captchaService.processResponse(response);

       //Rest of implementation
    }

    ...
}


3.2. 検証サービス

取得したキャプチャ応答は最初にサニタイズする必要があります。単純な正規表現が使用されています。

応答が合法的に見える場合は、

secret-key



captcha response

、およびクライアントの

IPアドレス

を使用してWebサービスに要求を送信します。

public class CaptchaService implements ICaptchaService {

    @Autowired
    private CaptchaSettings captchaSettings;

    @Autowired
    private RestOperations restTemplate;

    private static Pattern RESPONSE__PATTERN = Pattern.compile("[A-Za-z0-9__-]+");

    @Override
    public void processResponse(String response) {
        if(!responseSanityCheck(response)) {
            throw new InvalidReCaptchaException("Response contains invalid characters");
        }

        URI verifyUri = URI.create(String.format(
          "https://www.google.com/recaptcha/api/siteverify?secret=%s&response=%s&remoteip=%s",
          getReCaptchaSecret(), response, getClientIP()));

        GoogleResponse googleResponse = restTemplate.getForObject(verifyUri, GoogleResponse.class);

        if(!googleResponse.isSuccess()) {
            throw new ReCaptchaInvalidException("reCaptcha was not successfully validated");
        }
    }

    private boolean responseSanityCheck(String response) {
        return StringUtils.hasLength(response) && RESPONSE__PATTERN.matcher(response).matches();
    }
}


3.3. 検証の客観化


Jackson

アノテーションで装飾されたJava Beanは検証応答をカプセル化します。

@JsonInclude(JsonInclude.Include.NON__NULL)
@JsonIgnoreProperties(ignoreUnknown = true)
@JsonPropertyOrder({
    "success",
    "challenge__ts",
    "hostname",
    "error-codes"
})
public class GoogleResponse {

    @JsonProperty("success")
    private boolean success;

    @JsonProperty("challenge__ts")
    private String challengeTs;

    @JsonProperty("hostname")
    private String hostname;

    @JsonProperty("error-codes")
    private ErrorCode[]errorCodes;

    @JsonIgnore
    public boolean hasClientError() {
        ErrorCode[]errors = getErrorCodes();
        if(errors == null) {
            return false;
        }
        for(ErrorCode error : errors) {
            switch(error) {
                case InvalidResponse:
                case MissingResponse:
                    return true;
            }
        }
        return false;
    }

    static enum ErrorCode {
        MissingSecret,     InvalidSecret,
        MissingResponse,   InvalidResponse;

        private static Map<String, ErrorCode> errorsMap = new HashMap<String, ErrorCode>(4);

        static {
            errorsMap.put("missing-input-secret",   MissingSecret);
            errorsMap.put("invalid-input-secret",   InvalidSecret);
            errorsMap.put("missing-input-response", MissingResponse);
            errorsMap.put("invalid-input-response", InvalidResponse);
        }

        @JsonCreator
        public static ErrorCode forValue(String value) {
            return errorsMap.get(value.toLowerCase());
        }
    }

   //standard getters and setters
}

暗黙のとおり、

success

プロパティの真理値はユーザーが検証されたことを意味します。それ以外の場合は、

errorCodes

プロパティにその理由が表示されます。


hostname

は、ユーザーをreCAPTCHAにリダイレクトしたサーバーを表します。多数のドメインを管理し、それらすべてが同じキーペアを共有するようにしたい場合は、

hostname

プロパティを自分で検証することを選択できます。


3.4. 検証失敗

検証に失敗した場合は、例外がスローされます。 reCAPTCHAライブラリはクライアントに新しいチャレンジを作成するように指示する必要があります。

クライアントの登録エラーハンドラで、ライブラリの

grecaptcha

ウィジェットでresetを呼び出すことによってこれを行います。

register(event){
    event.preventDefault();

    var formData= $('form').serialize();
    $.post(serverContext + "user/registration", formData, function(data){
        if(data.message == "success") {
           //success handler
        }
    })
    .fail(function(data) {
        grecaptcha.reset();
        ...

        if(data.responseJSON.error == "InvalidReCaptcha"){
            $("#captchaError").show().html(data.responseJSON.message);
        }
        ...
    }
}


4サーバーリソースの保護

悪意のあるクライアントは、ブラウザのサンドボックスの規則に従う必要はありません。

したがって、私たちのセキュリティの考え方は、公開されているリソースと、それらがどのように悪用される可能性があるかにあるべきです。


4.1. キャッシュ試行

reCAPTCHAを統合することによって、行われたすべての要求がサーバーに要求を検証するためのソケットを作成させることを理解することは重要です。

真のDoS軽減のためには、より階層的なアプローチが必要です。クライアントを4つの失敗したキャプチャ応答に制限する基本キャッシュを実装できます。

public class ReCaptchaAttemptService {
    private int MAX__ATTEMPT = 4;
    private LoadingCache<String, Integer> attemptsCache;

    public ReCaptchaAttemptService() {
        super();
        attemptsCache = CacheBuilder.newBuilder()
          .expireAfterWrite(4, TimeUnit.HOURS).build(new CacheLoader<String, Integer>() {
            @Override
            public Integer load(String key) {
                return 0;
            }
        });
    }

    public void reCaptchaSucceeded(String key) {
        attemptsCache.invalidate(key);
    }

    public void reCaptchaFailed(String key) {
        int attempts = attemptsCache.getUnchecked(key);
        attempts++;
        attemptsCache.put(key, attempts);
    }

    public boolean isBlocked(String key) {
        return attemptsCache.getUnchecked(key) >= MAX__ATTEMPT;
    }
}


4.2. 検証サービスのリファクタリング

クライアントが試行回数の制限を超えた場合は、最初にキャッシュが中止されます。それ以外の場合、失敗した

GoogleResponse

を処理するときに、クライアントの応答にエラーを含む試行が記録されます。検証が成功すると、試行キャッシュがクリアされます。

public class CaptchaService implements ICaptchaService {

    @Autowired
    private ReCaptchaAttemptService reCaptchaAttemptService;

    ...

    @Override
    public void processResponse(String response) {

        ...

        if(reCaptchaAttemptService.isBlocked(getClientIP())) {
            throw new InvalidReCaptchaException("Client exceeded maximum number of failed attempts");
        }

        ...

        GoogleResponse googleResponse = ...

        if(!googleResponse.isSuccess()) {
            if(googleResponse.hasClientError()) {
                reCaptchaAttemptService.reCaptchaFailed(getClientIP());
            }
            throw new ReCaptchaInvalidException("reCaptcha was not successfully validated");
        }
        reCaptchaAttemptService.reCaptchaSucceeded(getClientIP());
    }
}


5結論

この記事では、GoogleのreCAPTCHAライブラリを登録ページに統合し、キャプチャ応答をサーバーサイドの要求で検証するサービスを実装しました。

このチュートリアルの完全な実装はhttps://github.com/eugenp/spring-security-registration/tree/master[githubプロジェクト]で利用可能です – これはMavenベースのプロジェクトなのでインポートと実行が簡単であるべきですそのまま。