Springへの登録 - reCAPTCHAを統合する

Springでの登録– reCAPTCHAの統合

1. 概要

この記事では、人間とボットを区別するために、登録プロセスにGooglereCAPTCHAを追加して、Spring Security Registrationシリーズを続けます。

2. GoogleのreCAPTCHAの統合

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

https://www.google.com/recaptcha/adminでサイトを登録しましょう。 登録プロセスは、Webサービスにアクセスするためのsite-keysecret-keyを生成します。

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. ウィジェットの表示

シリーズのチュートリアルに基づいて、registration.htmlを変更してGoogleのライブラリを含めます。

登録フォーム内に、属性data-sitekeysite-keyが含まれていることを期待するreCAPTCHAウィジェットを追加します。

ウィジェットはthe request parameter g-recaptcha-response when submittedを追加します:





...





    ...

    
...

3. サーバー側の検証

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

ただし、自分自身を識別することはできないため、ユーザーが送信したものが正当であるとは信頼できません。 WebサービスAPIを使用してcaptcha responseを検証するために、サーバー側の要求が行われます。

エンドポイントは、クエリパラメータsecretresponse、およびremoteip.を使用してURLhttps://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-keycaptcha response、およびクライアントのIP addressを使用して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 errorsMap = new HashMap(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 attemptsCache;

    public ReCaptchaAttemptService() {
        super();
        attemptsCache = CacheBuilder.newBuilder()
          .expireAfterWrite(4, TimeUnit.HOURS).build(new CacheLoader() {
            @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ライブラリを登録ページに統合し、サーバー側のリクエストでキャプチャ応答を検証するサービスを実装しました。

このチュートリアルの完全な実装はgithub projectで利用できます。これはMavenベースのプロジェクトであるため、そのままインポートして実行するのは簡単です。