Registrazione con Spring - Integrazione di reCAPTCHA

1. Panoramica

In questo tutorial, continueremo la serie Spring Security Registration aggiungendo Google reCAPTCHA al processo di registrazione per differenziare gli umani dai bot.

2. Integrazione di reCAPTCHA di Google

Per integrare il servizio web reCAPTCHA di Google, dobbiamo prima registrare il nostro sito con il servizio, aggiungere la loro libreria alla nostra pagina e quindi verificare la risposta captcha dell'utente con il servizio web.

Registriamo il nostro sito su //www.google.com/recaptcha/admin. Il processo di registrazione genera un sito-chiave e chiave segreta per l'accesso al web-service.

2.1. Archiviazione della coppia di chiavi API

Memorizziamo le chiavi in application.properties:

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

Ed esponili a Spring usando un bean annotato con @ConfigurationProperties:

@Component @ConfigurationProperties(prefix = "google.recaptcha.key") public class CaptchaSettings { private String site; private String secret; // standard getters and setters }

2.2. Visualizzazione del widget

Basandosi sul tutorial della serie, ora modificheremo il file registration.html per includere la libreria di Google.

All'interno del nostro modulo di registrazione, aggiungiamo il widget reCAPTCHA che prevede che l'attributo data-sitekey contenga la chiave del sito .

Il widget aggiungerà il parametro di richiesta g-recaptcha-response quando inviato :

   ...    ...  ... 

3. Convalida lato server

Il nuovo parametro di richiesta codifica la chiave del nostro sito e una stringa univoca che identifica il completamento riuscito della sfida da parte dell'utente.

Tuttavia, poiché non possiamo discernere ciò da soli, non possiamo fidarci di ciò che l'utente ha inviato è legittimo. Viene effettuata una richiesta lato server per convalidare la risposta captcha con l'API del servizio web.

L'endpoint accetta una richiesta HTTP sull'URL //www.google.com/recaptcha/api/siteverify, con i parametri di query secret , response e remoteip. Restituisce una risposta json con lo schema:

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

3.1. Recupera la risposta dell'utente

La risposta dell'utente alla sfida reCAPTCHA viene recuperata dal parametro di richiesta g-recaptcha-response utilizzando HttpServletRequest e convalidata con il nostro CaptchaService . Qualsiasi eccezione generata durante l'elaborazione della risposta interromperà il resto della logica di registrazione:

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. Servizio di convalida

La risposta captcha ottenuta deve essere prima disinfettata. Viene utilizzata una semplice espressione regolare.

Se la risposta sembra legittima, effettuiamo una richiesta al servizio Web con la chiave segreta , la risposta captcha e l' indirizzo IP del client :

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. Oggettivare la convalida

Un Java-bean decorato con annotazioni Jackson incapsula la risposta di convalida:

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

Come implicito, un valore di verità nella proprietà success indica che l'utente è stato convalidato. In caso contrario, la proprietà errorCodes verrà compilata con il motivo.

Il nome host si riferisce al server che ha reindirizzato l'utente a reCAPTCHA. Se gestisci molti domini e desideri che tutti condividano la stessa coppia di chiavi, puoi scegliere di verificare tu stesso la proprietà del nome host .

3.4. Validazione non riuscita

In caso di errore di convalida, viene generata un'eccezione. La libreria reCAPTCHA deve istruire il cliente a creare una nuova sfida.

Lo facciamo nel gestore degli errori di registrazione del client, invocando reset sul widget grecaptcha della libreria :

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. Protezione delle risorse del server

I client dannosi non devono obbedire alle regole della sandbox del browser. Quindi la nostra mentalità di sicurezza dovrebbe essere rivolta alle risorse esposte e al modo in cui potrebbero essere abusate.

4.1. Tentativi di cache

È importante capire che integrando reCAPTCHA, ogni richiesta effettuata farà sì che il server crei un socket per convalidare la richiesta.

Anche se avremmo bisogno di un approccio più stratificato per una vera mitigazione DoS, possiamo implementare una cache elementare che limita un client a 4 risposte captcha non riuscite:

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 del servizio di convalida

La cache viene incorporata per prima interrompendosi se il client ha superato il limite di tentativi. In caso contrario, durante l'elaborazione di una risposta Google non riuscita , registriamo i tentativi contenenti un errore con la risposta del cliente. La convalida riuscita cancella la cache dei tentativi:

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.

Successivamente, abbiamo aggiornato la pagina di registrazione con la libreria reCAPTCHA v3 di Google e abbiamo visto che il modulo di registrazione diventa più snello perché l'utente non ha più bisogno di intraprendere alcuna azione.

L'implementazione completa di questo tutorial è disponibile su GitHub.