Registratie bij Spring - Integreer reCAPTCHA

1. Overzicht

In deze tutorial gaan we verder met de Spring Security Registration-serie door GooglereCAPTCHA aan het registratieproces om mensen van bots te onderscheiden.

2. Integratie van Google's reCAPTCHA

Om de reCAPTCHA-webservice van Google te integreren, moeten we eerst onze site bij de service registreren, hun bibliotheek aan onze pagina toevoegen en vervolgens het captcha-antwoord van de gebruiker met de webservice verifiëren.

Laten we onze site registreren op //www.google.com/recaptcha/admin. Het registratieproces genereert een site-key en geheime sleutel voor toegang tot de webservice.

2.1. Het API-sleutelpaar opslaan

We bewaren de sleutels in het application.properties:

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

En stel ze bloot aan de lente met een boon met de annotatie @ConfigurationProperties:

@Component @ConfigurationProperties (prefix = "google.recaptcha.key") openbare klasse CaptchaSettings {privé String-site; privé String geheim; // standaard getters en setters}

2.2. De widget weergeven

Voortbouwend op de tutorial uit series, zullen we nu de registration.html om de Google-bibliotheek op te nemen.

In ons registratieformulier voegen we de reCAPTCHA-widget toe die het attribuut verwacht data-sitekey om de site-key.

De widget wordt toegevoegd de verzoekparameter g-recaptcha-reactie wanneer ingediend:

   ...    ...  ... 

3. Validatie aan de serverzijde

De nieuwe verzoekparameter codeert onze sitesleutel en een unieke tekenreeks die aangeeft of de gebruiker de uitdaging met succes heeft voltooid.

Omdat we dat echter zelf niet kunnen onderscheiden, kunnen we er niet op vertrouwen dat wat de gebruiker heeft ingediend legitiem is. Er wordt een server-side verzoek gedaan om het captcha reactie met de webservice-API.

Het eindpunt accepteert een HTTP-verzoek op de URL //www.google.com/recaptcha/api/siteverify, met de queryparameters geheim, reactie, en remoteip. Het retourneert een json-antwoord met het schema:

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

3.1. Haal de reactie van de gebruiker op

De reactie van de gebruiker op de reCAPTCHA-challenge wordt opgehaald uit de request-parameter g-recaptcha-reactie gebruik makend van HttpServletRequest en gevalideerd met onze CaptchaService. Elke uitzondering die wordt gegenereerd tijdens het verwerken van het antwoord, zal de rest van de registratielogica onderbreken:

openbare klasse RegistrationController {@Autowired privé ICaptchaService captchaService; ... @RequestMapping (value = "/ user / registration", method = RequestMethod.POST) @ResponseBody public GenericResponse registerUserAccount (@Valid UserDto accountDto, HttpServletRequest-verzoek) {String response = request.getParameter ("g-recaptcha-response" ); captchaService.processResponse (antwoord); // Rest van implementatie} ...}

3.2. Validatieservice

Het verkregen captcha-antwoord moet eerst worden opgeschoond. Er wordt een eenvoudige reguliere expressie gebruikt.

Als de reactie er legitiem uitziet, doen we een verzoek aan de webservice met de geheime sleutel, de captcha reactie, en die van de klant IP adres:

openbare klasse CaptchaService implementeert ICaptchaService {@Autowired privé CaptchaSettings captchaSettings; @Autowired privé RestOperations restTemplate; privé statisch Pattern RESPONSE_PATTERN = Pattern.compile ("[A-Za-z0-9 _-] +"); @Override public void processResponse (String response) {if (! ResponseSanityCheck (response)) {throw new InvalidReCaptchaException ("Response bevat ongeldige karakters"); } URI verifyUri = URI.create (String.format ("//www.google.com/recaptcha/api/siteverify?secret=%s&response=%s&remoteip=%s", getReCaptchaSecret (), antwoord, getClientIP ())) ; GoogleResponse googleResponse = restTemplate.getForObject (verifyUri, GoogleResponse.class); if (! googleResponse.isSuccess ()) {throw new ReCaptchaInvalidException ("reCaptcha is niet succesvol gevalideerd"); }} private boolean responseSanityCheck (String response) {return StringUtils.hasLength (antwoord) && RESPONSE_PATTERN.matcher (antwoord) .matches (); }}

3.3. Objectivering van de validatie

Een Javaboon versierd met Jackson annotaties omvat de validatiereactie:

@JsonInclude (JsonInclude.Include.NON_NULL) @JsonIgnoreProperties (ignoreUnknown = true) @JsonPropertyOrder ({"success", "challenge_ts", "hostname", "error-codes"}) openbare klasse GoogleResponse {@JsonProperty ("success") privé booleaans succes; @JsonProperty ("challenge_ts") private String challengeTs; @JsonProperty ("hostnaam") private String hostnaam; @JsonProperty ("foutcodes") privé ErrorCode [] errorCodes; @JsonIgnore openbare boolean hasClientError () {ErrorCode [] errors = getErrorCodes (); if (errors == null) {return false; } for (ErrorCode error: errors) {switch (error) {case InvalidResponse: case MissingResponse: return true; }} return false; } statische enum ErrorCode {MissingSecret, InvalidSecret, MissingResponse, InvalidResponse; private static Map errorsMap = nieuwe HashMap (4); statische {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 openbare statische ErrorCode forValue (String-waarde) {return errorsMap.get (value.toLowerCase ()); }} // standaard getters en setters}

Zoals geïmpliceerd, een waarheidswaarde in de succes eigenschap betekent dat de gebruiker is gevalideerd. Anders de errorCodes eigenschap wordt gevuld met de reden.

De hostnaam verwijst naar de server die de gebruiker heeft omgeleid naar de reCAPTCHA. Als u veel domeinen beheert en wilt dat ze allemaal hetzelfde sleutelpaar delen, kunt u ervoor kiezen om het hostnaam eigendom zelf.

3.4. Validatie mislukt

In het geval van een validatiefout, wordt er een uitzondering gegenereerd. De reCAPTCHA-bibliotheek moet de cliënt instrueren om een ​​nieuwe uitdaging te creëren.

We doen dit in de registratiefout-handler van de klant, door reset aan te roepen op het grecaptcha widget:

register (event) {event.preventDefault (); var formData = $ ('form'). serialize (); $ .post (serverContext + "gebruiker / registratie", formData, functie (data) {if (data.message == "success") {// success handler}}) .fail (functie (data) {grecaptcha.reset ( ); ... if (data.responseJSON.error == "InvalidReCaptcha") {$ ("# captchaError"). show (). html (data.responseJSON.message);} ...}}

4. Serverbronnen beschermen

Kwaadwillende clients hoeven niet te voldoen aan de regels van de browsersandbox. Onze beveiligingsmentaliteit moet dus gericht zijn op de bronnen die worden blootgesteld en hoe ze kunnen worden misbruikt.

4.1. Pogingen cachegeheugen

Het is belangrijk om te begrijpen dat door reCAPTCHA te integreren, elk verzoek ervoor zorgt dat de server een socket maakt om het verzoek te valideren.

Hoewel we een meer gelaagde aanpak nodig hebben voor een echte DoS-beperking, kunnen we een elementaire cache implementeren die een client beperkt tot 4 mislukte captcha-reacties:

openbare klasse ReCaptchaAttemptService {privé int MAX_ATTEMPT = 4; privé LoadingCache pogingenCache; openbare ReCaptchaAttemptService () {super (); pogingenCache = CacheBuilder.newBuilder () .expireAfterWrite (4, TimeUnit.HOURS) .build (nieuwe CacheLoader () {@Override publieke integer belasting (String key) {return 0;}}); } public void reCaptchaSucceeded (String-sleutel) {pogingenCache.invalidate (sleutel); } public void reCaptchaFailed (String-sleutel) {int pogingen = pogingenCache.getUnchecked (sleutel); pogingen ++; pogingenCache.put (sleutel, pogingen); } openbare boolean isBlocked (String-sleutel) {return pogingenCache.getUnchecked (sleutel)> = MAX_ATTEMPT; }}

4.2. Herstructurering van de validatiedienst

De cache wordt eerst opgenomen door af te breken als de client de limiet voor pogingen heeft overschreden. Anders bij het verwerken van een mislukt GoogleResponse we registreren de pogingen met een fout met het antwoord van de klant. Succesvolle validatie wist de cache van de pogingen:

openbare klasse CaptchaService implementeert ICaptchaService {@Autowired private ReCaptchaAttemptService reCaptchaAttemptService; ... @Override public void processResponse (String response) {... if (reCaptchaAttemptService.isBlocked (getClientIP ())) {throw new InvalidReCaptchaException ("Client heeft maximum aantal mislukte pogingen overschreden"); } ... GoogleResponse googleResponse = ... if (! GoogleResponse.isSuccess ()) {if (googleResponse.hasClientError ()) {reCaptchaAttemptService.reCaptchaFailed (getClientIP ()); } throw new ReCaptchaInvalidException ("reCaptcha is niet succesvol gevalideerd"); } reCaptchaAttemptService.reCaptchaSucceeded (getClientIP ()); }}

5. Integratie van Google's reCAPTCHA v3

Google's reCAPTCHA v3 verschilt van de vorige versies omdat het geen gebruikersinteractie vereist. Het geeft eenvoudigweg een score voor elk verzoek dat we verzenden en laat ons beslissen welke laatste acties we moeten ondernemen voor onze webapplicatie.

Nogmaals, om Google's reCAPTCHA 3 te integreren, moeten we eerst onze site bij de service registreren, hun bibliotheek aan onze pagina toevoegen en vervolgens de tokenreactie met de webservice verifiëren.

Dus laten we onze site registreren op //www.google.com/recaptcha/admin/create en, na het selecteren van reCAPTCHA v3, zullen we de nieuwe geheime en sitesleutels verkrijgen.

5.1. Updaten application.properties en CaptchaSettings

Na registratie moeten we updaten application.properties met de nieuwe sleutels en de door ons gekozen drempelwaarde voor de score:

google.recaptcha.key.site = 6LefKOAUAAAAAE ... google.recaptcha.key.secret = 6LefKOAUAAAA ... google.recaptcha.key.threshold = 0,5

Het is belangrijk op te merken dat de drempel is ingesteld op 0.5 is een standaardwaarde en kan in de loop van de tijd worden aangepast door de echte drempelwaarden in de Google-beheerconsole te analyseren.

Laten we vervolgens onze CaptchaSettings klasse:

@Component @ConfigurationProperties (prefix = "google.recaptcha.key") openbare klasse CaptchaSettings {// ... andere eigenschappen privé zwevende drempel; // standaard getters en setters}

5.2. Front-end-integratie

We gaan nu het registration.html om de Google-bibliotheek op te nemen met onze sitesleutel.

In ons registratieformulier voegen we een verborgen veld toe waarin het antwoordtoken wordt opgeslagen dat is ontvangen van de oproep naar het grecaptcha.uitvoeren functie:

   ... ... ... ... ... ... var siteKey = /*[[${@captchaService.getReCaptchaSite()}//??????????? grecaptcha.execute (siteKey, {action: / * val (antwoord); var formData = $ ('form'). serialize ();

5.3. Validatie aan de serverzijde

We zullen hetzelfde server-side verzoek moeten doen als in reCAPTCHA Server-Side Validation om het respons-token te valideren met de webservice-API.

Het antwoord-JSON-object bevat twee aanvullende eigenschappen:

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

De score is gebaseerd op de interacties van de gebruiker en is een waarde tussen 0 (zeer waarschijnlijk een bot) en 1,0 (zeer waarschijnlijk een mens).

Actie is een nieuw concept dat Google heeft geïntroduceerd, zodat we veel reCAPTCHA-verzoeken op dezelfde webpagina kunnen uitvoeren.

Elke keer dat we de reCAPTCHA v3. En we moeten verifiëren dat de waarde van de actie eigenschap in het antwoord komt overeen met de verwachte naam.

5.4. Haal het antwoordtoken op

Het reCAPTCHA v3-antwoordtoken wordt opgehaald uit het reactie verzoekparameter met HttpServletRequest en gevalideerd met onze CaptchaService. Het mechanisme is identiek aan het mechanisme hierboven in de reCAPTCHA:

openbare klasse RegistrationController {@Autowired privé ICaptchaService captchaService; ... @RequestMapping (waarde = "/ gebruiker / registratie", method = RequestMethod.POST) @ResponseBody openbare GenericResponse registerUserAccount (@Valid UserDto accountDto, HttpServletRequest-verzoek) {String response = request.getParameter ("response"); captchaService.processResponse (antwoord, CaptchaService.REGISTER_ACTION); // rest van implementatie} ...}

5.5. Herstructurering van de validatieservice met v3

De refactored CaptchaService validatieserviceklasse bevat een processResponse methode analoog aan de processResponse methode van de vorige versie, maar u moet de actie en de score parameters van de GoogleResponse:

openbare klasse CaptchaService implementeert ICaptchaService {openbare statische laatste 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 is niet succesvol gevalideerd" ); } reCaptchaAttemptService.reCaptchaSucceeded (getClientIP ()); }}

Als de validatie mislukt, gooien we een uitzondering, maar houd er rekening mee dat er met v3 geen resetten methode om aan te roepen in de JavaScript-client.

We hebben nog steeds dezelfde implementatie als hierboven voor het beschermen van serverbronnen.

5.6. Updaten van het GoogleResponse Klasse

We moeten de nieuwe eigenschappen toevoegen score en actie naar de GoogleResponse Javaboon:

@JsonPropertyOrder ({"success", "score", "action", "challenge_ts", "hostname", "error-codes"}) openbare klasse GoogleResponse {// ... andere eigenschappen @JsonProperty ("score") privé float score; @JsonProperty ("action") privé String-actie; // standaard getters en setters}

6. Conclusie

In dit artikel hebben we de reCAPTCHA-bibliotheek van Google in onze registratiepagina geïntegreerd en een service geïmplementeerd om de captcha-respons te verifiëren met een server-side request.

Later hebben we de registratiepagina geüpgraded met de reCAPTCHA v3-bibliotheek van Google en zagen we dat het registratieformulier slanker werd omdat de gebruiker geen actie meer hoeft te ondernemen.

De volledige implementatie van deze tutorial is beschikbaar op GitHub.