Inscription avec Spring - Intégrez reCAPTCHA

1. Vue d'ensemble

Dans ce didacticiel, nous continuerons la série Spring Security Registration en ajoutant Google reCAPTCHA au processus d'inscription afin de différencier l'humain des bots.

2. Intégration du reCAPTCHA de Google

Pour intégrer le service Web reCAPTCHA de Google, nous devons d'abord enregistrer notre site auprès du service, ajouter leur bibliothèque à notre page, puis vérifier la réponse captcha de l'utilisateur avec le service Web.

Enregistrons notre site sur //www.google.com/recaptcha/admin. Le processus d'enregistrement génère une clé du site et clé secrète pour accéder au service Web.

2.1. Stockage de la paire de clés API

Nous stockons les clés dans l' application.properties:

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

Et exposez-les à Spring à l'aide d'un bean annoté avec @ConfigurationProperties:

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

2.2. Affichage du widget

En nous appuyant sur le tutoriel de la série, nous allons maintenant modifier le registration.html pour inclure la bibliothèque de Google.

Dans notre formulaire d'inscription, nous ajoutons le widget reCAPTCHA qui s'attend à ce que l'attribut data-sitekey contienne la clé de site .

Le widget ajoutera le paramètre de requête g-recaptcha-response une fois soumis :

   ...    ...  ... 

3. Validation côté serveur

Le nouveau paramètre de demande code notre clé de site et une chaîne unique identifiant la réussite du défi de l'utilisateur.

Cependant, comme nous ne pouvons pas le discerner nous-mêmes, nous ne pouvons pas croire que ce que l'utilisateur a soumis est légitime. Une requête côté serveur est effectuée pour valider la réponse captcha avec l'API du service Web.

Le point de terminaison accepte une requête HTTP sur l'URL //www.google.com/recaptcha/api/siteverify, avec les paramètres de requête secret , response et remoteip. Il renvoie une réponse json ayant le schéma:

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

3.1. Récupérer la réponse de l'utilisateur

La réponse de l'utilisateur au défi reCAPTCHA est extraite du paramètre de requête g-recaptcha-response à l' aide de HttpServletRequest et validée avec notre CaptchaService . Toute exception levée lors du traitement de la réponse annulera le reste de la logique d'enregistrement:

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. Service de validation

La réponse captcha obtenue doit être nettoyée en premier. Une expression régulière simple est utilisée.

Si la réponse semble légitime, nous faisons alors une demande au service Web avec la clé secrète , la réponse captcha et l' adresse IP du 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. Objectiver la validation

Un bean Java décoré d' annotations Jackson encapsule la réponse de validation:

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

Comme implicite, une valeur de vérité dans la propriété success signifie que l'utilisateur a été validé. Sinon, la propriété errorCodes s'affichera avec la raison.

Le nom d'hôte fait référence au serveur qui a redirigé l'utilisateur vers reCAPTCHA. Si vous gérez de nombreux domaines et souhaitez qu'ils partagent tous la même paire de clés, vous pouvez choisir de vérifier vous-même la propriété du nom d'hôte .

3.4. Échec de la validation

En cas d'échec de la validation, une exception est levée. La bibliothèque reCAPTCHA doit demander au client de créer un nouveau défi.

Nous le faisons dans le gestionnaire d'erreurs d'enregistrement du client, en invoquant reset sur le widget grecaptcha de la bibliothèque :

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. Protection des ressources du serveur

Les clients malveillants n'ont pas besoin d'obéir aux règles du sandbox du navigateur. Notre état d'esprit en matière de sécurité devrait donc être axé sur les ressources exposées et sur la manière dont elles pourraient être utilisées abusivement.

4.1. Cache des tentatives

Il est important de comprendre qu'en intégrant reCAPTCHA, chaque requête effectuée amènera le serveur à créer une socket pour valider la requête.

Bien que nous ayons besoin d'une approche plus en couches pour une véritable atténuation du DoS, nous pouvons implémenter un cache élémentaire qui limite un client à 4 réponses captcha échouées:

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. Refactoriser le service de validation

Le cache est d'abord incorporé par abandon si le client a dépassé la limite de tentatives. Sinon, lors du traitement d'une réponse Google infructueuse, nous enregistrons les tentatives contenant une erreur avec la réponse du client. Une validation réussie efface le cache des tentatives:

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.

Plus tard, nous avons mis à jour la page d'inscription avec la bibliothèque reCAPTCHA v3 de Google et avons constaté que le formulaire d'inscription devenait plus léger car l'utilisateur n'avait plus besoin de prendre aucune mesure.

L'implémentation complète de ce tutoriel est disponible à l'adresse over sur GitHub.