Enregistrement avec Spring - Integrate reCAPTCHA
1. Vue d'ensemble
Dans cet article, nous allons continuer la sérieSpring Security Registration en ajoutantGooglereCAPTCHA au processus d'enregistrement afin de différencier l'humain des bots.
2. Intégrer le 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 àhttps://www.google.com/recaptcha/admin. Le processus d'enregistrement génère unsite-key et unsecret-key pour accéder au service Web.
2.1. Stockage de la paire de clés API
Nous stockons les clés dans lesapplication.properties:
google.recaptcha.key.site=6LfaHiITAAAA...
google.recaptcha.key.secret=6LfaHiITAAAA...
Et exposez-les à Spring en utilisant 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 lesregistration.html pour inclure la bibliothèque de Google.
Dans notre formulaire d'inscription, nous ajoutons le widget reCAPTCHA qui s'attend à ce que l'attributdata-sitekey contienne lessite-key.
Le widget ajouterathe request parameter g-recaptcha-response when submitted:
...
...
3. Validation côté serveur
Le nouveau paramètre de requête encode la clé de notre site et une chaîne unique identifiant la réussite du défi par l'utilisateur.
Cependant, puisque 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 lescaptcha response avec l'API du service Web.
Le point de terminaison accepte une requête HTTP sur l'URLhttps://www.google.com/recaptcha/api/siteverify, avec les paramètres de requêtesecret,response etremoteip..Il renvoie une réponse json ayant le schéma:
{
"success": true|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êteg-recaptcha-response à l’aide deHttpServletRequest et validée avec nosCaptchaService. Toute exception levée lors du traitement de la réponse annulera le reste de la logique d'inscription:
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 adressons une requête au service Web avec lessecret-key, lescaptcha response et lesIP address 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(
"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. Objectiver la validation
Un bean Java décoré d'annotationsJackson 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 remplira avec la raison.
Lehostname fait référence au serveur qui a redirigé l'utilisateur vers le 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éhostname.
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 widgetgrecaptcha 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. Protéger les ressources du serveur
Les clients malveillants n'ont pas besoin d'obéir aux règles du sandbox du navigateur. Notre mentalité en matière de sécurité devrait donc être axée sur les ressources exposées et la manière dont elles pourraient être maltraitées.
4.1. Cache des tentatives
Il est important de comprendre qu'en intégrant reCAPTCHA, chaque demande effectuée entraînera le serveur à créer un socket pour valider la demande.
Bien qu'une approche plus en couches soit nécessaire pour une véritable atténuation de la DoS; Nous pouvons implémenter un cache élémentaire qui limite un client à 4 réponses captcha ayant échoué:
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é en abandonnant si le client a dépassé la limite de tentatives. Sinon, lors du traitement d'unGoogleResponse infructueux, nous enregistrons les tentatives contenant une erreur avec la réponse du client. La 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. Conclusion
Dans cet article, nous avons intégré la bibliothèque reCAPTCHA de Google dans notre page d'inscription et mis en œuvre un service pour vérifier la réponse captcha avec une requête côté serveur.
L'implémentation complète de ce tutoriel est disponible dans legithub project - il s'agit d'un projet basé sur Maven, il devrait donc être facile à importer et à exécuter tel quel.