Registrierung bei Spring - Integrieren Sie reCAPTCHA

1. Übersicht

In diesem Tutorial setzen wir die Spring Security Registration-Reihe fort, indem wir dem Registrierungsprozess Google reCAPTCHA hinzufügen, um Menschen von Bots zu unterscheiden.

2. Integration von reCAPTCHA von Google

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 Benutzers beim Webdienst überprüfen.

Registrieren wir unsere Website unter //www.google.com/recaptcha/admin. Der Registrierungsprozess generiert einen Site-Schlüssel und einen geheimen Schlüssel für den Zugriff auf den Webdienst.

2.1. Speichern des API-Schlüsselpaars

Wir speichern die Schlüssel in der application.properties:

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

Und setzen Sie sie Spring mit einer Bean aus, die mit @ConfigurationProperties versehen ist:

@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 werden wir ändern nun die registration.html Googles Bibliothek aufzunehmen.

In unserem Registrierungsformular fügen wir das reCAPTCHA-Widget hinzu, das erwartet, dass das Attribut data-sitekey den Site-Schlüssel enthält .

Das Widget hängt den Anforderungsparameter g-recaptcha-response an, wenn es gesendet wird :

   ...    ...  ... 

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 selbst nicht erkennen können, können wir nicht darauf vertrauen, dass das, was der Benutzer eingereicht hat, legitim ist. Es wird eine serverseitige Anforderung gestellt, um die Captcha-Antwort mit der Webdienst-API zu validieren .

Der Endpunkt akzeptiert eine HTTP-Anforderung unter der URL //www.google.com/recaptcha/api/siteverify mit den Abfrageparametern secret , response und remoteip. Es gibt eine JSON-Antwort mit dem folgenden Schema zurück:

false, "challenge_ts": timestamp, "hostname": string, "error-codes": [ ... ] 

3.1. Benutzerantwort abrufen

Die Antwort des Benutzers auf die reCAPTCHA-Herausforderung wird mithilfe von HttpServletRequest aus dem Anforderungsparameter g-recaptcha-response abgerufen und mit unserem CaptchaService 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 dem geheimen Schlüssel , der Captcha-Antwort und der IP-Adresse 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( "//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 mit Jackson- Annotationen verzierte Java-Bean 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 Erfolgseigenschaft, dass der Benutzer validiert wurde. Andernfalls wird die errorCodes- Eigenschaft mit dem Grund gefüllt .

Der 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 Eigenschaft hostname selbst überprüfen .

3.4. Validierungsfehler

Bei einem Validierungsfehler 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 das Zurücksetzen im grecaptcha- Widget der Bibliothek 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

Malicious clients do not need to obey the rules of the browser sandbox. So our security mindset should be at the resources exposed and how they might be abused.

4.1. Attempts Cache

It is important to understand that by integrating reCAPTCHA, every request made will cause the server to create a socket to validate the request.

While we'd need a more layered approach for a true DoS mitigation, we can implement an elementary cache that restricts a client to 4 failed captcha responses:

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 the Validation Service

The cache is incorporated first by aborting if the client has exceeded the attempt limit. Otherwise when processing an unsuccessful GoogleResponse we record the attempts containing an error with the client's response. Successful validation clears the attempts cache:

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. Integrating Google's reCAPTCHA v3

Google's reCAPTCHA v3 differs from the previous versions because it doesn't require any user interaction. It simply gives a score for each request that we send and lets us decide what final actions to take for our web application.

Again, to integrate Google's reCAPTCHA 3, we first need to register our site with the service, add their library to our page, and then verify the token response with the web service.

So, let's register our site at //www.google.com/recaptcha/admin/create and, after selecting reCAPTCHA v3, we'll obtain the new secret and site keys.

5.1. Updating application.properties and CaptchaSettings

After registering, we need to update application.properties with the new keys and our chosen score threshold value:

google.recaptcha.key.site=6LefKOAUAAAAAE... google.recaptcha.key.secret=6LefKOAUAAAA... google.recaptcha.key.threshold=0.5

It's important to note that the threshold set to 0.5 is a default value and can be tuned over time by analyzing the real threshold values in the Google admin console.

Next, let's update our CaptchaSettings class:

@Component @ConfigurationProperties(prefix = "google.recaptcha.key") public class CaptchaSettings { // ... other properties private float threshold; // standard getters and setters }

5.2. Front-End Integration

We'll now modify the registration.html to include Google's library with our site key.

Inside our registration form, we add a hidden field that will store the response token received from the call to the grecaptcha.execute function:

   ...    ...  ...  ...  ...  ... var siteKey = /*[[${@captchaService.getReCaptchaSite()}]]*/; grecaptcha.execute(siteKey, {action: /*[[${T(com.baeldung.captcha.CaptchaService).REGISTER_ACTION}]]*/}).then(function(response) { $('#response').val(response); var formData= $('form').serialize();

5.3. Server-Side Validation

We'll have to make the same server-side request seen in reCAPTCHA Server-Side Validation to validate the response token with the web service API.

The response JSON object will contain two additional properties:

{ ... "score": number, "action": string }

The score is based on the user's interactions and is a value between 0 (very likely a bot) and 1.0 (very likely a human).

Action is a new concept that Google introduced so that we can execute many reCAPTCHA requests on the same web page.

An action must be specified every time we execute the reCAPTCHA v3. And, we have to verify that the value of the action property in the response corresponds to the expected name.

5.4. Retrieve the Response Token

The reCAPTCHA v3 response token is retrieved from the response request parameter using HttpServletRequest and validated with our CaptchaService. The mechanism is identical to the one seen above in the reCAPTCHA:

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("response"); captchaService.processResponse(response, CaptchaService.REGISTER_ACTION); // rest of implementation } ... }

5.5. Refactoring the Validation Service With v3

The refactored CaptchaService validation service class contains a processResponse method analog to the processResponse method of the previous version, but it takes care to check the action and the score parameters of the GoogleResponse:

public class CaptchaService implements ICaptchaService { public static final String REGISTER_ACTION = "register"; ... @Override public void processResponse(String response, String action) { ... GoogleResponse googleResponse = restTemplate.getForObject(verifyUri, GoogleResponse.class); if(!googleResponse.isSuccess() || !googleResponse.getAction().equals(action) || googleResponse.getScore() < captchaSettings.getThreshold()) { ... throw new ReCaptchaInvalidException("reCaptcha was not successfully validated"); } reCaptchaAttemptService.reCaptchaSucceeded(getClientIP()); } }

In case validation fails, we'll throw an exception, but note that with v3, there's no reset method to invoke in the JavaScript client.

We'll still have the same implementation seen above for protecting server resources.

5.6. Updating the GoogleResponse Class

We need to add the new properties score and action to the GoogleResponse Java bean:

@JsonPropertyOrder({ "success", "score", "action", "challenge_ts", "hostname", "error-codes" }) public class GoogleResponse { // ... other properties @JsonProperty("score") private float score; @JsonProperty("action") private String action; // standard getters and setters }

6. Conclusion

In this article, we integrated Google's reCAPTCHA library into our registration page and implemented a service to verify the captcha response with a server-side request.

Später haben wir die Registrierungsseite mit der reCAPTCHA v3-Bibliothek von Google aktualisiert und festgestellt, dass das Registrierungsformular schlanker wird, da der Benutzer keine Maßnahmen mehr ergreifen muss.

Die vollständige Implementierung dieses Tutorials finden Sie auf GitHub.