Registrierung bei Spring - Integrieren Sie reCAPTCHA

Registrierung bei Spring - reCAPTCHA integrieren

1. Überblick

In diesem Artikel setzen wir dieSpring Security Registration-Reihe fort, indem wirGooglereCAPTCHA zum Registrierungsprozess hinzufügen, um Menschen von Bots zu unterscheiden.

2. Integration von Google reCAPTCHA

Um den reCAPTCHA-Webdienst von Google zu integrieren, müssen wir zuerst unsere Website beim Dienst registrieren, ihre Bibliothek zu unserer Seite hinzufügen und dann die Captcha-Antwort des Nutzers beim Webdienst überprüfen.

Registrieren wir unsere Website unterhttps://www.google.com/recaptcha/admin. Der Registrierungsprozess generiertsite-key undsecret-key für den Zugriff auf den Webdienst.

2.1. Speichern des API-Schlüsselpaars

Wir speichern die Schlüssel inapplication.properties:

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

Und setzen Sie sie Spring mit einer mit@ConfigurationProperties: beschrifteten Bohne aus

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

    private String site;
    private String secret;

    // standard getters and setters
}

2.2. Widget anzeigen

Aufbauend auf dem Tutorial aus der Serie ändern wir jetzt dieregistration.html, um die Google-Bibliothek einzuschließen.

In unserem Registrierungsformular fügen wir das reCAPTCHA-Widget hinzu, das erwartet, dass das Attributdata-sitekey diesite-key enthält.

Das Widget hängtthe request parameter g-recaptcha-response when submitted an:





...





    ...

    
...

3. Serverseitige Validierung

Der neue Anforderungsparameter codiert unseren Site-Schlüssel und eine eindeutige Zeichenfolge, die den erfolgreichen Abschluss der Herausforderung durch den Benutzer angibt.

Da wir dies jedoch nicht selbst erkennen können, können wir nicht darauf vertrauen, dass das, was der Benutzer übermittelt hat, legitim ist. Es wird eine serverseitige Anforderung gestellt, um diecaptcha response mit der Webdienst-API zu validieren.

Der Endpunkt akzeptiert eine HTTP-Anforderung für die URLhttps://www.google.com/recaptcha/api/siteverify mit den Abfrageparameternsecret,response undremoteip.. Er gibt eine JSON-Antwort mit dem folgenden Schema zurück:

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

3.1. Benutzerantwort abrufen

Die Antwort des Benutzers auf die reCAPTCHA-Herausforderung wird aus dem Anforderungsparameterg-recaptcha-response mitHttpServletRequest abgerufen und mit unserenCaptchaService validiert. Jede Ausnahme, die während der Verarbeitung der Antwort ausgelöst wird, bricht den Rest der Registrierungslogik ab:

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. Validierungsservice

Die erhaltene Captcha-Antwort sollte zuerst bereinigt werden. Ein einfacher regulärer Ausdruck wird verwendet.

Wenn die Antwort legitim erscheint, senden wir eine Anfrage an den Webdienst mit densecret-key, dencaptcha response und denIP address des Clients:

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. Objektivierung der Validierung

Eine Java-Bean, die mitJackson Annotationen verziert ist, kapselt die Validierungsantwort:

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

Wie impliziert bedeutet ein Wahrheitswert in der Eigenschaftsuccess, dass der Benutzer validiert wurde. Andernfalls wird die EigenschafterrorCodesmit dem Grund gefüllt.

hostname bezieht sich auf den Server, der den Benutzer zu reCAPTCHA umgeleitet hat. Wenn Sie viele Domänen verwalten und möchten, dass alle dasselbe Schlüsselpaar verwenden, können Sie die Eigenschafthostnameelbst überprüfen.

3.4. Validierungsfehler

Im Falle eines Validierungsfehlers wird eine Ausnahme ausgelöst. Die reCAPTCHA-Bibliothek muss den Client anweisen, eine neue Herausforderung zu erstellen.

Wir tun dies im Registrierungsfehler-Handler des Clients, indem wir im Widgetgrecaptchader Bibliothek das Zurücksetzen aufrufen:

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. Schutz der Serverressourcen

Böswillige Clients müssen die Regeln der Browser-Sandbox nicht befolgen. Unser Sicherheitsbewusstsein sollte sich also auf die Ressourcen konzentrieren, die verfügbar sind, und darauf, wie sie möglicherweise missbraucht werden.

4.1. Versucht den Cache

Es ist wichtig zu verstehen, dass durch die Integration von reCAPTCHA jede Anforderung dazu führt, dass der Server einen Socket erstellt, um die Anforderung zu validieren.

Während ein mehrschichtiger Ansatz für eine echte DoS-Minderung erforderlich wäre; Wir können einen elementaren Cache implementieren, der einen Client auf 4 fehlgeschlagene Captcha-Antworten beschränkt:

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. Refactoring des Validierungsdienstes

Der Cache wird zuerst eingebunden, indem abgebrochen wird, wenn der Client das Versuchslimit überschritten hat. Andernfalls zeichnen wir bei der Verarbeitung eines nicht erfolgreichenGoogleResponse die Versuche, die einen Fehler enthalten, mit der Antwort des Clients auf. Bei erfolgreicher Validierung wird der Versuchs-Cache geleert:

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. Fazit

In diesem Artikel haben wir die reCAPTCHA-Bibliothek von Google in unsere Registrierungsseite integriert und einen Dienst implementiert, um die Captcha-Antwort mit einer serverseitigen Anforderung zu überprüfen.

Die vollständige Implementierung dieses Tutorials ist ingithub project verfügbar. Dies ist ein Maven-basiertes Projekt, daher sollte es einfach zu importieren und auszuführen sein.