Регистрация в Spring - Интеграция reCAPTCHA

Регистрация в Spring - интегрируйте reCAPTCHA

1. обзор

В этой статье мы продолжим сериюSpring Security Registration, добавивGooglereCAPTCHA к процессу регистрации, чтобы отличать человека от ботов.

2. Интеграция Google reCAPTCHA

Чтобы интегрировать веб-службу reCAPTCHA Google, нам сначала нужно зарегистрировать наш сайт в службе, добавить их библиотеку на нашу страницу, а затем проверить ответ пользователя с помощью кода проверки с помощью веб-службы.

Зарегистрируем наш сайт наhttps://www.google.com/recaptcha/admin. В процессе регистрации создаютсяsite-key иsecret-key для доступа к веб-сервису.

2.1. Хранение пары ключей API

Мы храним ключи вapplication.properties:

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

И выставить их в Spring с помощью bean-компонента, помеченного@ConfigurationProperties:

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

    private String site;
    private String secret;

    // standard getters and setters
}

2.2. Отображение виджета

Основываясь на руководстве из серии, теперь мы изменимregistration.html, чтобы включить библиотеку Google.

Внутри нашей регистрационной формы мы добавляем виджет reCAPTCHA, который ожидает, что атрибутdata-sitekey будет содержатьsite-key.

Виджет добавитthe request parameter g-recaptcha-response when submitted:





...





    ...

    
...

3. Проверка на стороне сервера

Новый параметр запроса кодирует ключ нашего сайта и уникальную строку, идентифицирующую успешное выполнение пользователем задачи.

Тем не менее, поскольку мы не можем различить это сами, мы не можем доверять тому, что предоставил пользователь, является законным. На стороне сервера делается запрос для проверкиcaptcha response с помощью API веб-службы.

Конечная точка принимает HTTP-запрос по URL-адресуhttps://www.google.com/recaptcha/api/siteverify с параметрами запросаsecret,response иremoteip.. Она возвращает ответ json, имеющий схему:

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

3.1. Получить ответ пользователя

Ответ пользователя на запрос reCAPTCHA извлекается из параметра запросаg-recaptcha-response с помощьюHttpServletRequest и подтверждается с помощью нашего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 address клиента:

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. Объективизация валидации

Java-bean, украшенный аннотациямиJackson, инкапсулирует ответ проверки:

@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:

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 потребуется более многоуровневый подход; Мы можем реализовать элементарный кеш, который ограничивает клиента четырьмя ошибочными ответами:

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 на нашу страницу регистрации и реализовали службу для проверки ответа captcha с помощью запроса на стороне сервера.

Полная реализация этого руководства доступна вgithub project - это проект на основе Maven, поэтому его должно быть легко импортировать и запускать как есть.